mirror of
https://github.com/tendermint/tendermint.git
synced 2026-09-25 01:14:28 +00:00
state: move package to internal (#6964)
This commit is contained in:
@@ -9,11 +9,11 @@ import (
|
||||
bc "github.com/tendermint/tendermint/internal/blocksync"
|
||||
cons "github.com/tendermint/tendermint/internal/consensus"
|
||||
"github.com/tendermint/tendermint/internal/p2p"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/libs/service"
|
||||
tmSync "github.com/tendermint/tendermint/libs/sync"
|
||||
bcproto "github.com/tendermint/tendermint/proto/tendermint/blocksync"
|
||||
sm "github.com/tendermint/tendermint/state"
|
||||
"github.com/tendermint/tendermint/store"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
@@ -15,11 +15,11 @@ import (
|
||||
"github.com/tendermint/tendermint/internal/p2p"
|
||||
"github.com/tendermint/tendermint/internal/p2p/p2ptest"
|
||||
"github.com/tendermint/tendermint/internal/proxy"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
sf "github.com/tendermint/tendermint/internal/state/test/factory"
|
||||
"github.com/tendermint/tendermint/internal/test/factory"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
bcproto "github.com/tendermint/tendermint/proto/tendermint/blocksync"
|
||||
sm "github.com/tendermint/tendermint/state"
|
||||
sf "github.com/tendermint/tendermint/state/test/factory"
|
||||
"github.com/tendermint/tendermint/store"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/tendermint/tendermint/internal/p2p"
|
||||
"github.com/tendermint/tendermint/internal/state"
|
||||
bcproto "github.com/tendermint/tendermint/proto/tendermint/blocksync"
|
||||
"github.com/tendermint/tendermint/state"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package v2
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
tmState "github.com/tendermint/tendermint/state"
|
||||
tmState "github.com/tendermint/tendermint/internal/state"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
cons "github.com/tendermint/tendermint/internal/consensus"
|
||||
"github.com/tendermint/tendermint/state"
|
||||
"github.com/tendermint/tendermint/internal/state"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
tmState "github.com/tendermint/tendermint/state"
|
||||
tmState "github.com/tendermint/tendermint/internal/state"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@ import (
|
||||
cons "github.com/tendermint/tendermint/internal/consensus"
|
||||
tmsync "github.com/tendermint/tendermint/internal/libs/sync"
|
||||
"github.com/tendermint/tendermint/internal/p2p"
|
||||
"github.com/tendermint/tendermint/internal/state"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/libs/sync"
|
||||
bcproto "github.com/tendermint/tendermint/proto/tendermint/blocksync"
|
||||
"github.com/tendermint/tendermint/state"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
|
||||
@@ -22,12 +22,12 @@ import (
|
||||
"github.com/tendermint/tendermint/internal/p2p"
|
||||
"github.com/tendermint/tendermint/internal/p2p/conn"
|
||||
"github.com/tendermint/tendermint/internal/proxy"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
sf "github.com/tendermint/tendermint/internal/state/test/factory"
|
||||
"github.com/tendermint/tendermint/internal/test/factory"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/libs/service"
|
||||
bcproto "github.com/tendermint/tendermint/proto/tendermint/blocksync"
|
||||
sm "github.com/tendermint/tendermint/state"
|
||||
sf "github.com/tendermint/tendermint/state/test/factory"
|
||||
tmstore "github.com/tendermint/tendermint/store"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/tendermint/tendermint/state"
|
||||
"github.com/tendermint/tendermint/internal/state"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@ import (
|
||||
tmsync "github.com/tendermint/tendermint/internal/libs/sync"
|
||||
mempoolv0 "github.com/tendermint/tendermint/internal/mempool/v0"
|
||||
"github.com/tendermint/tendermint/internal/p2p"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
"github.com/tendermint/tendermint/internal/test/factory"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
tmcons "github.com/tendermint/tendermint/proto/tendermint/consensus"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
sm "github.com/tendermint/tendermint/state"
|
||||
"github.com/tendermint/tendermint/store"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
cstypes "github.com/tendermint/tendermint/internal/consensus/types"
|
||||
tmsync "github.com/tendermint/tendermint/internal/libs/sync"
|
||||
mempoolv0 "github.com/tendermint/tendermint/internal/mempool/v0"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
"github.com/tendermint/tendermint/internal/test/factory"
|
||||
tmbytes "github.com/tendermint/tendermint/libs/bytes"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
@@ -33,7 +34,6 @@ import (
|
||||
tmtime "github.com/tendermint/tendermint/libs/time"
|
||||
"github.com/tendermint/tendermint/privval"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
sm "github.com/tendermint/tendermint/state"
|
||||
"github.com/tendermint/tendermint/store"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"github.com/tendermint/tendermint/abci/example/code"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
mempl "github.com/tendermint/tendermint/internal/mempool"
|
||||
sm "github.com/tendermint/tendermint/state"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
"github.com/tendermint/tendermint/store"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ package mocks
|
||||
|
||||
import (
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
state "github.com/tendermint/tendermint/state"
|
||||
state "github.com/tendermint/tendermint/internal/state"
|
||||
)
|
||||
|
||||
// ConsSyncReactor is an autogenerated mock type for the ConsSyncReactor type
|
||||
|
||||
@@ -4,7 +4,7 @@ package mocks
|
||||
|
||||
import (
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
state "github.com/tendermint/tendermint/state"
|
||||
state "github.com/tendermint/tendermint/internal/state"
|
||||
|
||||
time "time"
|
||||
)
|
||||
|
||||
@@ -8,13 +8,13 @@ import (
|
||||
cstypes "github.com/tendermint/tendermint/internal/consensus/types"
|
||||
tmsync "github.com/tendermint/tendermint/internal/libs/sync"
|
||||
"github.com/tendermint/tendermint/internal/p2p"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
"github.com/tendermint/tendermint/libs/bits"
|
||||
tmevents "github.com/tendermint/tendermint/libs/events"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/libs/service"
|
||||
tmcons "github.com/tendermint/tendermint/proto/tendermint/consensus"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
sm "github.com/tendermint/tendermint/state"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
|
||||
@@ -23,12 +23,12 @@ import (
|
||||
mempoolv0 "github.com/tendermint/tendermint/internal/mempool/v0"
|
||||
"github.com/tendermint/tendermint/internal/p2p"
|
||||
"github.com/tendermint/tendermint/internal/p2p/p2ptest"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
statemocks "github.com/tendermint/tendermint/internal/state/mocks"
|
||||
"github.com/tendermint/tendermint/internal/test/factory"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
tmpubsub "github.com/tendermint/tendermint/libs/pubsub"
|
||||
tmcons "github.com/tendermint/tendermint/proto/tendermint/consensus"
|
||||
sm "github.com/tendermint/tendermint/state"
|
||||
statemocks "github.com/tendermint/tendermint/state/mocks"
|
||||
"github.com/tendermint/tendermint/store"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/crypto/merkle"
|
||||
"github.com/tendermint/tendermint/internal/proxy"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
sm "github.com/tendermint/tendermint/state"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
|
||||
@@ -14,10 +14,10 @@ import (
|
||||
|
||||
cfg "github.com/tendermint/tendermint/config"
|
||||
"github.com/tendermint/tendermint/internal/proxy"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
tmos "github.com/tendermint/tendermint/libs/os"
|
||||
tmpubsub "github.com/tendermint/tendermint/libs/pubsub"
|
||||
sm "github.com/tendermint/tendermint/state"
|
||||
"github.com/tendermint/tendermint/store"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
@@ -27,14 +27,14 @@ import (
|
||||
cryptoenc "github.com/tendermint/tendermint/crypto/encoding"
|
||||
mempl "github.com/tendermint/tendermint/internal/mempool"
|
||||
"github.com/tendermint/tendermint/internal/proxy"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
sf "github.com/tendermint/tendermint/internal/state/test/factory"
|
||||
"github.com/tendermint/tendermint/internal/test/factory"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
tmrand "github.com/tendermint/tendermint/libs/rand"
|
||||
"github.com/tendermint/tendermint/privval"
|
||||
tmstate "github.com/tendermint/tendermint/proto/tendermint/state"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
sm "github.com/tendermint/tendermint/state"
|
||||
sf "github.com/tendermint/tendermint/state/test/factory"
|
||||
"github.com/tendermint/tendermint/store"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
cstypes "github.com/tendermint/tendermint/internal/consensus/types"
|
||||
"github.com/tendermint/tendermint/internal/libs/fail"
|
||||
tmsync "github.com/tendermint/tendermint/internal/libs/sync"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
tmevents "github.com/tendermint/tendermint/libs/events"
|
||||
tmjson "github.com/tendermint/tendermint/libs/json"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
@@ -27,7 +28,6 @@ import (
|
||||
"github.com/tendermint/tendermint/privval"
|
||||
tmgrpc "github.com/tendermint/tendermint/privval/grpc"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
sm "github.com/tendermint/tendermint/state"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ import (
|
||||
"github.com/tendermint/tendermint/abci/example/kvstore"
|
||||
cfg "github.com/tendermint/tendermint/config"
|
||||
"github.com/tendermint/tendermint/internal/proxy"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/privval"
|
||||
sm "github.com/tendermint/tendermint/state"
|
||||
"github.com/tendermint/tendermint/store"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
@@ -14,9 +14,9 @@ import (
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
|
||||
clist "github.com/tendermint/tendermint/internal/libs/clist"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
sm "github.com/tendermint/tendermint/state"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@ import (
|
||||
|
||||
"github.com/tendermint/tendermint/internal/evidence"
|
||||
"github.com/tendermint/tendermint/internal/evidence/mocks"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
smmocks "github.com/tendermint/tendermint/internal/state/mocks"
|
||||
sf "github.com/tendermint/tendermint/internal/state/test/factory"
|
||||
"github.com/tendermint/tendermint/internal/test/factory"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
sm "github.com/tendermint/tendermint/state"
|
||||
smmocks "github.com/tendermint/tendermint/state/mocks"
|
||||
sf "github.com/tendermint/tendermint/state/test/factory"
|
||||
"github.com/tendermint/tendermint/store"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
"github.com/tendermint/tendermint/version"
|
||||
|
||||
@@ -20,9 +20,9 @@ import (
|
||||
"github.com/tendermint/tendermint/internal/evidence/mocks"
|
||||
"github.com/tendermint/tendermint/internal/p2p"
|
||||
"github.com/tendermint/tendermint/internal/p2p/p2ptest"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
sm "github.com/tendermint/tendermint/state"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
|
||||
@@ -15,11 +15,11 @@ import (
|
||||
"github.com/tendermint/tendermint/crypto/tmhash"
|
||||
"github.com/tendermint/tendermint/internal/evidence"
|
||||
"github.com/tendermint/tendermint/internal/evidence/mocks"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
smmocks "github.com/tendermint/tendermint/internal/state/mocks"
|
||||
"github.com/tendermint/tendermint/internal/test/factory"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
sm "github.com/tendermint/tendermint/state"
|
||||
smmocks "github.com/tendermint/tendermint/state/mocks"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@ import (
|
||||
|
||||
"github.com/tendermint/tendermint/config"
|
||||
"github.com/tendermint/tendermint/internal/inspect/rpc"
|
||||
rpccore "github.com/tendermint/tendermint/internal/rpc/core"
|
||||
"github.com/tendermint/tendermint/internal/state"
|
||||
"github.com/tendermint/tendermint/internal/state/indexer"
|
||||
"github.com/tendermint/tendermint/internal/state/indexer/sink"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
tmstrings "github.com/tendermint/tendermint/libs/strings"
|
||||
rpccore "github.com/tendermint/tendermint/rpc/core"
|
||||
"github.com/tendermint/tendermint/state"
|
||||
"github.com/tendermint/tendermint/state/indexer"
|
||||
"github.com/tendermint/tendermint/state/indexer/sink"
|
||||
"github.com/tendermint/tendermint/store"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
|
||||
|
||||
@@ -16,13 +16,13 @@ import (
|
||||
abcitypes "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/config"
|
||||
"github.com/tendermint/tendermint/internal/inspect"
|
||||
"github.com/tendermint/tendermint/internal/state/indexer"
|
||||
indexermocks "github.com/tendermint/tendermint/internal/state/indexer/mocks"
|
||||
statemocks "github.com/tendermint/tendermint/internal/state/mocks"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/libs/pubsub/query"
|
||||
"github.com/tendermint/tendermint/proto/tendermint/state"
|
||||
httpclient "github.com/tendermint/tendermint/rpc/client/http"
|
||||
"github.com/tendermint/tendermint/state/indexer"
|
||||
indexermocks "github.com/tendermint/tendermint/state/indexer/mocks"
|
||||
statemocks "github.com/tendermint/tendermint/state/mocks"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ import (
|
||||
|
||||
"github.com/tendermint/tendermint/config"
|
||||
"github.com/tendermint/tendermint/internal/consensus"
|
||||
"github.com/tendermint/tendermint/internal/rpc/core"
|
||||
"github.com/tendermint/tendermint/internal/state"
|
||||
"github.com/tendermint/tendermint/internal/state/indexer"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/libs/pubsub"
|
||||
"github.com/tendermint/tendermint/rpc/core"
|
||||
"github.com/tendermint/tendermint/rpc/jsonrpc/server"
|
||||
"github.com/tendermint/tendermint/state"
|
||||
"github.com/tendermint/tendermint/state/indexer"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# Openapi docs
|
||||
|
||||
Do not forget to update ../openapi/openapi.yaml if making changes to any
|
||||
endpoint.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Tendermint RPC
|
||||
|
||||
## Pagination
|
||||
|
||||
Requests that return multiple items will be paginated to 30 items by default.
|
||||
You can specify further pages with the ?page parameter. You can also set a
|
||||
custom page size up to 100 with the ?per_page parameter.
|
||||
|
||||
## Subscribing to events
|
||||
|
||||
The user can subscribe to events emitted by Tendermint, using `/subscribe`. If
|
||||
the maximum number of clients is reached or the client has too many
|
||||
subscriptions, an error will be returned. The subscription timeout is 5 sec.
|
||||
Each subscription has a buffer to accommodate short bursts of events or some
|
||||
slowness in clients. If the buffer gets full, the subscription will be canceled
|
||||
("client is not pulling messages fast enough"). If Tendermint exits, all
|
||||
subscriptions are canceled ("Tendermint exited"). The user can unsubscribe
|
||||
using either `/unsubscribe` or `/unsubscribe_all`.
|
||||
@@ -0,0 +1,42 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/internal/proxy"
|
||||
"github.com/tendermint/tendermint/libs/bytes"
|
||||
ctypes "github.com/tendermint/tendermint/rpc/coretypes"
|
||||
rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types"
|
||||
)
|
||||
|
||||
// ABCIQuery queries the application for some information.
|
||||
// More: https://docs.tendermint.com/master/rpc/#/ABCI/abci_query
|
||||
func (env *Environment) ABCIQuery(
|
||||
ctx *rpctypes.Context,
|
||||
path string,
|
||||
data bytes.HexBytes,
|
||||
height int64,
|
||||
prove bool,
|
||||
) (*ctypes.ResultABCIQuery, error) {
|
||||
resQuery, err := env.ProxyAppQuery.QuerySync(ctx.Context(), abci.RequestQuery{
|
||||
Path: path,
|
||||
Data: data,
|
||||
Height: height,
|
||||
Prove: prove,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ctypes.ResultABCIQuery{Response: *resQuery}, nil
|
||||
}
|
||||
|
||||
// ABCIInfo gets some info about the application.
|
||||
// More: https://docs.tendermint.com/master/rpc/#/ABCI/abci_info
|
||||
func (env *Environment) ABCIInfo(ctx *rpctypes.Context) (*ctypes.ResultABCIInfo, error) {
|
||||
resInfo, err := env.ProxyAppQuery.InfoSync(ctx.Context(), proxy.RequestInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ctypes.ResultABCIInfo{Response: *resInfo}, nil
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/tendermint/tendermint/internal/state/indexer"
|
||||
"github.com/tendermint/tendermint/libs/bytes"
|
||||
tmmath "github.com/tendermint/tendermint/libs/math"
|
||||
tmquery "github.com/tendermint/tendermint/libs/pubsub/query"
|
||||
ctypes "github.com/tendermint/tendermint/rpc/coretypes"
|
||||
rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
// BlockchainInfo gets block headers for minHeight <= height <= maxHeight.
|
||||
//
|
||||
// If maxHeight does not yet exist, blocks up to the current height will be
|
||||
// returned. If minHeight does not exist (due to pruning), earliest existing
|
||||
// height will be used.
|
||||
//
|
||||
// At most 20 items will be returned. Block headers are returned in descending
|
||||
// order (highest first).
|
||||
//
|
||||
// More: https://docs.tendermint.com/master/rpc/#/Info/blockchain
|
||||
func (env *Environment) BlockchainInfo(
|
||||
ctx *rpctypes.Context,
|
||||
minHeight, maxHeight int64) (*ctypes.ResultBlockchainInfo, error) {
|
||||
|
||||
const limit int64 = 20
|
||||
|
||||
var err error
|
||||
minHeight, maxHeight, err = filterMinMax(
|
||||
env.BlockStore.Base(),
|
||||
env.BlockStore.Height(),
|
||||
minHeight,
|
||||
maxHeight,
|
||||
limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
env.Logger.Debug("BlockchainInfo", "maxHeight", maxHeight, "minHeight", minHeight)
|
||||
|
||||
blockMetas := make([]*types.BlockMeta, 0, maxHeight-minHeight+1)
|
||||
for height := maxHeight; height >= minHeight; height-- {
|
||||
blockMeta := env.BlockStore.LoadBlockMeta(height)
|
||||
if blockMeta != nil {
|
||||
blockMetas = append(blockMetas, blockMeta)
|
||||
}
|
||||
}
|
||||
|
||||
return &ctypes.ResultBlockchainInfo{
|
||||
LastHeight: env.BlockStore.Height(),
|
||||
BlockMetas: blockMetas}, nil
|
||||
}
|
||||
|
||||
// error if either min or max are negative or min > max
|
||||
// if 0, use blockstore base for min, latest block height for max
|
||||
// enforce limit.
|
||||
func filterMinMax(base, height, min, max, limit int64) (int64, int64, error) {
|
||||
// filter negatives
|
||||
if min < 0 || max < 0 {
|
||||
return min, max, ctypes.ErrZeroOrNegativeHeight
|
||||
}
|
||||
|
||||
// adjust for default values
|
||||
if min == 0 {
|
||||
min = 1
|
||||
}
|
||||
if max == 0 {
|
||||
max = height
|
||||
}
|
||||
|
||||
// limit max to the height
|
||||
max = tmmath.MinInt64(height, max)
|
||||
|
||||
// limit min to the base
|
||||
min = tmmath.MaxInt64(base, min)
|
||||
|
||||
// limit min to within `limit` of max
|
||||
// so the total number of blocks returned will be `limit`
|
||||
min = tmmath.MaxInt64(min, max-limit+1)
|
||||
|
||||
if min > max {
|
||||
return min, max, fmt.Errorf("%w: min height %d can't be greater than max height %d",
|
||||
ctypes.ErrInvalidRequest, min, max)
|
||||
}
|
||||
return min, max, nil
|
||||
}
|
||||
|
||||
// Block gets block at a given height.
|
||||
// If no height is provided, it will fetch the latest block.
|
||||
// More: https://docs.tendermint.com/master/rpc/#/Info/block
|
||||
func (env *Environment) Block(ctx *rpctypes.Context, heightPtr *int64) (*ctypes.ResultBlock, error) {
|
||||
height, err := env.getHeight(env.BlockStore.Height(), heightPtr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blockMeta := env.BlockStore.LoadBlockMeta(height)
|
||||
if blockMeta == nil {
|
||||
return &ctypes.ResultBlock{BlockID: types.BlockID{}, Block: nil}, nil
|
||||
}
|
||||
|
||||
block := env.BlockStore.LoadBlock(height)
|
||||
return &ctypes.ResultBlock{BlockID: blockMeta.BlockID, Block: block}, nil
|
||||
}
|
||||
|
||||
// BlockByHash gets block by hash.
|
||||
// More: https://docs.tendermint.com/master/rpc/#/Info/block_by_hash
|
||||
func (env *Environment) BlockByHash(ctx *rpctypes.Context, hash bytes.HexBytes) (*ctypes.ResultBlock, error) {
|
||||
// N.B. The hash parameter is HexBytes so that the reflective parameter
|
||||
// decoding logic in the HTTP service will correctly translate from JSON.
|
||||
// See https://github.com/tendermint/tendermint/issues/6802 for context.
|
||||
|
||||
block := env.BlockStore.LoadBlockByHash(hash)
|
||||
if block == nil {
|
||||
return &ctypes.ResultBlock{BlockID: types.BlockID{}, Block: nil}, nil
|
||||
}
|
||||
// If block is not nil, then blockMeta can't be nil.
|
||||
blockMeta := env.BlockStore.LoadBlockMeta(block.Height)
|
||||
return &ctypes.ResultBlock{BlockID: blockMeta.BlockID, Block: block}, 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.tendermint.com/master/rpc/#/Info/commit
|
||||
func (env *Environment) Commit(ctx *rpctypes.Context, heightPtr *int64) (*ctypes.ResultCommit, error) {
|
||||
height, err := env.getHeight(env.BlockStore.Height(), heightPtr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blockMeta := env.BlockStore.LoadBlockMeta(height)
|
||||
if blockMeta == nil {
|
||||
return nil, nil
|
||||
}
|
||||
header := blockMeta.Header
|
||||
|
||||
// If the next block has not been committed yet,
|
||||
// use a non-canonical commit
|
||||
if height == env.BlockStore.Height() {
|
||||
commit := env.BlockStore.LoadSeenCommit()
|
||||
// NOTE: we can't yet ensure atomicity of operations in asserting
|
||||
// whether this is the latest height and retrieving the seen commit
|
||||
if commit != nil && commit.Height == height {
|
||||
return ctypes.NewResultCommit(&header, commit, false), nil
|
||||
}
|
||||
}
|
||||
|
||||
// Return the canonical commit (comes from the block at height+1)
|
||||
commit := env.BlockStore.LoadBlockCommit(height)
|
||||
if commit == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return ctypes.NewResultCommit(&header, commit, true), nil
|
||||
}
|
||||
|
||||
// BlockResults gets ABCIResults at a given height.
|
||||
// If no height is provided, it will fetch results for the latest block.
|
||||
//
|
||||
// Results are for the height of the block containing the txs.
|
||||
// Thus response.results.deliver_tx[5] is the results of executing
|
||||
// getBlock(h).Txs[5]
|
||||
// More: https://docs.tendermint.com/master/rpc/#/Info/block_results
|
||||
func (env *Environment) BlockResults(ctx *rpctypes.Context, heightPtr *int64) (*ctypes.ResultBlockResults, error) {
|
||||
height, err := env.getHeight(env.BlockStore.Height(), heightPtr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results, err := env.StateStore.LoadABCIResponses(height)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var totalGasUsed int64
|
||||
for _, tx := range results.GetDeliverTxs() {
|
||||
totalGasUsed += tx.GetGasUsed()
|
||||
}
|
||||
|
||||
return &ctypes.ResultBlockResults{
|
||||
Height: height,
|
||||
TxsResults: results.DeliverTxs,
|
||||
TotalGasUsed: totalGasUsed,
|
||||
BeginBlockEvents: results.BeginBlock.Events,
|
||||
EndBlockEvents: results.EndBlock.Events,
|
||||
ValidatorUpdates: results.EndBlock.ValidatorUpdates,
|
||||
ConsensusParamUpdates: results.EndBlock.ConsensusParamUpdates,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BlockSearch searches for a paginated set of blocks matching BeginBlock and
|
||||
// EndBlock event search criteria.
|
||||
func (env *Environment) BlockSearch(
|
||||
ctx *rpctypes.Context,
|
||||
query string,
|
||||
pagePtr, perPagePtr *int,
|
||||
orderBy string,
|
||||
) (*ctypes.ResultBlockSearch, error) {
|
||||
|
||||
if !indexer.KVSinkEnabled(env.EventSinks) {
|
||||
return nil, fmt.Errorf("block searching is disabled due to no kvEventSink")
|
||||
}
|
||||
|
||||
q, err := tmquery.New(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var kvsink indexer.EventSink
|
||||
for _, sink := range env.EventSinks {
|
||||
if sink.Type() == indexer.KV {
|
||||
kvsink = sink
|
||||
}
|
||||
}
|
||||
|
||||
results, err := kvsink.SearchBlockEvents(ctx.Context(), q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// sort results (must be done before pagination)
|
||||
switch orderBy {
|
||||
case "desc", "":
|
||||
sort.Slice(results, func(i, j int) bool { return results[i] > results[j] })
|
||||
|
||||
case "asc":
|
||||
sort.Slice(results, func(i, j int) bool { return results[i] < results[j] })
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("expected order_by to be either `asc` or `desc` or empty: %w", ctypes.ErrInvalidRequest)
|
||||
}
|
||||
|
||||
// paginate results
|
||||
totalCount := len(results)
|
||||
perPage := env.validatePerPage(perPagePtr)
|
||||
|
||||
page, err := validatePage(pagePtr, perPage, totalCount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
skipCount := validateSkipCount(page, perPage)
|
||||
pageSize := tmmath.MinInt(perPage, totalCount-skipCount)
|
||||
|
||||
apiResults := make([]*ctypes.ResultBlock, 0, pageSize)
|
||||
for i := skipCount; i < skipCount+pageSize; i++ {
|
||||
block := env.BlockStore.LoadBlock(results[i])
|
||||
if block != nil {
|
||||
blockMeta := env.BlockStore.LoadBlockMeta(block.Height)
|
||||
if blockMeta != nil {
|
||||
apiResults = append(apiResults, &ctypes.ResultBlock{
|
||||
Block: block,
|
||||
BlockID: blockMeta.BlockID,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &ctypes.ResultBlockSearch{Blocks: apiResults, TotalCount: totalCount}, nil
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
tmstate "github.com/tendermint/tendermint/proto/tendermint/state"
|
||||
ctypes "github.com/tendermint/tendermint/rpc/coretypes"
|
||||
rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
func TestBlockchainInfo(t *testing.T) {
|
||||
cases := []struct {
|
||||
min, max int64
|
||||
base, height int64
|
||||
limit int64
|
||||
resultLength int64
|
||||
wantErr bool
|
||||
}{
|
||||
|
||||
// min > max
|
||||
{0, 0, 0, 0, 10, 0, true}, // min set to 1
|
||||
{0, 1, 0, 0, 10, 0, true}, // max set to height (0)
|
||||
{0, 0, 0, 1, 10, 1, false}, // max set to height (1)
|
||||
{2, 0, 0, 1, 10, 0, true}, // max set to height (1)
|
||||
{2, 1, 0, 5, 10, 0, true},
|
||||
|
||||
// negative
|
||||
{1, 10, 0, 14, 10, 10, false}, // control
|
||||
{-1, 10, 0, 14, 10, 0, true},
|
||||
{1, -10, 0, 14, 10, 0, true},
|
||||
{-9223372036854775808, -9223372036854775788, 0, 100, 20, 0, true},
|
||||
|
||||
// check base
|
||||
{1, 1, 1, 1, 1, 1, false},
|
||||
{2, 5, 3, 5, 5, 3, false},
|
||||
|
||||
// check limit and height
|
||||
{1, 1, 0, 1, 10, 1, false},
|
||||
{1, 1, 0, 5, 10, 1, false},
|
||||
{2, 2, 0, 5, 10, 1, false},
|
||||
{1, 2, 0, 5, 10, 2, false},
|
||||
{1, 5, 0, 1, 10, 1, false},
|
||||
{1, 5, 0, 10, 10, 5, false},
|
||||
{1, 15, 0, 10, 10, 10, false},
|
||||
{1, 15, 0, 15, 10, 10, false},
|
||||
{1, 15, 0, 15, 20, 15, false},
|
||||
{1, 20, 0, 15, 20, 15, false},
|
||||
{1, 20, 0, 20, 20, 20, false},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
caseString := fmt.Sprintf("test %d failed", i)
|
||||
min, max, err := filterMinMax(c.base, c.height, c.min, c.max, c.limit)
|
||||
if c.wantErr {
|
||||
require.Error(t, err, caseString)
|
||||
} else {
|
||||
require.NoError(t, err, caseString)
|
||||
require.Equal(t, 1+max-min, c.resultLength, caseString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockResults(t *testing.T) {
|
||||
results := &tmstate.ABCIResponses{
|
||||
DeliverTxs: []*abci.ResponseDeliverTx{
|
||||
{Code: 0, Data: []byte{0x01}, Log: "ok", GasUsed: 10},
|
||||
{Code: 0, Data: []byte{0x02}, Log: "ok", GasUsed: 5},
|
||||
{Code: 1, Log: "not ok", GasUsed: 0},
|
||||
},
|
||||
EndBlock: &abci.ResponseEndBlock{},
|
||||
BeginBlock: &abci.ResponseBeginBlock{},
|
||||
}
|
||||
|
||||
env := &Environment{}
|
||||
env.StateStore = sm.NewStore(dbm.NewMemDB())
|
||||
err := env.StateStore.SaveABCIResponses(100, results)
|
||||
require.NoError(t, err)
|
||||
env.BlockStore = mockBlockStore{height: 100}
|
||||
|
||||
testCases := []struct {
|
||||
height int64
|
||||
wantErr bool
|
||||
wantRes *ctypes.ResultBlockResults
|
||||
}{
|
||||
{-1, true, nil},
|
||||
{0, true, nil},
|
||||
{101, true, nil},
|
||||
{100, false, &ctypes.ResultBlockResults{
|
||||
Height: 100,
|
||||
TxsResults: results.DeliverTxs,
|
||||
TotalGasUsed: 15,
|
||||
BeginBlockEvents: results.BeginBlock.Events,
|
||||
EndBlockEvents: results.EndBlock.Events,
|
||||
ValidatorUpdates: results.EndBlock.ValidatorUpdates,
|
||||
ConsensusParamUpdates: results.EndBlock.ConsensusParamUpdates,
|
||||
}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
res, err := env.BlockResults(&rpctypes.Context{}, &tc.height)
|
||||
if tc.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.wantRes, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type mockBlockStore struct {
|
||||
height int64
|
||||
}
|
||||
|
||||
func (mockBlockStore) Base() int64 { return 1 }
|
||||
func (store mockBlockStore) Height() int64 { return store.height }
|
||||
func (store mockBlockStore) Size() int64 { return store.height }
|
||||
func (mockBlockStore) LoadBaseMeta() *types.BlockMeta { return nil }
|
||||
func (mockBlockStore) LoadBlockMeta(height int64) *types.BlockMeta { return nil }
|
||||
func (mockBlockStore) LoadBlock(height int64) *types.Block { return nil }
|
||||
func (mockBlockStore) LoadBlockByHash(hash []byte) *types.Block { return nil }
|
||||
func (mockBlockStore) LoadBlockPart(height int64, index int) *types.Part { return nil }
|
||||
func (mockBlockStore) LoadBlockCommit(height int64) *types.Commit { return nil }
|
||||
func (mockBlockStore) LoadSeenCommit() *types.Commit { return nil }
|
||||
func (mockBlockStore) PruneBlocks(height int64) (uint64, error) { return 0, nil }
|
||||
func (mockBlockStore) SaveBlock(block *types.Block, blockParts *types.PartSet, seenCommit *types.Commit) {
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
cm "github.com/tendermint/tendermint/internal/consensus"
|
||||
tmmath "github.com/tendermint/tendermint/libs/math"
|
||||
ctypes "github.com/tendermint/tendermint/rpc/coretypes"
|
||||
rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
// Validators gets the validator set at the given block height.
|
||||
//
|
||||
// If no height is provided, it will fetch the latest validator set. Note the
|
||||
// validators are sorted by their voting power - this is the canonical order
|
||||
// for the validators in the set as used in computing their Merkle root.
|
||||
//
|
||||
// More: https://docs.tendermint.com/master/rpc/#/Info/validators
|
||||
func (env *Environment) Validators(
|
||||
ctx *rpctypes.Context,
|
||||
heightPtr *int64,
|
||||
pagePtr, perPagePtr *int) (*ctypes.ResultValidators, error) {
|
||||
|
||||
// The latest validator that we know is the NextValidator of the last block.
|
||||
height, err := env.getHeight(env.latestUncommittedHeight(), heightPtr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
validators, err := env.StateStore.LoadValidators(height)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
totalCount := len(validators.Validators)
|
||||
perPage := env.validatePerPage(perPagePtr)
|
||||
page, err := validatePage(pagePtr, perPage, totalCount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
skipCount := validateSkipCount(page, perPage)
|
||||
|
||||
v := validators.Validators[skipCount : skipCount+tmmath.MinInt(perPage, totalCount-skipCount)]
|
||||
|
||||
return &ctypes.ResultValidators{
|
||||
BlockHeight: height,
|
||||
Validators: v,
|
||||
Count: len(v),
|
||||
Total: totalCount}, nil
|
||||
}
|
||||
|
||||
// DumpConsensusState dumps consensus state.
|
||||
// UNSTABLE
|
||||
// More: https://docs.tendermint.com/master/rpc/#/Info/dump_consensus_state
|
||||
func (env *Environment) DumpConsensusState(ctx *rpctypes.Context) (*ctypes.ResultDumpConsensusState, error) {
|
||||
// Get Peer consensus states.
|
||||
|
||||
var peerStates []ctypes.PeerStateInfo
|
||||
switch {
|
||||
case env.P2PPeers != nil:
|
||||
peers := env.P2PPeers.Peers().List()
|
||||
peerStates = make([]ctypes.PeerStateInfo, 0, len(peers))
|
||||
for _, peer := range peers {
|
||||
peerState, ok := peer.Get(types.PeerStateKey).(*cm.PeerState)
|
||||
if !ok { // peer does not have a state yet
|
||||
continue
|
||||
}
|
||||
peerStateJSON, err := peerState.ToJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
peerStates = append(peerStates, ctypes.PeerStateInfo{
|
||||
// Peer basic info.
|
||||
NodeAddress: peer.SocketAddr().String(),
|
||||
// Peer consensus state.
|
||||
PeerState: peerStateJSON,
|
||||
})
|
||||
}
|
||||
case env.PeerManager != nil:
|
||||
peers := env.PeerManager.Peers()
|
||||
peerStates = make([]ctypes.PeerStateInfo, 0, len(peers))
|
||||
for _, pid := range peers {
|
||||
peerState, ok := env.ConsensusReactor.GetPeerState(pid)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
peerStateJSON, err := peerState.ToJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addr := env.PeerManager.Addresses(pid)
|
||||
if len(addr) >= 1 {
|
||||
peerStates = append(peerStates, ctypes.PeerStateInfo{
|
||||
// Peer basic info.
|
||||
NodeAddress: addr[0].String(),
|
||||
// Peer consensus state.
|
||||
PeerState: peerStateJSON,
|
||||
})
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("no peer system configured")
|
||||
}
|
||||
|
||||
// Get self round state.
|
||||
roundState, err := env.ConsensusState.GetRoundStateJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ctypes.ResultDumpConsensusState{
|
||||
RoundState: roundState,
|
||||
Peers: peerStates}, nil
|
||||
}
|
||||
|
||||
// ConsensusState returns a concise summary of the consensus state.
|
||||
// UNSTABLE
|
||||
// More: https://docs.tendermint.com/master/rpc/#/Info/consensus_state
|
||||
func (env *Environment) GetConsensusState(ctx *rpctypes.Context) (*ctypes.ResultConsensusState, error) {
|
||||
// Get self round state.
|
||||
bz, err := env.ConsensusState.GetRoundStateSimpleJSON()
|
||||
return &ctypes.ResultConsensusState{RoundState: bz}, err
|
||||
}
|
||||
|
||||
// ConsensusParams gets the consensus parameters at the given block height.
|
||||
// If no height is provided, it will fetch the latest consensus params.
|
||||
// More: https://docs.tendermint.com/master/rpc/#/Info/consensus_params
|
||||
func (env *Environment) ConsensusParams(
|
||||
ctx *rpctypes.Context,
|
||||
heightPtr *int64) (*ctypes.ResultConsensusParams, error) {
|
||||
|
||||
// The latest consensus params that we know is the consensus params after the
|
||||
// last block.
|
||||
height, err := env.getHeight(env.latestUncommittedHeight(), heightPtr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
consensusParams, err := env.StateStore.LoadConsensusParams(height)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ctypes.ResultConsensusParams{
|
||||
BlockHeight: height,
|
||||
ConsensusParams: consensusParams}, nil
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
ctypes "github.com/tendermint/tendermint/rpc/coretypes"
|
||||
rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types"
|
||||
)
|
||||
|
||||
// UnsafeFlushMempool removes all transactions from the mempool.
|
||||
func (env *Environment) UnsafeFlushMempool(ctx *rpctypes.Context) (*ctypes.ResultUnsafeFlushMempool, error) {
|
||||
env.Mempool.Flush()
|
||||
return &ctypes.ResultUnsafeFlushMempool{}, nil
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
Package core defines the Tendermint RPC endpoints.
|
||||
|
||||
Tendermint ships with its own JSONRPC library -
|
||||
https://github.com/tendermint/tendermint/tree/master/rpc/jsonrpc.
|
||||
|
||||
## Get the list
|
||||
|
||||
An HTTP Get request to the root RPC endpoint shows a list of available endpoints.
|
||||
|
||||
```bash
|
||||
curl 'localhost:26657'
|
||||
```
|
||||
|
||||
> Response:
|
||||
|
||||
```plain
|
||||
Available endpoints:
|
||||
/abci_info
|
||||
/dump_consensus_state
|
||||
/genesis
|
||||
/net_info
|
||||
/num_unconfirmed_txs
|
||||
/status
|
||||
/health
|
||||
/unconfirmed_txs
|
||||
/unsafe_flush_mempool
|
||||
/validators
|
||||
|
||||
Endpoints that require arguments:
|
||||
/abci_query?path=_&data=_&prove=_
|
||||
/block?height=_
|
||||
/blockchain?minHeight=_&maxHeight=_
|
||||
/broadcast_tx_async?tx=_
|
||||
/broadcast_tx_commit?tx=_
|
||||
/broadcast_tx_sync?tx=_
|
||||
/commit?height=_
|
||||
/dial_seeds?seeds=_
|
||||
/dial_persistent_peers?persistent_peers=_
|
||||
/subscribe?event=_
|
||||
/tx?hash=_&prove=_
|
||||
/unsubscribe?event=_
|
||||
```
|
||||
*/
|
||||
package core
|
||||
@@ -0,0 +1,8 @@
|
||||
{{with .PDoc}}
|
||||
{{comment_md .Doc}}
|
||||
{{example_html $ ""}}
|
||||
|
||||
{{range .Funcs}}{{$name_html := html .Name}}## [{{$name_html}}]({{posLink_url $ .Decl}})
|
||||
{{comment_md .Doc}}{{end}}
|
||||
{{end}}
|
||||
---
|
||||
@@ -0,0 +1,215 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
cfg "github.com/tendermint/tendermint/config"
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
"github.com/tendermint/tendermint/internal/consensus"
|
||||
mempl "github.com/tendermint/tendermint/internal/mempool"
|
||||
"github.com/tendermint/tendermint/internal/p2p"
|
||||
"github.com/tendermint/tendermint/internal/proxy"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
"github.com/tendermint/tendermint/internal/state/indexer"
|
||||
"github.com/tendermint/tendermint/internal/statesync"
|
||||
tmjson "github.com/tendermint/tendermint/libs/json"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
ctypes "github.com/tendermint/tendermint/rpc/coretypes"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
const (
|
||||
// see README
|
||||
defaultPerPage = 30
|
||||
maxPerPage = 100
|
||||
|
||||
// SubscribeTimeout is the maximum time we wait to subscribe for an event.
|
||||
// must be less than the server's write timeout (see rpcserver.DefaultConfig)
|
||||
SubscribeTimeout = 5 * time.Second
|
||||
|
||||
// genesisChunkSize is the maximum size, in bytes, of each
|
||||
// chunk in the genesis structure for the chunked API
|
||||
genesisChunkSize = 16 * 1024 * 1024 // 16
|
||||
)
|
||||
|
||||
//----------------------------------------------
|
||||
// These interfaces are used by RPC and must be thread safe
|
||||
|
||||
type consensusState interface {
|
||||
GetState() sm.State
|
||||
GetValidators() (int64, []*types.Validator)
|
||||
GetLastHeight() int64
|
||||
GetRoundStateJSON() ([]byte, error)
|
||||
GetRoundStateSimpleJSON() ([]byte, error)
|
||||
}
|
||||
|
||||
type transport interface {
|
||||
Listeners() []string
|
||||
IsListening() bool
|
||||
NodeInfo() types.NodeInfo
|
||||
}
|
||||
|
||||
type peers interface {
|
||||
AddPersistentPeers([]string) error
|
||||
AddUnconditionalPeerIDs([]string) error
|
||||
AddPrivatePeerIDs([]string) error
|
||||
DialPeersAsync([]string) error
|
||||
Peers() p2p.IPeerSet
|
||||
}
|
||||
|
||||
type consensusReactor interface {
|
||||
WaitSync() bool
|
||||
GetPeerState(peerID types.NodeID) (*consensus.PeerState, bool)
|
||||
}
|
||||
|
||||
type peerManager interface {
|
||||
Peers() []types.NodeID
|
||||
Addresses(types.NodeID) []p2p.NodeAddress
|
||||
}
|
||||
|
||||
//----------------------------------------------
|
||||
// Environment contains objects and interfaces used by the RPC. It is expected
|
||||
// to be setup once during startup.
|
||||
type Environment struct {
|
||||
// external, thread safe interfaces
|
||||
ProxyAppQuery proxy.AppConnQuery
|
||||
ProxyAppMempool proxy.AppConnMempool
|
||||
|
||||
// interfaces defined in types and above
|
||||
StateStore sm.Store
|
||||
BlockStore sm.BlockStore
|
||||
EvidencePool sm.EvidencePool
|
||||
ConsensusState consensusState
|
||||
ConsensusReactor consensusReactor
|
||||
P2PPeers peers
|
||||
|
||||
// Legacy p2p stack
|
||||
P2PTransport transport
|
||||
|
||||
// interfaces for new p2p interfaces
|
||||
PeerManager peerManager
|
||||
|
||||
// objects
|
||||
PubKey crypto.PubKey
|
||||
GenDoc *types.GenesisDoc // cache the genesis structure
|
||||
EventSinks []indexer.EventSink
|
||||
EventBus *types.EventBus // thread safe
|
||||
Mempool mempl.Mempool
|
||||
BlockSyncReactor consensus.BlockSyncReactor
|
||||
StateSyncMetricer statesync.Metricer
|
||||
|
||||
Logger log.Logger
|
||||
|
||||
Config cfg.RPCConfig
|
||||
|
||||
// cache of chunked genesis data.
|
||||
genChunks []string
|
||||
}
|
||||
|
||||
//----------------------------------------------
|
||||
|
||||
func validatePage(pagePtr *int, perPage, totalCount int) (int, error) {
|
||||
// this can only happen if we haven't first run validatePerPage
|
||||
if perPage < 1 {
|
||||
panic(fmt.Errorf("%w (%d)", ctypes.ErrZeroOrNegativePerPage, perPage))
|
||||
}
|
||||
|
||||
if pagePtr == nil { // no page parameter
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
pages := ((totalCount - 1) / perPage) + 1
|
||||
if pages == 0 {
|
||||
pages = 1 // one page (even if it's empty)
|
||||
}
|
||||
page := *pagePtr
|
||||
if page <= 0 || page > pages {
|
||||
return 1, fmt.Errorf("%w expected range: [1, %d], given %d", ctypes.ErrPageOutOfRange, pages, page)
|
||||
}
|
||||
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (env *Environment) validatePerPage(perPagePtr *int) int {
|
||||
if perPagePtr == nil { // no per_page parameter
|
||||
return defaultPerPage
|
||||
}
|
||||
|
||||
perPage := *perPagePtr
|
||||
if perPage < 1 {
|
||||
return defaultPerPage
|
||||
// in unsafe mode there is no max on the page size but in safe mode
|
||||
// we cap it to maxPerPage
|
||||
} else if perPage > maxPerPage && !env.Config.Unsafe {
|
||||
return maxPerPage
|
||||
}
|
||||
return perPage
|
||||
}
|
||||
|
||||
// InitGenesisChunks configures the environment and should be called on service
|
||||
// startup.
|
||||
func (env *Environment) InitGenesisChunks() error {
|
||||
if env.genChunks != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if env.GenDoc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := tmjson.Marshal(env.GenDoc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := 0; i < len(data); i += genesisChunkSize {
|
||||
end := i + genesisChunkSize
|
||||
|
||||
if end > len(data) {
|
||||
end = len(data)
|
||||
}
|
||||
|
||||
env.genChunks = append(env.genChunks, base64.StdEncoding.EncodeToString(data[i:end]))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSkipCount(page, perPage int) int {
|
||||
skipCount := (page - 1) * perPage
|
||||
if skipCount < 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return skipCount
|
||||
}
|
||||
|
||||
// latestHeight can be either latest committed or uncommitted (+1) height.
|
||||
func (env *Environment) getHeight(latestHeight int64, heightPtr *int64) (int64, error) {
|
||||
if heightPtr != nil {
|
||||
height := *heightPtr
|
||||
if height <= 0 {
|
||||
return 0, fmt.Errorf("%w (requested height: %d)", ctypes.ErrZeroOrNegativeHeight, height)
|
||||
}
|
||||
if height > latestHeight {
|
||||
return 0, fmt.Errorf("%w (requested height: %d, blockchain height: %d)",
|
||||
ctypes.ErrHeightExceedsChainHead, height, latestHeight)
|
||||
}
|
||||
base := env.BlockStore.Base()
|
||||
if height < base {
|
||||
return 0, fmt.Errorf("%w (requested height: %d, base height: %d)", ctypes.ErrHeightNotAvailable, height, base)
|
||||
}
|
||||
return height, nil
|
||||
}
|
||||
return latestHeight, nil
|
||||
}
|
||||
|
||||
func (env *Environment) latestUncommittedHeight() int64 {
|
||||
nodeIsSyncing := env.ConsensusReactor.WaitSync()
|
||||
if nodeIsSyncing {
|
||||
return env.BlockStore.Height()
|
||||
}
|
||||
return env.BlockStore.Height() + 1
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPaginationPage(t *testing.T) {
|
||||
cases := []struct {
|
||||
totalCount int
|
||||
perPage int
|
||||
page int
|
||||
newPage int
|
||||
expErr bool
|
||||
}{
|
||||
{0, 10, 1, 1, false},
|
||||
|
||||
{0, 10, 0, 1, false},
|
||||
{0, 10, 1, 1, false},
|
||||
{0, 10, 2, 0, true},
|
||||
|
||||
{5, 10, -1, 0, true},
|
||||
{5, 10, 0, 1, false},
|
||||
{5, 10, 1, 1, false},
|
||||
{5, 10, 2, 0, true},
|
||||
{5, 10, 2, 0, true},
|
||||
|
||||
{5, 5, 1, 1, false},
|
||||
{5, 5, 2, 0, true},
|
||||
{5, 5, 3, 0, true},
|
||||
|
||||
{5, 3, 2, 2, false},
|
||||
{5, 3, 3, 0, true},
|
||||
|
||||
{5, 2, 2, 2, false},
|
||||
{5, 2, 3, 3, false},
|
||||
{5, 2, 4, 0, true},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
p, err := validatePage(&c.page, c.perPage, c.totalCount)
|
||||
if c.expErr {
|
||||
assert.Error(t, err)
|
||||
continue
|
||||
}
|
||||
|
||||
assert.Equal(t, c.newPage, p, fmt.Sprintf("%v", c))
|
||||
}
|
||||
|
||||
// nil case
|
||||
p, err := validatePage(nil, 1, 1)
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, 1, p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaginationPerPage(t *testing.T) {
|
||||
cases := []struct {
|
||||
perPage int
|
||||
newPerPage int
|
||||
}{
|
||||
{0, defaultPerPage},
|
||||
{1, 1},
|
||||
{2, 2},
|
||||
{defaultPerPage, defaultPerPage},
|
||||
{maxPerPage - 1, maxPerPage - 1},
|
||||
{maxPerPage, maxPerPage},
|
||||
{maxPerPage + 1, maxPerPage},
|
||||
}
|
||||
|
||||
env := &Environment{}
|
||||
|
||||
for _, c := range cases {
|
||||
p := env.validatePerPage(&c.perPage)
|
||||
assert.Equal(t, c.newPerPage, p, fmt.Sprintf("%v", c))
|
||||
}
|
||||
|
||||
// nil case
|
||||
p := env.validatePerPage(nil)
|
||||
assert.Equal(t, defaultPerPage, p)
|
||||
|
||||
// test in unsafe mode
|
||||
env.Config.Unsafe = true
|
||||
perPage := 1000
|
||||
p = env.validatePerPage(&perPage)
|
||||
assert.Equal(t, perPage, p)
|
||||
env.Config.Unsafe = false
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
tmpubsub "github.com/tendermint/tendermint/libs/pubsub"
|
||||
tmquery "github.com/tendermint/tendermint/libs/pubsub/query"
|
||||
ctypes "github.com/tendermint/tendermint/rpc/coretypes"
|
||||
rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types"
|
||||
)
|
||||
|
||||
const (
|
||||
// Buffer on the Tendermint (server) side to allow some slowness in clients.
|
||||
subBufferSize = 100
|
||||
)
|
||||
|
||||
// Subscribe for events via WebSocket.
|
||||
// More: https://docs.tendermint.com/master/rpc/#/Websocket/subscribe
|
||||
func (env *Environment) Subscribe(ctx *rpctypes.Context, query string) (*ctypes.ResultSubscribe, error) {
|
||||
addr := ctx.RemoteAddr()
|
||||
|
||||
if env.EventBus.NumClients() >= env.Config.MaxSubscriptionClients {
|
||||
return nil, fmt.Errorf("max_subscription_clients %d reached", env.Config.MaxSubscriptionClients)
|
||||
} else if env.EventBus.NumClientSubscriptions(addr) >= env.Config.MaxSubscriptionsPerClient {
|
||||
return nil, fmt.Errorf("max_subscriptions_per_client %d reached", env.Config.MaxSubscriptionsPerClient)
|
||||
}
|
||||
|
||||
env.Logger.Info("Subscribe to query", "remote", addr, "query", query)
|
||||
|
||||
q, err := tmquery.New(query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse query: %w", err)
|
||||
}
|
||||
|
||||
subCtx, cancel := context.WithTimeout(ctx.Context(), SubscribeTimeout)
|
||||
defer cancel()
|
||||
|
||||
sub, err := env.EventBus.Subscribe(subCtx, addr, q, subBufferSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Capture the current ID, since it can change in the future.
|
||||
subscriptionID := ctx.JSONReq.ID
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case msg := <-sub.Out():
|
||||
var (
|
||||
resultEvent = &ctypes.ResultEvent{Query: query, Data: msg.Data(), Events: msg.Events()}
|
||||
resp = rpctypes.NewRPCSuccessResponse(subscriptionID, resultEvent)
|
||||
)
|
||||
writeCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
if err := ctx.WSConn.WriteRPCResponse(writeCtx, resp); err != nil {
|
||||
env.Logger.Info("Can't write response (slow client)",
|
||||
"to", addr, "subscriptionID", subscriptionID, "err", err)
|
||||
}
|
||||
case <-sub.Canceled():
|
||||
if sub.Err() != tmpubsub.ErrUnsubscribed {
|
||||
var reason string
|
||||
if sub.Err() == nil {
|
||||
reason = "Tendermint exited"
|
||||
} else {
|
||||
reason = sub.Err().Error()
|
||||
}
|
||||
var (
|
||||
err = fmt.Errorf("subscription was canceled (reason: %s)", reason)
|
||||
resp = rpctypes.RPCServerError(subscriptionID, err)
|
||||
)
|
||||
if ok := ctx.WSConn.TryWriteRPCResponse(resp); !ok {
|
||||
env.Logger.Info("Can't write response (slow client)",
|
||||
"to", addr, "subscriptionID", subscriptionID, "err", err)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return &ctypes.ResultSubscribe{}, nil
|
||||
}
|
||||
|
||||
// Unsubscribe from events via WebSocket.
|
||||
// More: https://docs.tendermint.com/master/rpc/#/Websocket/unsubscribe
|
||||
func (env *Environment) Unsubscribe(ctx *rpctypes.Context, query string) (*ctypes.ResultUnsubscribe, error) {
|
||||
args := tmpubsub.UnsubscribeArgs{Subscriber: ctx.RemoteAddr()}
|
||||
env.Logger.Info("Unsubscribe from query", "remote", args.Subscriber, "subscription", query)
|
||||
|
||||
var err error
|
||||
args.Query, err = tmquery.New(query)
|
||||
|
||||
if err != nil {
|
||||
args.ID = query
|
||||
}
|
||||
|
||||
err = env.EventBus.Unsubscribe(ctx.Context(), args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ctypes.ResultUnsubscribe{}, nil
|
||||
}
|
||||
|
||||
// UnsubscribeAll from all events via WebSocket.
|
||||
// More: https://docs.tendermint.com/master/rpc/#/Websocket/unsubscribe_all
|
||||
func (env *Environment) UnsubscribeAll(ctx *rpctypes.Context) (*ctypes.ResultUnsubscribe, error) {
|
||||
addr := ctx.RemoteAddr()
|
||||
env.Logger.Info("Unsubscribe from all", "remote", addr)
|
||||
err := env.EventBus.UnsubscribeAll(ctx.Context(), addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ctypes.ResultUnsubscribe{}, nil
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
ctypes "github.com/tendermint/tendermint/rpc/coretypes"
|
||||
rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
// BroadcastEvidence broadcasts evidence of the misbehavior.
|
||||
// More: https://docs.tendermint.com/master/rpc/#/Evidence/broadcast_evidence
|
||||
func (env *Environment) BroadcastEvidence(
|
||||
ctx *rpctypes.Context,
|
||||
ev types.Evidence) (*ctypes.ResultBroadcastEvidence, error) {
|
||||
|
||||
if ev == nil {
|
||||
return nil, fmt.Errorf("%w: no evidence was provided", ctypes.ErrInvalidRequest)
|
||||
}
|
||||
|
||||
if err := ev.ValidateBasic(); err != nil {
|
||||
return nil, fmt.Errorf("evidence.ValidateBasic failed: %w", err)
|
||||
}
|
||||
|
||||
if err := env.EvidencePool.AddEvidence(ev); err != nil {
|
||||
return nil, fmt.Errorf("failed to add evidence: %w", err)
|
||||
}
|
||||
return &ctypes.ResultBroadcastEvidence{Hash: ev.Hash()}, nil
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
ctypes "github.com/tendermint/tendermint/rpc/coretypes"
|
||||
rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types"
|
||||
)
|
||||
|
||||
// Health gets node health. Returns empty result (200 OK) on success, no
|
||||
// response - in case of an error.
|
||||
// More: https://docs.tendermint.com/master/rpc/#/Info/health
|
||||
func (env *Environment) Health(ctx *rpctypes.Context) (*ctypes.ResultHealth, error) {
|
||||
return &ctypes.ResultHealth{}, nil
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
mempl "github.com/tendermint/tendermint/internal/mempool"
|
||||
tmpubsub "github.com/tendermint/tendermint/libs/pubsub"
|
||||
ctypes "github.com/tendermint/tendermint/rpc/coretypes"
|
||||
rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// NOTE: tx should be signed, but this is only checked at the app level (not by Tendermint!)
|
||||
|
||||
// BroadcastTxAsync returns right away, with no response. Does not wait for
|
||||
// CheckTx nor DeliverTx results.
|
||||
// More: https://docs.tendermint.com/master/rpc/#/Tx/broadcast_tx_async
|
||||
func (env *Environment) BroadcastTxAsync(ctx *rpctypes.Context, tx types.Tx) (*ctypes.ResultBroadcastTx, error) {
|
||||
err := env.Mempool.CheckTx(ctx.Context(), tx, nil, mempl.TxInfo{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ctypes.ResultBroadcastTx{Hash: tx.Hash()}, nil
|
||||
}
|
||||
|
||||
// BroadcastTxSync returns with the response from CheckTx. Does not wait for
|
||||
// DeliverTx result.
|
||||
// More: https://docs.tendermint.com/master/rpc/#/Tx/broadcast_tx_sync
|
||||
func (env *Environment) BroadcastTxSync(ctx *rpctypes.Context, tx types.Tx) (*ctypes.ResultBroadcastTx, error) {
|
||||
resCh := make(chan *abci.Response, 1)
|
||||
err := env.Mempool.CheckTx(
|
||||
ctx.Context(),
|
||||
tx,
|
||||
func(res *abci.Response) { resCh <- res },
|
||||
mempl.TxInfo{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := <-resCh
|
||||
r := res.GetCheckTx()
|
||||
|
||||
return &ctypes.ResultBroadcastTx{
|
||||
Code: r.Code,
|
||||
Data: r.Data,
|
||||
Log: r.Log,
|
||||
Codespace: r.Codespace,
|
||||
MempoolError: r.MempoolError,
|
||||
Hash: tx.Hash(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BroadcastTxCommit returns with the responses from CheckTx and DeliverTx.
|
||||
// More: https://docs.tendermint.com/master/rpc/#/Tx/broadcast_tx_commit
|
||||
func (env *Environment) BroadcastTxCommit(ctx *rpctypes.Context, tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) {
|
||||
subscriber := ctx.RemoteAddr()
|
||||
|
||||
if env.EventBus.NumClients() >= env.Config.MaxSubscriptionClients {
|
||||
return nil, fmt.Errorf("max_subscription_clients %d reached", env.Config.MaxSubscriptionClients)
|
||||
} else if env.EventBus.NumClientSubscriptions(subscriber) >= env.Config.MaxSubscriptionsPerClient {
|
||||
return nil, fmt.Errorf("max_subscriptions_per_client %d reached", env.Config.MaxSubscriptionsPerClient)
|
||||
}
|
||||
|
||||
// Subscribe to tx being committed in block.
|
||||
subCtx, cancel := context.WithTimeout(ctx.Context(), SubscribeTimeout)
|
||||
defer cancel()
|
||||
q := types.EventQueryTxFor(tx)
|
||||
deliverTxSub, err := env.EventBus.Subscribe(subCtx, subscriber, q)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to subscribe to tx: %w", err)
|
||||
env.Logger.Error("Error on broadcast_tx_commit", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
args := tmpubsub.UnsubscribeArgs{Subscriber: subscriber, Query: q}
|
||||
if err := env.EventBus.Unsubscribe(context.Background(), args); err != nil {
|
||||
env.Logger.Error("Error unsubscribing from eventBus", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Broadcast tx and wait for CheckTx result
|
||||
checkTxResCh := make(chan *abci.Response, 1)
|
||||
err = env.Mempool.CheckTx(
|
||||
ctx.Context(),
|
||||
tx,
|
||||
func(res *abci.Response) { checkTxResCh <- res },
|
||||
mempl.TxInfo{},
|
||||
)
|
||||
if err != nil {
|
||||
env.Logger.Error("Error on broadcastTxCommit", "err", err)
|
||||
return nil, fmt.Errorf("error on broadcastTxCommit: %v", err)
|
||||
}
|
||||
|
||||
checkTxResMsg := <-checkTxResCh
|
||||
checkTxRes := checkTxResMsg.GetCheckTx()
|
||||
|
||||
if checkTxRes.Code != abci.CodeTypeOK {
|
||||
return &ctypes.ResultBroadcastTxCommit{
|
||||
CheckTx: *checkTxRes,
|
||||
DeliverTx: abci.ResponseDeliverTx{},
|
||||
Hash: tx.Hash(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Wait for the tx to be included in a block or timeout.
|
||||
select {
|
||||
case msg := <-deliverTxSub.Out(): // The tx was included in a block.
|
||||
deliverTxRes := msg.Data().(types.EventDataTx)
|
||||
return &ctypes.ResultBroadcastTxCommit{
|
||||
CheckTx: *checkTxRes,
|
||||
DeliverTx: deliverTxRes.Result,
|
||||
Hash: tx.Hash(),
|
||||
Height: deliverTxRes.Height,
|
||||
}, nil
|
||||
case <-deliverTxSub.Canceled():
|
||||
var reason string
|
||||
if deliverTxSub.Err() == nil {
|
||||
reason = "Tendermint exited"
|
||||
} else {
|
||||
reason = deliverTxSub.Err().Error()
|
||||
}
|
||||
err = fmt.Errorf("deliverTxSub was canceled (reason: %s)", reason)
|
||||
env.Logger.Error("Error on broadcastTxCommit", "err", err)
|
||||
return &ctypes.ResultBroadcastTxCommit{
|
||||
CheckTx: *checkTxRes,
|
||||
DeliverTx: abci.ResponseDeliverTx{},
|
||||
Hash: tx.Hash(),
|
||||
}, err
|
||||
case <-time.After(env.Config.TimeoutBroadcastTxCommit):
|
||||
err = errors.New("timed out waiting for tx to be included in a block")
|
||||
env.Logger.Error("Error on broadcastTxCommit", "err", err)
|
||||
return &ctypes.ResultBroadcastTxCommit{
|
||||
CheckTx: *checkTxRes,
|
||||
DeliverTx: abci.ResponseDeliverTx{},
|
||||
Hash: tx.Hash(),
|
||||
}, err
|
||||
}
|
||||
}
|
||||
|
||||
// UnconfirmedTxs gets unconfirmed transactions (maximum ?limit entries)
|
||||
// including their number.
|
||||
// More: https://docs.tendermint.com/master/rpc/#/Info/unconfirmed_txs
|
||||
func (env *Environment) UnconfirmedTxs(ctx *rpctypes.Context, limitPtr *int) (*ctypes.ResultUnconfirmedTxs, error) {
|
||||
// reuse per_page validator
|
||||
limit := env.validatePerPage(limitPtr)
|
||||
|
||||
txs := env.Mempool.ReapMaxTxs(limit)
|
||||
return &ctypes.ResultUnconfirmedTxs{
|
||||
Count: len(txs),
|
||||
Total: env.Mempool.Size(),
|
||||
TotalBytes: env.Mempool.SizeBytes(),
|
||||
Txs: txs}, nil
|
||||
}
|
||||
|
||||
// NumUnconfirmedTxs gets number of unconfirmed transactions.
|
||||
// More: https://docs.tendermint.com/master/rpc/#/Info/num_unconfirmed_txs
|
||||
func (env *Environment) NumUnconfirmedTxs(ctx *rpctypes.Context) (*ctypes.ResultUnconfirmedTxs, error) {
|
||||
return &ctypes.ResultUnconfirmedTxs{
|
||||
Count: env.Mempool.Size(),
|
||||
Total: env.Mempool.Size(),
|
||||
TotalBytes: env.Mempool.SizeBytes()}, nil
|
||||
}
|
||||
|
||||
// CheckTx checks the transaction without executing it. The transaction won't
|
||||
// be added to the mempool either.
|
||||
// More: https://docs.tendermint.com/master/rpc/#/Tx/check_tx
|
||||
func (env *Environment) CheckTx(ctx *rpctypes.Context, tx types.Tx) (*ctypes.ResultCheckTx, error) {
|
||||
res, err := env.ProxyAppMempool.CheckTxSync(ctx.Context(), abci.RequestCheckTx{Tx: tx})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ctypes.ResultCheckTx{ResponseCheckTx: *res}, nil
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/tendermint/tendermint/internal/p2p"
|
||||
ctypes "github.com/tendermint/tendermint/rpc/coretypes"
|
||||
rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types"
|
||||
)
|
||||
|
||||
// NetInfo returns network info.
|
||||
// More: https://docs.tendermint.com/master/rpc/#/Info/net_info
|
||||
func (env *Environment) NetInfo(ctx *rpctypes.Context) (*ctypes.ResultNetInfo, error) {
|
||||
var peers []ctypes.Peer
|
||||
|
||||
switch {
|
||||
case env.P2PPeers != nil:
|
||||
peersList := env.P2PPeers.Peers().List()
|
||||
peers = make([]ctypes.Peer, 0, len(peersList))
|
||||
for _, peer := range peersList {
|
||||
peers = append(peers, ctypes.Peer{
|
||||
ID: peer.ID(),
|
||||
URL: peer.SocketAddr().String(),
|
||||
})
|
||||
}
|
||||
case env.PeerManager != nil:
|
||||
peerList := env.PeerManager.Peers()
|
||||
for _, peer := range peerList {
|
||||
addrs := env.PeerManager.Addresses(peer)
|
||||
if len(addrs) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
peers = append(peers, ctypes.Peer{
|
||||
ID: peer,
|
||||
URL: addrs[0].String(),
|
||||
})
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("peer management system does not support NetInfo responses")
|
||||
}
|
||||
|
||||
return &ctypes.ResultNetInfo{
|
||||
Listening: env.P2PTransport.IsListening(),
|
||||
Listeners: env.P2PTransport.Listeners(),
|
||||
NPeers: len(peers),
|
||||
Peers: peers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UnsafeDialSeeds dials the given seeds (comma-separated id@IP:PORT).
|
||||
func (env *Environment) UnsafeDialSeeds(ctx *rpctypes.Context, seeds []string) (*ctypes.ResultDialSeeds, error) {
|
||||
if env.P2PPeers == nil {
|
||||
return nil, errors.New("peer management system does not support this operation")
|
||||
}
|
||||
|
||||
if len(seeds) == 0 {
|
||||
return &ctypes.ResultDialSeeds{}, fmt.Errorf("%w: no seeds provided", ctypes.ErrInvalidRequest)
|
||||
}
|
||||
env.Logger.Info("DialSeeds", "seeds", seeds)
|
||||
if err := env.P2PPeers.DialPeersAsync(seeds); err != nil {
|
||||
return &ctypes.ResultDialSeeds{}, err
|
||||
}
|
||||
return &ctypes.ResultDialSeeds{Log: "Dialing seeds in progress. See /net_info for details"}, nil
|
||||
}
|
||||
|
||||
// UnsafeDialPeers dials the given peers (comma-separated id@IP:PORT),
|
||||
// optionally making them persistent.
|
||||
func (env *Environment) UnsafeDialPeers(
|
||||
ctx *rpctypes.Context,
|
||||
peers []string,
|
||||
persistent, unconditional, private bool) (*ctypes.ResultDialPeers, error) {
|
||||
|
||||
if env.P2PPeers == nil {
|
||||
return nil, errors.New("peer management system does not support this operation")
|
||||
}
|
||||
|
||||
if len(peers) == 0 {
|
||||
return &ctypes.ResultDialPeers{}, fmt.Errorf("%w: no peers provided", ctypes.ErrInvalidRequest)
|
||||
}
|
||||
|
||||
ids, err := getIDs(peers)
|
||||
if err != nil {
|
||||
return &ctypes.ResultDialPeers{}, err
|
||||
}
|
||||
|
||||
env.Logger.Info("DialPeers", "peers", peers, "persistent",
|
||||
persistent, "unconditional", unconditional, "private", private)
|
||||
|
||||
if persistent {
|
||||
if err := env.P2PPeers.AddPersistentPeers(peers); err != nil {
|
||||
return &ctypes.ResultDialPeers{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if private {
|
||||
if err := env.P2PPeers.AddPrivatePeerIDs(ids); err != nil {
|
||||
return &ctypes.ResultDialPeers{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if unconditional {
|
||||
if err := env.P2PPeers.AddUnconditionalPeerIDs(ids); err != nil {
|
||||
return &ctypes.ResultDialPeers{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := env.P2PPeers.DialPeersAsync(peers); err != nil {
|
||||
return &ctypes.ResultDialPeers{}, err
|
||||
}
|
||||
|
||||
return &ctypes.ResultDialPeers{Log: "Dialing peers in progress. See /net_info for details"}, nil
|
||||
}
|
||||
|
||||
// Genesis returns genesis file.
|
||||
// More: https://docs.tendermint.com/master/rpc/#/Info/genesis
|
||||
func (env *Environment) Genesis(ctx *rpctypes.Context) (*ctypes.ResultGenesis, error) {
|
||||
if len(env.genChunks) > 1 {
|
||||
return nil, errors.New("genesis response is large, please use the genesis_chunked API instead")
|
||||
}
|
||||
|
||||
return &ctypes.ResultGenesis{Genesis: env.GenDoc}, nil
|
||||
}
|
||||
|
||||
func (env *Environment) GenesisChunked(ctx *rpctypes.Context, chunk uint) (*ctypes.ResultGenesisChunk, error) {
|
||||
if env.genChunks == nil {
|
||||
return nil, fmt.Errorf("service configuration error, genesis chunks are not initialized")
|
||||
}
|
||||
|
||||
if len(env.genChunks) == 0 {
|
||||
return nil, fmt.Errorf("service configuration error, there are no chunks")
|
||||
}
|
||||
|
||||
id := int(chunk)
|
||||
|
||||
if id > len(env.genChunks)-1 {
|
||||
return nil, fmt.Errorf("there are %d chunks, %d is invalid", len(env.genChunks)-1, id)
|
||||
}
|
||||
|
||||
return &ctypes.ResultGenesisChunk{
|
||||
TotalChunks: len(env.genChunks),
|
||||
ChunkNumber: id,
|
||||
Data: env.genChunks[id],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getIDs(peers []string) ([]string, error) {
|
||||
ids := make([]string, 0, len(peers))
|
||||
|
||||
for _, peer := range peers {
|
||||
|
||||
spl := strings.Split(peer, "@")
|
||||
if len(spl) != 2 {
|
||||
return nil, p2p.ErrNetAddressNoID{Addr: peer}
|
||||
}
|
||||
ids = append(ids, spl[0])
|
||||
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
cfg "github.com/tendermint/tendermint/config"
|
||||
"github.com/tendermint/tendermint/internal/p2p"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types"
|
||||
)
|
||||
|
||||
func TestUnsafeDialSeeds(t *testing.T) {
|
||||
sw := p2p.MakeSwitch(cfg.DefaultP2PConfig(), 1, "testing", "123.123.123",
|
||||
func(n int, sw *p2p.Switch) *p2p.Switch { return sw }, log.TestingLogger())
|
||||
err := sw.Start()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
if err := sw.Stop(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
|
||||
env := &Environment{}
|
||||
env.Logger = log.TestingLogger()
|
||||
env.P2PPeers = sw
|
||||
|
||||
testCases := []struct {
|
||||
seeds []string
|
||||
isErr bool
|
||||
}{
|
||||
{[]string{}, true},
|
||||
{[]string{"d51fb70907db1c6c2d5237e78379b25cf1a37ab4@127.0.0.1:41198"}, false},
|
||||
{[]string{"127.0.0.1:41198"}, true},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
res, err := env.UnsafeDialSeeds(&rpctypes.Context{}, tc.seeds)
|
||||
if tc.isErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnsafeDialPeers(t *testing.T) {
|
||||
sw := p2p.MakeSwitch(cfg.DefaultP2PConfig(), 1, "testing", "123.123.123",
|
||||
func(n int, sw *p2p.Switch) *p2p.Switch { return sw }, log.TestingLogger())
|
||||
sw.SetAddrBook(&p2p.AddrBookMock{
|
||||
Addrs: make(map[string]struct{}),
|
||||
OurAddrs: make(map[string]struct{}),
|
||||
PrivateAddrs: make(map[string]struct{}),
|
||||
})
|
||||
err := sw.Start()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
if err := sw.Stop(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
|
||||
env := &Environment{}
|
||||
env.Logger = log.TestingLogger()
|
||||
env.P2PPeers = sw
|
||||
|
||||
testCases := []struct {
|
||||
peers []string
|
||||
persistence, unconditional, private bool
|
||||
isErr bool
|
||||
}{
|
||||
{[]string{}, false, false, false, true},
|
||||
{[]string{"d51fb70907db1c6c2d5237e78379b25cf1a37ab4@127.0.0.1:41198"}, true, true, true, false},
|
||||
{[]string{"127.0.0.1:41198"}, true, true, false, true},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
res, err := env.UnsafeDialPeers(&rpctypes.Context{}, tc.peers, tc.persistence, tc.unconditional, tc.private)
|
||||
if tc.isErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
rpc "github.com/tendermint/tendermint/rpc/jsonrpc/server"
|
||||
)
|
||||
|
||||
// TODO: better system than "unsafe" prefix
|
||||
|
||||
type RoutesMap map[string]*rpc.RPCFunc
|
||||
|
||||
// Routes is a map of available routes.
|
||||
func (env *Environment) GetRoutes() RoutesMap {
|
||||
return RoutesMap{
|
||||
// subscribe/unsubscribe are reserved for websocket events.
|
||||
"subscribe": rpc.NewWSRPCFunc(env.Subscribe, "query"),
|
||||
"unsubscribe": rpc.NewWSRPCFunc(env.Unsubscribe, "query"),
|
||||
"unsubscribe_all": rpc.NewWSRPCFunc(env.UnsubscribeAll, ""),
|
||||
|
||||
// info API
|
||||
"health": rpc.NewRPCFunc(env.Health, "", false),
|
||||
"status": rpc.NewRPCFunc(env.Status, "", false),
|
||||
"net_info": rpc.NewRPCFunc(env.NetInfo, "", false),
|
||||
"blockchain": rpc.NewRPCFunc(env.BlockchainInfo, "minHeight,maxHeight", true),
|
||||
"genesis": rpc.NewRPCFunc(env.Genesis, "", true),
|
||||
"genesis_chunked": rpc.NewRPCFunc(env.GenesisChunked, "chunk", true),
|
||||
"block": rpc.NewRPCFunc(env.Block, "height", true),
|
||||
"block_by_hash": rpc.NewRPCFunc(env.BlockByHash, "hash", true),
|
||||
"block_results": rpc.NewRPCFunc(env.BlockResults, "height", true),
|
||||
"commit": rpc.NewRPCFunc(env.Commit, "height", true),
|
||||
"check_tx": rpc.NewRPCFunc(env.CheckTx, "tx", true),
|
||||
"tx": rpc.NewRPCFunc(env.Tx, "hash,prove", true),
|
||||
"tx_search": rpc.NewRPCFunc(env.TxSearch, "query,prove,page,per_page,order_by", false),
|
||||
"block_search": rpc.NewRPCFunc(env.BlockSearch, "query,page,per_page,order_by", false),
|
||||
"validators": rpc.NewRPCFunc(env.Validators, "height,page,per_page", true),
|
||||
"dump_consensus_state": rpc.NewRPCFunc(env.DumpConsensusState, "", false),
|
||||
"consensus_state": rpc.NewRPCFunc(env.GetConsensusState, "", false),
|
||||
"consensus_params": rpc.NewRPCFunc(env.ConsensusParams, "height", true),
|
||||
"unconfirmed_txs": rpc.NewRPCFunc(env.UnconfirmedTxs, "limit", false),
|
||||
"num_unconfirmed_txs": rpc.NewRPCFunc(env.NumUnconfirmedTxs, "", false),
|
||||
|
||||
// tx broadcast API
|
||||
"broadcast_tx_commit": rpc.NewRPCFunc(env.BroadcastTxCommit, "tx", false),
|
||||
"broadcast_tx_sync": rpc.NewRPCFunc(env.BroadcastTxSync, "tx", false),
|
||||
"broadcast_tx_async": rpc.NewRPCFunc(env.BroadcastTxAsync, "tx", false),
|
||||
|
||||
// abci API
|
||||
"abci_query": rpc.NewRPCFunc(env.ABCIQuery, "path,data,height,prove", false),
|
||||
"abci_info": rpc.NewRPCFunc(env.ABCIInfo, "", true),
|
||||
|
||||
// evidence API
|
||||
"broadcast_evidence": rpc.NewRPCFunc(env.BroadcastEvidence, "evidence", false),
|
||||
}
|
||||
}
|
||||
|
||||
// AddUnsafeRoutes adds unsafe routes.
|
||||
func (env *Environment) AddUnsafe(routes RoutesMap) {
|
||||
// control API
|
||||
routes["dial_seeds"] = rpc.NewRPCFunc(env.UnsafeDialSeeds, "seeds", false)
|
||||
routes["dial_peers"] = rpc.NewRPCFunc(env.UnsafeDialPeers, "peers,persistent,unconditional,private", false)
|
||||
routes["unsafe_flush_mempool"] = rpc.NewRPCFunc(env.UnsafeFlushMempool, "", false)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"time"
|
||||
|
||||
tmbytes "github.com/tendermint/tendermint/libs/bytes"
|
||||
ctypes "github.com/tendermint/tendermint/rpc/coretypes"
|
||||
rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
// Status returns Tendermint status including node info, pubkey, latest block
|
||||
// hash, app hash, block height, current max peer block height, and time.
|
||||
// More: https://docs.tendermint.com/master/rpc/#/Info/status
|
||||
func (env *Environment) Status(ctx *rpctypes.Context) (*ctypes.ResultStatus, error) {
|
||||
var (
|
||||
earliestBlockHeight int64
|
||||
earliestBlockHash tmbytes.HexBytes
|
||||
earliestAppHash tmbytes.HexBytes
|
||||
earliestBlockTimeNano int64
|
||||
)
|
||||
|
||||
if earliestBlockMeta := env.BlockStore.LoadBaseMeta(); earliestBlockMeta != nil {
|
||||
earliestBlockHeight = earliestBlockMeta.Header.Height
|
||||
earliestAppHash = earliestBlockMeta.Header.AppHash
|
||||
earliestBlockHash = earliestBlockMeta.BlockID.Hash
|
||||
earliestBlockTimeNano = earliestBlockMeta.Header.Time.UnixNano()
|
||||
}
|
||||
|
||||
var (
|
||||
latestBlockHash tmbytes.HexBytes
|
||||
latestAppHash tmbytes.HexBytes
|
||||
latestBlockTimeNano int64
|
||||
|
||||
latestHeight = env.BlockStore.Height()
|
||||
)
|
||||
|
||||
if latestHeight != 0 {
|
||||
if latestBlockMeta := env.BlockStore.LoadBlockMeta(latestHeight); latestBlockMeta != nil {
|
||||
latestBlockHash = latestBlockMeta.BlockID.Hash
|
||||
latestAppHash = latestBlockMeta.Header.AppHash
|
||||
latestBlockTimeNano = latestBlockMeta.Header.Time.UnixNano()
|
||||
}
|
||||
}
|
||||
|
||||
// Return the very last voting power, not the voting power of this validator
|
||||
// during the last block.
|
||||
var votingPower int64
|
||||
if val := env.validatorAtHeight(env.latestUncommittedHeight()); val != nil {
|
||||
votingPower = val.VotingPower
|
||||
}
|
||||
validatorInfo := ctypes.ValidatorInfo{}
|
||||
if env.PubKey != nil {
|
||||
validatorInfo = ctypes.ValidatorInfo{
|
||||
Address: env.PubKey.Address(),
|
||||
PubKey: env.PubKey,
|
||||
VotingPower: votingPower,
|
||||
}
|
||||
}
|
||||
result := &ctypes.ResultStatus{
|
||||
NodeInfo: env.P2PTransport.NodeInfo(),
|
||||
SyncInfo: ctypes.SyncInfo{
|
||||
LatestBlockHash: latestBlockHash,
|
||||
LatestAppHash: latestAppHash,
|
||||
LatestBlockHeight: latestHeight,
|
||||
LatestBlockTime: time.Unix(0, latestBlockTimeNano),
|
||||
EarliestBlockHash: earliestBlockHash,
|
||||
EarliestAppHash: earliestAppHash,
|
||||
EarliestBlockHeight: earliestBlockHeight,
|
||||
EarliestBlockTime: time.Unix(0, earliestBlockTimeNano),
|
||||
MaxPeerBlockHeight: env.BlockSyncReactor.GetMaxPeerBlockHeight(),
|
||||
CatchingUp: env.ConsensusReactor.WaitSync(),
|
||||
TotalSyncedTime: env.BlockSyncReactor.GetTotalSyncedTime(),
|
||||
RemainingTime: env.BlockSyncReactor.GetRemainingSyncTime(),
|
||||
},
|
||||
ValidatorInfo: validatorInfo,
|
||||
}
|
||||
|
||||
if env.StateSyncMetricer != nil {
|
||||
result.SyncInfo.TotalSnapshots = env.StateSyncMetricer.TotalSnapshots()
|
||||
result.SyncInfo.ChunkProcessAvgTime = env.StateSyncMetricer.ChunkProcessAvgTime()
|
||||
result.SyncInfo.SnapshotHeight = env.StateSyncMetricer.SnapshotHeight()
|
||||
result.SyncInfo.SnapshotChunksCount = env.StateSyncMetricer.SnapshotChunksCount()
|
||||
result.SyncInfo.SnapshotChunksTotal = env.StateSyncMetricer.SnapshotChunksTotal()
|
||||
result.SyncInfo.BackFilledBlocks = env.StateSyncMetricer.BackFilledBlocks()
|
||||
result.SyncInfo.BackFillBlocksTotal = env.StateSyncMetricer.BackFillBlocksTotal()
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (env *Environment) validatorAtHeight(h int64) *types.Validator {
|
||||
valsWithH, err := env.StateStore.LoadValidators(h)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if env.PubKey == nil {
|
||||
return nil
|
||||
}
|
||||
privValAddress := env.PubKey.Address()
|
||||
|
||||
// If we're still at height h, search in the current validator set.
|
||||
lastBlockHeight, vals := env.ConsensusState.GetValidators()
|
||||
if lastBlockHeight == h {
|
||||
for _, val := range vals {
|
||||
if bytes.Equal(val.Address, privValAddress) {
|
||||
return val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, val := valsWithH.GetByAddress(privValAddress)
|
||||
return val
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/tendermint/tendermint/internal/state/indexer"
|
||||
"github.com/tendermint/tendermint/libs/bytes"
|
||||
tmmath "github.com/tendermint/tendermint/libs/math"
|
||||
tmquery "github.com/tendermint/tendermint/libs/pubsub/query"
|
||||
ctypes "github.com/tendermint/tendermint/rpc/coretypes"
|
||||
rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
// 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.
|
||||
// More: https://docs.tendermint.com/master/rpc/#/Info/tx
|
||||
func (env *Environment) Tx(ctx *rpctypes.Context, hash bytes.HexBytes, prove bool) (*ctypes.ResultTx, error) {
|
||||
// if index is disabled, return error
|
||||
|
||||
// N.B. The hash parameter is HexBytes so that the reflective parameter
|
||||
// decoding logic in the HTTP service will correctly translate from JSON.
|
||||
// See https://github.com/tendermint/tendermint/issues/6802 for context.
|
||||
|
||||
if !indexer.KVSinkEnabled(env.EventSinks) {
|
||||
return nil, errors.New("transaction querying is disabled due to no kvEventSink")
|
||||
}
|
||||
|
||||
for _, sink := range env.EventSinks {
|
||||
if sink.Type() == indexer.KV {
|
||||
r, err := sink.GetTxByHash(hash)
|
||||
if r == nil {
|
||||
return nil, fmt.Errorf("tx (%X) not found, err: %w", hash, err)
|
||||
}
|
||||
|
||||
height := r.Height
|
||||
index := r.Index
|
||||
|
||||
var proof types.TxProof
|
||||
if prove {
|
||||
block := env.BlockStore.LoadBlock(height)
|
||||
proof = block.Data.Txs.Proof(int(index)) // XXX: overflow on 32-bit machines
|
||||
}
|
||||
|
||||
return &ctypes.ResultTx{
|
||||
Hash: hash,
|
||||
Height: height,
|
||||
Index: index,
|
||||
TxResult: r.Result,
|
||||
Tx: r.Tx,
|
||||
Proof: proof,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("transaction querying is disabled on this node due to the KV event sink being disabled")
|
||||
}
|
||||
|
||||
// TxSearch allows you to query for multiple transactions results. It returns a
|
||||
// list of transactions (maximum ?per_page entries) and the total count.
|
||||
// More: https://docs.tendermint.com/master/rpc/#/Info/tx_search
|
||||
func (env *Environment) TxSearch(
|
||||
ctx *rpctypes.Context,
|
||||
query string,
|
||||
prove bool,
|
||||
pagePtr, perPagePtr *int,
|
||||
orderBy string,
|
||||
) (*ctypes.ResultTxSearch, error) {
|
||||
|
||||
if !indexer.KVSinkEnabled(env.EventSinks) {
|
||||
return nil, fmt.Errorf("transaction searching is disabled due to no kvEventSink")
|
||||
}
|
||||
|
||||
q, err := tmquery.New(query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, sink := range env.EventSinks {
|
||||
if sink.Type() == indexer.KV {
|
||||
results, err := sink.SearchTxEvents(ctx.Context(), q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// sort results (must be done before pagination)
|
||||
switch orderBy {
|
||||
case "desc", "":
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
if results[i].Height == results[j].Height {
|
||||
return results[i].Index > results[j].Index
|
||||
}
|
||||
return results[i].Height > results[j].Height
|
||||
})
|
||||
case "asc":
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
if results[i].Height == results[j].Height {
|
||||
return results[i].Index < results[j].Index
|
||||
}
|
||||
return results[i].Height < results[j].Height
|
||||
})
|
||||
default:
|
||||
return nil, fmt.Errorf("expected order_by to be either `asc` or `desc` or empty: %w", ctypes.ErrInvalidRequest)
|
||||
}
|
||||
|
||||
// paginate results
|
||||
totalCount := len(results)
|
||||
perPage := env.validatePerPage(perPagePtr)
|
||||
|
||||
page, err := validatePage(pagePtr, perPage, totalCount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
skipCount := validateSkipCount(page, perPage)
|
||||
pageSize := tmmath.MinInt(perPage, totalCount-skipCount)
|
||||
|
||||
apiResults := make([]*ctypes.ResultTx, 0, pageSize)
|
||||
for i := skipCount; i < skipCount+pageSize; i++ {
|
||||
r := results[i]
|
||||
|
||||
var proof types.TxProof
|
||||
if prove {
|
||||
block := env.BlockStore.LoadBlock(r.Height)
|
||||
proof = block.Data.Txs.Proof(int(r.Index)) // XXX: overflow on 32-bit machines
|
||||
}
|
||||
|
||||
apiResults = append(apiResults, &ctypes.ResultTx{
|
||||
Hash: types.Tx(r.Tx).Hash(),
|
||||
Height: r.Height,
|
||||
Index: r.Index,
|
||||
TxResult: r.Result,
|
||||
Tx: r.Tx,
|
||||
Proof: proof,
|
||||
})
|
||||
}
|
||||
|
||||
return &ctypes.ResultTxSearch{Txs: apiResults, TotalCount: totalCount}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("transaction searching is disabled on this node due to the KV event sink being disabled")
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package state
|
||||
|
||||
import "fmt"
|
||||
|
||||
type (
|
||||
ErrInvalidBlock error
|
||||
ErrProxyAppConn error
|
||||
|
||||
ErrUnknownBlock struct {
|
||||
Height int64
|
||||
}
|
||||
|
||||
ErrBlockHashMismatch struct {
|
||||
CoreHash []byte
|
||||
AppHash []byte
|
||||
Height int64
|
||||
}
|
||||
|
||||
ErrAppBlockHeightTooHigh struct {
|
||||
CoreHeight int64
|
||||
AppHeight int64
|
||||
}
|
||||
|
||||
ErrAppBlockHeightTooLow struct {
|
||||
AppHeight int64
|
||||
StoreBase int64
|
||||
}
|
||||
|
||||
ErrLastStateMismatch struct {
|
||||
Height int64
|
||||
Core []byte
|
||||
App []byte
|
||||
}
|
||||
|
||||
ErrStateMismatch struct {
|
||||
Got *State
|
||||
Expected *State
|
||||
}
|
||||
|
||||
ErrNoValSetForHeight struct {
|
||||
Height int64
|
||||
}
|
||||
|
||||
ErrNoConsensusParamsForHeight struct {
|
||||
Height int64
|
||||
}
|
||||
|
||||
ErrNoABCIResponsesForHeight struct {
|
||||
Height int64
|
||||
}
|
||||
)
|
||||
|
||||
func (e ErrUnknownBlock) Error() string {
|
||||
return fmt.Sprintf("could not find block #%d", e.Height)
|
||||
}
|
||||
|
||||
func (e ErrBlockHashMismatch) Error() string {
|
||||
return fmt.Sprintf(
|
||||
"app block hash (%X) does not match core block hash (%X) for height %d",
|
||||
e.AppHash,
|
||||
e.CoreHash,
|
||||
e.Height,
|
||||
)
|
||||
}
|
||||
|
||||
func (e ErrAppBlockHeightTooHigh) Error() string {
|
||||
return fmt.Sprintf("app block height (%d) is higher than core (%d)", e.AppHeight, e.CoreHeight)
|
||||
}
|
||||
|
||||
func (e ErrAppBlockHeightTooLow) Error() string {
|
||||
return fmt.Sprintf("app block height (%d) is too far below block store base (%d)", e.AppHeight, e.StoreBase)
|
||||
}
|
||||
|
||||
func (e ErrLastStateMismatch) Error() string {
|
||||
return fmt.Sprintf(
|
||||
"latest tendermint block (%d) LastAppHash (%X) does not match app's AppHash (%X)",
|
||||
e.Height,
|
||||
e.Core,
|
||||
e.App,
|
||||
)
|
||||
}
|
||||
|
||||
func (e ErrStateMismatch) Error() string {
|
||||
return fmt.Sprintf(
|
||||
"state after replay does not match saved state. Got ----\n%v\nExpected ----\n%v\n",
|
||||
e.Got,
|
||||
e.Expected,
|
||||
)
|
||||
}
|
||||
|
||||
func (e ErrNoValSetForHeight) Error() string {
|
||||
return fmt.Sprintf("could not find validator set for height #%d", e.Height)
|
||||
}
|
||||
|
||||
func (e ErrNoConsensusParamsForHeight) Error() string {
|
||||
return fmt.Sprintf("could not find consensus params for height #%d", e.Height)
|
||||
}
|
||||
|
||||
func (e ErrNoABCIResponsesForHeight) Error() string {
|
||||
return fmt.Sprintf("could not find results for height #%d", e.Height)
|
||||
}
|
||||
@@ -0,0 +1,629 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
cryptoenc "github.com/tendermint/tendermint/crypto/encoding"
|
||||
"github.com/tendermint/tendermint/internal/libs/fail"
|
||||
mempl "github.com/tendermint/tendermint/internal/mempool"
|
||||
"github.com/tendermint/tendermint/internal/proxy"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
tmstate "github.com/tendermint/tendermint/proto/tendermint/state"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// BlockExecutor handles block execution and state updates.
|
||||
// It exposes ApplyBlock(), which validates & executes the block, updates state w/ ABCI responses,
|
||||
// then commits and updates the mempool atomically, then saves state.
|
||||
|
||||
// BlockExecutor provides the context and accessories for properly executing a block.
|
||||
type BlockExecutor struct {
|
||||
// save state, validators, consensus params, abci responses here
|
||||
store Store
|
||||
|
||||
// use blockstore for the pruning functions.
|
||||
blockStore BlockStore
|
||||
|
||||
// execute the app against this
|
||||
proxyApp proxy.AppConnConsensus
|
||||
|
||||
// events
|
||||
eventBus types.BlockEventPublisher
|
||||
|
||||
// manage the mempool lock during commit
|
||||
// and update both with block results after commit.
|
||||
mempool mempl.Mempool
|
||||
evpool EvidencePool
|
||||
|
||||
logger log.Logger
|
||||
metrics *Metrics
|
||||
|
||||
// cache the verification results over a single height
|
||||
cache map[string]struct{}
|
||||
}
|
||||
|
||||
type BlockExecutorOption func(executor *BlockExecutor)
|
||||
|
||||
func BlockExecutorWithMetrics(metrics *Metrics) BlockExecutorOption {
|
||||
return func(blockExec *BlockExecutor) {
|
||||
blockExec.metrics = metrics
|
||||
}
|
||||
}
|
||||
|
||||
// NewBlockExecutor returns a new BlockExecutor with a NopEventBus.
|
||||
// Call SetEventBus to provide one.
|
||||
func NewBlockExecutor(
|
||||
stateStore Store,
|
||||
logger log.Logger,
|
||||
proxyApp proxy.AppConnConsensus,
|
||||
mempool mempl.Mempool,
|
||||
evpool EvidencePool,
|
||||
blockStore BlockStore,
|
||||
options ...BlockExecutorOption,
|
||||
) *BlockExecutor {
|
||||
res := &BlockExecutor{
|
||||
store: stateStore,
|
||||
proxyApp: proxyApp,
|
||||
eventBus: types.NopEventBus{},
|
||||
mempool: mempool,
|
||||
evpool: evpool,
|
||||
logger: logger,
|
||||
metrics: NopMetrics(),
|
||||
cache: make(map[string]struct{}),
|
||||
blockStore: blockStore,
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
option(res)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (blockExec *BlockExecutor) Store() Store {
|
||||
return blockExec.store
|
||||
}
|
||||
|
||||
// SetEventBus - sets the event bus for publishing block related events.
|
||||
// If not called, it defaults to types.NopEventBus.
|
||||
func (blockExec *BlockExecutor) SetEventBus(eventBus types.BlockEventPublisher) {
|
||||
blockExec.eventBus = eventBus
|
||||
}
|
||||
|
||||
// CreateProposalBlock calls state.MakeBlock with evidence from the evpool
|
||||
// and txs from the mempool. The max bytes must be big enough to fit the commit.
|
||||
// Up to 1/10th of the block space is allcoated for maximum sized evidence.
|
||||
// The rest is given to txs, up to the max gas.
|
||||
func (blockExec *BlockExecutor) CreateProposalBlock(
|
||||
height int64,
|
||||
state State, commit *types.Commit,
|
||||
proposerAddr []byte,
|
||||
) (*types.Block, *types.PartSet) {
|
||||
|
||||
maxBytes := state.ConsensusParams.Block.MaxBytes
|
||||
maxGas := state.ConsensusParams.Block.MaxGas
|
||||
|
||||
evidence, evSize := blockExec.evpool.PendingEvidence(state.ConsensusParams.Evidence.MaxBytes)
|
||||
|
||||
// Fetch a limited amount of valid txs
|
||||
maxDataBytes := types.MaxDataBytes(maxBytes, evSize, state.Validators.Size())
|
||||
|
||||
txs := blockExec.mempool.ReapMaxBytesMaxGas(maxDataBytes, maxGas)
|
||||
|
||||
return state.MakeBlock(height, txs, commit, evidence, proposerAddr)
|
||||
}
|
||||
|
||||
// ValidateBlock validates the given block against the given state.
|
||||
// If the block is invalid, it returns an error.
|
||||
// Validation does not mutate state, but does require historical information from the stateDB,
|
||||
// ie. to verify evidence from a validator at an old height.
|
||||
func (blockExec *BlockExecutor) ValidateBlock(state State, block *types.Block) error {
|
||||
hash := block.Hash()
|
||||
if _, ok := blockExec.cache[hash.String()]; ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := validateBlock(state, block)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = blockExec.evpool.CheckEvidence(block.Evidence.Evidence)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
blockExec.cache[hash.String()] = struct{}{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyBlock validates the block against the state, executes it against the app,
|
||||
// fires the relevant events, commits the app, and saves the new state and responses.
|
||||
// It returns the new state.
|
||||
// It's the only function that needs to be called
|
||||
// from outside this package to process and commit an entire block.
|
||||
// It takes a blockID to avoid recomputing the parts hash.
|
||||
func (blockExec *BlockExecutor) ApplyBlock(
|
||||
state State, blockID types.BlockID, block *types.Block,
|
||||
) (State, error) {
|
||||
|
||||
// validate the block if we haven't already
|
||||
if err := blockExec.ValidateBlock(state, block); err != nil {
|
||||
return state, ErrInvalidBlock(err)
|
||||
}
|
||||
|
||||
startTime := time.Now().UnixNano()
|
||||
abciResponses, err := execBlockOnProxyApp(
|
||||
blockExec.logger, blockExec.proxyApp, block, blockExec.store, state.InitialHeight,
|
||||
)
|
||||
endTime := time.Now().UnixNano()
|
||||
blockExec.metrics.BlockProcessingTime.Observe(float64(endTime-startTime) / 1000000)
|
||||
if err != nil {
|
||||
return state, ErrProxyAppConn(err)
|
||||
}
|
||||
|
||||
fail.Fail() // XXX
|
||||
|
||||
// Save the results before we commit.
|
||||
if err := blockExec.store.SaveABCIResponses(block.Height, abciResponses); err != nil {
|
||||
return state, err
|
||||
}
|
||||
|
||||
fail.Fail() // XXX
|
||||
|
||||
// validate the validator updates and convert to tendermint types
|
||||
abciValUpdates := abciResponses.EndBlock.ValidatorUpdates
|
||||
err = validateValidatorUpdates(abciValUpdates, state.ConsensusParams.Validator)
|
||||
if err != nil {
|
||||
return state, fmt.Errorf("error in validator updates: %v", err)
|
||||
}
|
||||
|
||||
validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciValUpdates)
|
||||
if err != nil {
|
||||
return state, err
|
||||
}
|
||||
if len(validatorUpdates) > 0 {
|
||||
blockExec.logger.Debug("updates to validators", "updates", types.ValidatorListString(validatorUpdates))
|
||||
}
|
||||
|
||||
// Update the state with the block and responses.
|
||||
state, err = updateState(state, blockID, &block.Header, abciResponses, validatorUpdates)
|
||||
if err != nil {
|
||||
return state, fmt.Errorf("commit failed for application: %v", err)
|
||||
}
|
||||
|
||||
// Lock mempool, commit app state, update mempoool.
|
||||
appHash, retainHeight, err := blockExec.Commit(state, block, abciResponses.DeliverTxs)
|
||||
if err != nil {
|
||||
return state, fmt.Errorf("commit failed for application: %v", err)
|
||||
}
|
||||
|
||||
// Update evpool with the latest state.
|
||||
blockExec.evpool.Update(state, block.Evidence.Evidence)
|
||||
|
||||
fail.Fail() // XXX
|
||||
|
||||
// Update the app hash and save the state.
|
||||
state.AppHash = appHash
|
||||
if err := blockExec.store.Save(state); err != nil {
|
||||
return state, err
|
||||
}
|
||||
|
||||
fail.Fail() // XXX
|
||||
|
||||
// Prune old heights, if requested by ABCI app.
|
||||
if retainHeight > 0 {
|
||||
pruned, err := blockExec.pruneBlocks(retainHeight)
|
||||
if err != nil {
|
||||
blockExec.logger.Error("failed to prune blocks", "retain_height", retainHeight, "err", err)
|
||||
} else {
|
||||
blockExec.logger.Debug("pruned blocks", "pruned", pruned, "retain_height", retainHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// reset the verification cache
|
||||
blockExec.cache = make(map[string]struct{})
|
||||
|
||||
// Events are fired after everything else.
|
||||
// NOTE: if we crash between Commit and Save, events wont be fired during replay
|
||||
fireEvents(blockExec.logger, blockExec.eventBus, block, blockID, abciResponses, validatorUpdates)
|
||||
|
||||
return state, nil
|
||||
}
|
||||
|
||||
// Commit locks the mempool, runs the ABCI Commit message, and updates the
|
||||
// mempool.
|
||||
// It returns the result of calling abci.Commit (the AppHash) and the height to retain (if any).
|
||||
// The Mempool must be locked during commit and update because state is
|
||||
// typically reset on Commit and old txs must be replayed against committed
|
||||
// state before new txs are run in the mempool, lest they be invalid.
|
||||
func (blockExec *BlockExecutor) Commit(
|
||||
state State,
|
||||
block *types.Block,
|
||||
deliverTxResponses []*abci.ResponseDeliverTx,
|
||||
) ([]byte, int64, error) {
|
||||
blockExec.mempool.Lock()
|
||||
defer blockExec.mempool.Unlock()
|
||||
|
||||
// while mempool is Locked, flush to ensure all async requests have completed
|
||||
// in the ABCI app before Commit.
|
||||
err := blockExec.mempool.FlushAppConn()
|
||||
if err != nil {
|
||||
blockExec.logger.Error("client error during mempool.FlushAppConn", "err", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Commit block, get hash back
|
||||
res, err := blockExec.proxyApp.CommitSync(context.Background())
|
||||
if err != nil {
|
||||
blockExec.logger.Error("client error during proxyAppConn.CommitSync", "err", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// ResponseCommit has no error code - just data
|
||||
blockExec.logger.Info(
|
||||
"committed state",
|
||||
"height", block.Height,
|
||||
"num_txs", len(block.Txs),
|
||||
"app_hash", fmt.Sprintf("%X", res.Data),
|
||||
)
|
||||
|
||||
// Update mempool.
|
||||
err = blockExec.mempool.Update(
|
||||
block.Height,
|
||||
block.Txs,
|
||||
deliverTxResponses,
|
||||
TxPreCheck(state),
|
||||
TxPostCheck(state),
|
||||
)
|
||||
|
||||
return res.Data, res.RetainHeight, err
|
||||
}
|
||||
|
||||
//---------------------------------------------------------
|
||||
// Helper functions for executing blocks and updating state
|
||||
|
||||
// Executes block's transactions on proxyAppConn.
|
||||
// Returns a list of transaction results and updates to the validator set
|
||||
func execBlockOnProxyApp(
|
||||
logger log.Logger,
|
||||
proxyAppConn proxy.AppConnConsensus,
|
||||
block *types.Block,
|
||||
store Store,
|
||||
initialHeight int64,
|
||||
) (*tmstate.ABCIResponses, error) {
|
||||
var validTxs, invalidTxs = 0, 0
|
||||
|
||||
txIndex := 0
|
||||
abciResponses := new(tmstate.ABCIResponses)
|
||||
dtxs := make([]*abci.ResponseDeliverTx, len(block.Txs))
|
||||
abciResponses.DeliverTxs = dtxs
|
||||
|
||||
// Execute transactions and get hash.
|
||||
proxyCb := func(req *abci.Request, res *abci.Response) {
|
||||
if r, ok := res.Value.(*abci.Response_DeliverTx); ok {
|
||||
// TODO: make use of res.Log
|
||||
// TODO: make use of this info
|
||||
// Blocks may include invalid txs.
|
||||
txRes := r.DeliverTx
|
||||
if txRes.Code == abci.CodeTypeOK {
|
||||
validTxs++
|
||||
} else {
|
||||
logger.Debug("invalid tx", "code", txRes.Code, "log", txRes.Log)
|
||||
invalidTxs++
|
||||
}
|
||||
|
||||
abciResponses.DeliverTxs[txIndex] = txRes
|
||||
txIndex++
|
||||
}
|
||||
}
|
||||
proxyAppConn.SetResponseCallback(proxyCb)
|
||||
|
||||
commitInfo := getBeginBlockValidatorInfo(block, store, initialHeight)
|
||||
|
||||
byzVals := make([]abci.Evidence, 0)
|
||||
for _, evidence := range block.Evidence.Evidence {
|
||||
byzVals = append(byzVals, evidence.ABCI()...)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Begin block
|
||||
var err error
|
||||
pbh := block.Header.ToProto()
|
||||
if pbh == nil {
|
||||
return nil, errors.New("nil header")
|
||||
}
|
||||
|
||||
abciResponses.BeginBlock, err = proxyAppConn.BeginBlockSync(
|
||||
ctx,
|
||||
abci.RequestBeginBlock{
|
||||
Hash: block.Hash(),
|
||||
Header: *pbh,
|
||||
LastCommitInfo: commitInfo,
|
||||
ByzantineValidators: byzVals,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error("error in proxyAppConn.BeginBlock", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// run txs of block
|
||||
for _, tx := range block.Txs {
|
||||
_, err = proxyAppConn.DeliverTxAsync(ctx, abci.RequestDeliverTx{Tx: tx})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
abciResponses.EndBlock, err = proxyAppConn.EndBlockSync(ctx, abci.RequestEndBlock{Height: block.Height})
|
||||
if err != nil {
|
||||
logger.Error("error in proxyAppConn.EndBlock", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Info("executed block", "height", block.Height, "num_valid_txs", validTxs, "num_invalid_txs", invalidTxs)
|
||||
return abciResponses, nil
|
||||
}
|
||||
|
||||
func getBeginBlockValidatorInfo(block *types.Block, store Store,
|
||||
initialHeight int64) abci.LastCommitInfo {
|
||||
voteInfos := make([]abci.VoteInfo, block.LastCommit.Size())
|
||||
// Initial block -> LastCommitInfo.Votes are empty.
|
||||
// Remember that the first LastCommit is intentionally empty, so it makes
|
||||
// sense for LastCommitInfo.Votes to also be empty.
|
||||
if block.Height > initialHeight {
|
||||
lastValSet, err := store.LoadValidators(block.Height - 1)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Sanity check that commit size matches validator set size - only applies
|
||||
// after first block.
|
||||
var (
|
||||
commitSize = block.LastCommit.Size()
|
||||
valSetLen = len(lastValSet.Validators)
|
||||
)
|
||||
if commitSize != valSetLen {
|
||||
panic(fmt.Sprintf(
|
||||
"commit size (%d) doesn't match valset length (%d) at height %d\n\n%v\n\n%v",
|
||||
commitSize, valSetLen, block.Height, block.LastCommit.Signatures, lastValSet.Validators,
|
||||
))
|
||||
}
|
||||
|
||||
for i, val := range lastValSet.Validators {
|
||||
commitSig := block.LastCommit.Signatures[i]
|
||||
voteInfos[i] = abci.VoteInfo{
|
||||
Validator: types.TM2PB.Validator(val),
|
||||
SignedLastBlock: !commitSig.Absent(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return abci.LastCommitInfo{
|
||||
Round: block.LastCommit.Round,
|
||||
Votes: voteInfos,
|
||||
}
|
||||
}
|
||||
|
||||
func validateValidatorUpdates(abciUpdates []abci.ValidatorUpdate,
|
||||
params types.ValidatorParams) error {
|
||||
for _, valUpdate := range abciUpdates {
|
||||
if valUpdate.GetPower() < 0 {
|
||||
return fmt.Errorf("voting power can't be negative %v", valUpdate)
|
||||
} else if valUpdate.GetPower() == 0 {
|
||||
// continue, since this is deleting the validator, and thus there is no
|
||||
// pubkey to check
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if validator's pubkey matches an ABCI type in the consensus params
|
||||
pk, err := cryptoenc.PubKeyFromProto(valUpdate.PubKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !params.IsValidPubkeyType(pk.Type()) {
|
||||
return fmt.Errorf("validator %v is using pubkey %s, which is unsupported for consensus",
|
||||
valUpdate, pk.Type())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateState returns a new State updated according to the header and responses.
|
||||
func updateState(
|
||||
state State,
|
||||
blockID types.BlockID,
|
||||
header *types.Header,
|
||||
abciResponses *tmstate.ABCIResponses,
|
||||
validatorUpdates []*types.Validator,
|
||||
) (State, error) {
|
||||
|
||||
// Copy the valset so we can apply changes from EndBlock
|
||||
// and update s.LastValidators and s.Validators.
|
||||
nValSet := state.NextValidators.Copy()
|
||||
|
||||
// Update the validator set with the latest abciResponses.
|
||||
lastHeightValsChanged := state.LastHeightValidatorsChanged
|
||||
if len(validatorUpdates) > 0 {
|
||||
err := nValSet.UpdateWithChangeSet(validatorUpdates)
|
||||
if err != nil {
|
||||
return state, fmt.Errorf("error changing validator set: %v", err)
|
||||
}
|
||||
// Change results from this height but only applies to the next next height.
|
||||
lastHeightValsChanged = header.Height + 1 + 1
|
||||
}
|
||||
|
||||
// Update validator proposer priority and set state variables.
|
||||
nValSet.IncrementProposerPriority(1)
|
||||
|
||||
// Update the params with the latest abciResponses.
|
||||
nextParams := state.ConsensusParams
|
||||
lastHeightParamsChanged := state.LastHeightConsensusParamsChanged
|
||||
if abciResponses.EndBlock.ConsensusParamUpdates != nil {
|
||||
// NOTE: must not mutate s.ConsensusParams
|
||||
nextParams = state.ConsensusParams.UpdateConsensusParams(abciResponses.EndBlock.ConsensusParamUpdates)
|
||||
err := nextParams.ValidateConsensusParams()
|
||||
if err != nil {
|
||||
return state, fmt.Errorf("error updating consensus params: %v", err)
|
||||
}
|
||||
|
||||
state.Version.Consensus.App = nextParams.Version.AppVersion
|
||||
|
||||
// Change results from this height but only applies to the next height.
|
||||
lastHeightParamsChanged = header.Height + 1
|
||||
}
|
||||
|
||||
nextVersion := state.Version
|
||||
|
||||
// NOTE: the AppHash has not been populated.
|
||||
// It will be filled on state.Save.
|
||||
return State{
|
||||
Version: nextVersion,
|
||||
ChainID: state.ChainID,
|
||||
InitialHeight: state.InitialHeight,
|
||||
LastBlockHeight: header.Height,
|
||||
LastBlockID: blockID,
|
||||
LastBlockTime: header.Time,
|
||||
NextValidators: nValSet,
|
||||
Validators: state.NextValidators.Copy(),
|
||||
LastValidators: state.Validators.Copy(),
|
||||
LastHeightValidatorsChanged: lastHeightValsChanged,
|
||||
ConsensusParams: nextParams,
|
||||
LastHeightConsensusParamsChanged: lastHeightParamsChanged,
|
||||
LastResultsHash: ABCIResponsesResultsHash(abciResponses),
|
||||
AppHash: nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Fire NewBlock, NewBlockHeader.
|
||||
// Fire TxEvent for every tx.
|
||||
// NOTE: if Tendermint crashes before commit, some or all of these events may be published again.
|
||||
func fireEvents(
|
||||
logger log.Logger,
|
||||
eventBus types.BlockEventPublisher,
|
||||
block *types.Block,
|
||||
blockID types.BlockID,
|
||||
abciResponses *tmstate.ABCIResponses,
|
||||
validatorUpdates []*types.Validator,
|
||||
) {
|
||||
if err := eventBus.PublishEventNewBlock(types.EventDataNewBlock{
|
||||
Block: block,
|
||||
BlockID: blockID,
|
||||
ResultBeginBlock: *abciResponses.BeginBlock,
|
||||
ResultEndBlock: *abciResponses.EndBlock,
|
||||
}); err != nil {
|
||||
logger.Error("failed publishing new block", "err", err)
|
||||
}
|
||||
|
||||
if err := eventBus.PublishEventNewBlockHeader(types.EventDataNewBlockHeader{
|
||||
Header: block.Header,
|
||||
NumTxs: int64(len(block.Txs)),
|
||||
ResultBeginBlock: *abciResponses.BeginBlock,
|
||||
ResultEndBlock: *abciResponses.EndBlock,
|
||||
}); err != nil {
|
||||
logger.Error("failed publishing new block header", "err", err)
|
||||
}
|
||||
|
||||
if len(block.Evidence.Evidence) != 0 {
|
||||
for _, ev := range block.Evidence.Evidence {
|
||||
if err := eventBus.PublishEventNewEvidence(types.EventDataNewEvidence{
|
||||
Evidence: ev,
|
||||
Height: block.Height,
|
||||
}); err != nil {
|
||||
logger.Error("failed publishing new evidence", "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i, tx := range block.Data.Txs {
|
||||
if err := eventBus.PublishEventTx(types.EventDataTx{TxResult: abci.TxResult{
|
||||
Height: block.Height,
|
||||
Index: uint32(i),
|
||||
Tx: tx,
|
||||
Result: *(abciResponses.DeliverTxs[i]),
|
||||
}}); err != nil {
|
||||
logger.Error("failed publishing event TX", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(validatorUpdates) > 0 {
|
||||
if err := eventBus.PublishEventValidatorSetUpdates(
|
||||
types.EventDataValidatorSetUpdates{ValidatorUpdates: validatorUpdates}); err != nil {
|
||||
logger.Error("failed publishing event", "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------------------------------
|
||||
// Execute block without state. TODO: eliminate
|
||||
|
||||
// ExecCommitBlock executes and commits a block on the proxyApp without validating or mutating the state.
|
||||
// It returns the application root hash (result of abci.Commit).
|
||||
func ExecCommitBlock(
|
||||
be *BlockExecutor,
|
||||
appConnConsensus proxy.AppConnConsensus,
|
||||
block *types.Block,
|
||||
logger log.Logger,
|
||||
store Store,
|
||||
initialHeight int64,
|
||||
s State,
|
||||
) ([]byte, error) {
|
||||
abciResponses, err := execBlockOnProxyApp(logger, appConnConsensus, block, store, initialHeight)
|
||||
if err != nil {
|
||||
logger.Error("failed executing block on proxy app", "height", block.Height, "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// the BlockExecutor condition is using for the final block replay process.
|
||||
if be != nil {
|
||||
abciValUpdates := abciResponses.EndBlock.ValidatorUpdates
|
||||
err = validateValidatorUpdates(abciValUpdates, s.ConsensusParams.Validator)
|
||||
if err != nil {
|
||||
logger.Error("err", err)
|
||||
return nil, err
|
||||
}
|
||||
validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciValUpdates)
|
||||
if err != nil {
|
||||
logger.Error("err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blockID := types.BlockID{Hash: block.Hash(), PartSetHeader: block.MakePartSet(types.BlockPartSizeBytes).Header()}
|
||||
fireEvents(be.logger, be.eventBus, block, blockID, abciResponses, validatorUpdates)
|
||||
}
|
||||
|
||||
// Commit block, get hash back
|
||||
res, err := appConnConsensus.CommitSync(context.Background())
|
||||
if err != nil {
|
||||
logger.Error("client error during proxyAppConn.CommitSync", "err", res)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ResponseCommit has no error or log, just data
|
||||
return res.Data, nil
|
||||
}
|
||||
|
||||
func (blockExec *BlockExecutor) pruneBlocks(retainHeight int64) (uint64, error) {
|
||||
base := blockExec.blockStore.Base()
|
||||
if retainHeight <= base {
|
||||
return 0, nil
|
||||
}
|
||||
pruned, err := blockExec.blockStore.PruneBlocks(retainHeight)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to prune block store: %w", err)
|
||||
}
|
||||
|
||||
err = blockExec.Store().PruneStates(retainHeight)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to prune state store: %w", err)
|
||||
}
|
||||
return pruned, nil
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
package state_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
abciclient "github.com/tendermint/tendermint/abci/client"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
"github.com/tendermint/tendermint/crypto/ed25519"
|
||||
cryptoenc "github.com/tendermint/tendermint/crypto/encoding"
|
||||
"github.com/tendermint/tendermint/crypto/tmhash"
|
||||
mmock "github.com/tendermint/tendermint/internal/mempool/mock"
|
||||
"github.com/tendermint/tendermint/internal/proxy"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
"github.com/tendermint/tendermint/internal/state/mocks"
|
||||
sf "github.com/tendermint/tendermint/internal/state/test/factory"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
tmtime "github.com/tendermint/tendermint/libs/time"
|
||||
"github.com/tendermint/tendermint/store"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
"github.com/tendermint/tendermint/version"
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
)
|
||||
|
||||
var (
|
||||
chainID = "execution_chain"
|
||||
testPartSize uint32 = 65536
|
||||
)
|
||||
|
||||
func TestApplyBlock(t *testing.T) {
|
||||
app := &testApp{}
|
||||
cc := abciclient.NewLocalCreator(app)
|
||||
proxyApp := proxy.NewAppConns(cc)
|
||||
err := proxyApp.Start()
|
||||
require.Nil(t, err)
|
||||
defer proxyApp.Stop() //nolint:errcheck // ignore for tests
|
||||
|
||||
state, stateDB, _ := makeState(1, 1)
|
||||
stateStore := sm.NewStore(stateDB)
|
||||
blockStore := store.NewBlockStore(dbm.NewMemDB())
|
||||
blockExec := sm.NewBlockExecutor(stateStore, log.TestingLogger(), proxyApp.Consensus(),
|
||||
mmock.Mempool{}, sm.EmptyEvidencePool{}, blockStore)
|
||||
|
||||
block := sf.MakeBlock(state, 1, new(types.Commit))
|
||||
blockID := types.BlockID{Hash: block.Hash(), PartSetHeader: block.MakePartSet(testPartSize).Header()}
|
||||
|
||||
state, err = blockExec.ApplyBlock(state, blockID, block)
|
||||
require.Nil(t, err)
|
||||
|
||||
// TODO check state and mempool
|
||||
assert.EqualValues(t, 1, state.Version.Consensus.App, "App version wasn't updated")
|
||||
}
|
||||
|
||||
// TestBeginBlockValidators ensures we send absent validators list.
|
||||
func TestBeginBlockValidators(t *testing.T) {
|
||||
app := &testApp{}
|
||||
cc := abciclient.NewLocalCreator(app)
|
||||
proxyApp := proxy.NewAppConns(cc)
|
||||
err := proxyApp.Start()
|
||||
require.Nil(t, err)
|
||||
defer proxyApp.Stop() //nolint:errcheck // no need to check error again
|
||||
|
||||
state, stateDB, _ := makeState(2, 2)
|
||||
stateStore := sm.NewStore(stateDB)
|
||||
|
||||
prevHash := state.LastBlockID.Hash
|
||||
prevParts := types.PartSetHeader{}
|
||||
prevBlockID := types.BlockID{Hash: prevHash, PartSetHeader: prevParts}
|
||||
|
||||
var (
|
||||
now = tmtime.Now()
|
||||
commitSig0 = types.NewCommitSigForBlock(
|
||||
[]byte("Signature1"),
|
||||
state.Validators.Validators[0].Address,
|
||||
now)
|
||||
commitSig1 = types.NewCommitSigForBlock(
|
||||
[]byte("Signature2"),
|
||||
state.Validators.Validators[1].Address,
|
||||
now)
|
||||
absentSig = types.NewCommitSigAbsent()
|
||||
)
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
lastCommitSigs []types.CommitSig
|
||||
expectedAbsentValidators []int
|
||||
}{
|
||||
{"none absent", []types.CommitSig{commitSig0, commitSig1}, []int{}},
|
||||
{"one absent", []types.CommitSig{commitSig0, absentSig}, []int{1}},
|
||||
{"multiple absent", []types.CommitSig{absentSig, absentSig}, []int{0, 1}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
lastCommit := types.NewCommit(1, 0, prevBlockID, tc.lastCommitSigs)
|
||||
|
||||
// block for height 2
|
||||
block := sf.MakeBlock(state, 2, lastCommit)
|
||||
|
||||
_, err = sm.ExecCommitBlock(nil, proxyApp.Consensus(), block, log.TestingLogger(), stateStore, 1, state)
|
||||
require.Nil(t, err, tc.desc)
|
||||
|
||||
// -> app receives a list of validators with a bool indicating if they signed
|
||||
ctr := 0
|
||||
for i, v := range app.CommitVotes {
|
||||
if ctr < len(tc.expectedAbsentValidators) &&
|
||||
tc.expectedAbsentValidators[ctr] == i {
|
||||
|
||||
assert.False(t, v.SignedLastBlock)
|
||||
ctr++
|
||||
} else {
|
||||
assert.True(t, v.SignedLastBlock)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBeginBlockByzantineValidators ensures we send byzantine validators list.
|
||||
func TestBeginBlockByzantineValidators(t *testing.T) {
|
||||
app := &testApp{}
|
||||
cc := abciclient.NewLocalCreator(app)
|
||||
proxyApp := proxy.NewAppConns(cc)
|
||||
err := proxyApp.Start()
|
||||
require.Nil(t, err)
|
||||
defer proxyApp.Stop() //nolint:errcheck // ignore for tests
|
||||
|
||||
state, stateDB, privVals := makeState(1, 1)
|
||||
stateStore := sm.NewStore(stateDB)
|
||||
|
||||
defaultEvidenceTime := time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
privVal := privVals[state.Validators.Validators[0].Address.String()]
|
||||
blockID := makeBlockID([]byte("headerhash"), 1000, []byte("partshash"))
|
||||
header := &types.Header{
|
||||
Version: version.Consensus{Block: version.BlockProtocol, App: 1},
|
||||
ChainID: state.ChainID,
|
||||
Height: 10,
|
||||
Time: defaultEvidenceTime,
|
||||
LastBlockID: blockID,
|
||||
LastCommitHash: crypto.CRandBytes(tmhash.Size),
|
||||
DataHash: crypto.CRandBytes(tmhash.Size),
|
||||
ValidatorsHash: state.Validators.Hash(),
|
||||
NextValidatorsHash: state.Validators.Hash(),
|
||||
ConsensusHash: crypto.CRandBytes(tmhash.Size),
|
||||
AppHash: crypto.CRandBytes(tmhash.Size),
|
||||
LastResultsHash: crypto.CRandBytes(tmhash.Size),
|
||||
EvidenceHash: crypto.CRandBytes(tmhash.Size),
|
||||
ProposerAddress: crypto.CRandBytes(crypto.AddressSize),
|
||||
}
|
||||
|
||||
// we don't need to worry about validating the evidence as long as they pass validate basic
|
||||
dve := types.NewMockDuplicateVoteEvidenceWithValidator(3, defaultEvidenceTime, privVal, state.ChainID)
|
||||
dve.ValidatorPower = 1000
|
||||
lcae := &types.LightClientAttackEvidence{
|
||||
ConflictingBlock: &types.LightBlock{
|
||||
SignedHeader: &types.SignedHeader{
|
||||
Header: header,
|
||||
Commit: types.NewCommit(10, 0, makeBlockID(header.Hash(), 100, []byte("partshash")), []types.CommitSig{{
|
||||
BlockIDFlag: types.BlockIDFlagNil,
|
||||
ValidatorAddress: crypto.AddressHash([]byte("validator_address")),
|
||||
Timestamp: defaultEvidenceTime,
|
||||
Signature: crypto.CRandBytes(types.MaxSignatureSize),
|
||||
}}),
|
||||
},
|
||||
ValidatorSet: state.Validators,
|
||||
},
|
||||
CommonHeight: 8,
|
||||
ByzantineValidators: []*types.Validator{state.Validators.Validators[0]},
|
||||
TotalVotingPower: 12,
|
||||
Timestamp: defaultEvidenceTime,
|
||||
}
|
||||
|
||||
ev := []types.Evidence{dve, lcae}
|
||||
|
||||
abciEv := []abci.Evidence{
|
||||
{
|
||||
Type: abci.EvidenceType_DUPLICATE_VOTE,
|
||||
Height: 3,
|
||||
Time: defaultEvidenceTime,
|
||||
Validator: types.TM2PB.Validator(state.Validators.Validators[0]),
|
||||
TotalVotingPower: 10,
|
||||
},
|
||||
{
|
||||
Type: abci.EvidenceType_LIGHT_CLIENT_ATTACK,
|
||||
Height: 8,
|
||||
Time: defaultEvidenceTime,
|
||||
Validator: types.TM2PB.Validator(state.Validators.Validators[0]),
|
||||
TotalVotingPower: 12,
|
||||
},
|
||||
}
|
||||
|
||||
evpool := &mocks.EvidencePool{}
|
||||
evpool.On("PendingEvidence", mock.AnythingOfType("int64")).Return(ev, int64(100))
|
||||
evpool.On("Update", mock.AnythingOfType("state.State"), mock.AnythingOfType("types.EvidenceList")).Return()
|
||||
evpool.On("CheckEvidence", mock.AnythingOfType("types.EvidenceList")).Return(nil)
|
||||
|
||||
blockStore := store.NewBlockStore(dbm.NewMemDB())
|
||||
|
||||
blockExec := sm.NewBlockExecutor(stateStore, log.TestingLogger(), proxyApp.Consensus(),
|
||||
mmock.Mempool{}, evpool, blockStore)
|
||||
|
||||
block := sf.MakeBlock(state, 1, new(types.Commit))
|
||||
block.Evidence = types.EvidenceData{Evidence: ev}
|
||||
block.Header.EvidenceHash = block.Evidence.Hash()
|
||||
blockID = types.BlockID{Hash: block.Hash(), PartSetHeader: block.MakePartSet(testPartSize).Header()}
|
||||
|
||||
_, err = blockExec.ApplyBlock(state, blockID, block)
|
||||
require.Nil(t, err)
|
||||
|
||||
// TODO check state and mempool
|
||||
assert.Equal(t, abciEv, app.ByzantineValidators)
|
||||
}
|
||||
|
||||
func TestValidateValidatorUpdates(t *testing.T) {
|
||||
pubkey1 := ed25519.GenPrivKey().PubKey()
|
||||
pubkey2 := ed25519.GenPrivKey().PubKey()
|
||||
pk1, err := cryptoenc.PubKeyToProto(pubkey1)
|
||||
assert.NoError(t, err)
|
||||
pk2, err := cryptoenc.PubKeyToProto(pubkey2)
|
||||
assert.NoError(t, err)
|
||||
|
||||
defaultValidatorParams := types.ValidatorParams{PubKeyTypes: []string{types.ABCIPubKeyTypeEd25519}}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
abciUpdates []abci.ValidatorUpdate
|
||||
validatorParams types.ValidatorParams
|
||||
|
||||
shouldErr bool
|
||||
}{
|
||||
{
|
||||
"adding a validator is OK",
|
||||
[]abci.ValidatorUpdate{{PubKey: pk2, Power: 20}},
|
||||
defaultValidatorParams,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"updating a validator is OK",
|
||||
[]abci.ValidatorUpdate{{PubKey: pk1, Power: 20}},
|
||||
defaultValidatorParams,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"removing a validator is OK",
|
||||
[]abci.ValidatorUpdate{{PubKey: pk2, Power: 0}},
|
||||
defaultValidatorParams,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"adding a validator with negative power results in error",
|
||||
[]abci.ValidatorUpdate{{PubKey: pk2, Power: -100}},
|
||||
defaultValidatorParams,
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := sm.ValidateValidatorUpdates(tc.abciUpdates, tc.validatorParams)
|
||||
if tc.shouldErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateValidators(t *testing.T) {
|
||||
pubkey1 := ed25519.GenPrivKey().PubKey()
|
||||
val1 := types.NewValidator(pubkey1, 10)
|
||||
pubkey2 := ed25519.GenPrivKey().PubKey()
|
||||
val2 := types.NewValidator(pubkey2, 20)
|
||||
|
||||
pk, err := cryptoenc.PubKeyToProto(pubkey1)
|
||||
require.NoError(t, err)
|
||||
pk2, err := cryptoenc.PubKeyToProto(pubkey2)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
currentSet *types.ValidatorSet
|
||||
abciUpdates []abci.ValidatorUpdate
|
||||
|
||||
resultingSet *types.ValidatorSet
|
||||
shouldErr bool
|
||||
}{
|
||||
{
|
||||
"adding a validator is OK",
|
||||
types.NewValidatorSet([]*types.Validator{val1}),
|
||||
[]abci.ValidatorUpdate{{PubKey: pk2, Power: 20}},
|
||||
types.NewValidatorSet([]*types.Validator{val1, val2}),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"updating a validator is OK",
|
||||
types.NewValidatorSet([]*types.Validator{val1}),
|
||||
[]abci.ValidatorUpdate{{PubKey: pk, Power: 20}},
|
||||
types.NewValidatorSet([]*types.Validator{types.NewValidator(pubkey1, 20)}),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"removing a validator is OK",
|
||||
types.NewValidatorSet([]*types.Validator{val1, val2}),
|
||||
[]abci.ValidatorUpdate{{PubKey: pk2, Power: 0}},
|
||||
types.NewValidatorSet([]*types.Validator{val1}),
|
||||
false,
|
||||
},
|
||||
{
|
||||
"removing a non-existing validator results in error",
|
||||
types.NewValidatorSet([]*types.Validator{val1}),
|
||||
[]abci.ValidatorUpdate{{PubKey: pk2, Power: 0}},
|
||||
types.NewValidatorSet([]*types.Validator{val1}),
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
updates, err := types.PB2TM.ValidatorUpdates(tc.abciUpdates)
|
||||
assert.NoError(t, err)
|
||||
err = tc.currentSet.UpdateWithChangeSet(updates)
|
||||
if tc.shouldErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
require.Equal(t, tc.resultingSet.Size(), tc.currentSet.Size())
|
||||
|
||||
assert.Equal(t, tc.resultingSet.TotalVotingPower(), tc.currentSet.TotalVotingPower())
|
||||
|
||||
assert.Equal(t, tc.resultingSet.Validators[0].Address, tc.currentSet.Validators[0].Address)
|
||||
if tc.resultingSet.Size() > 1 {
|
||||
assert.Equal(t, tc.resultingSet.Validators[1].Address, tc.currentSet.Validators[1].Address)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestEndBlockValidatorUpdates ensures we update validator set and send an event.
|
||||
func TestEndBlockValidatorUpdates(t *testing.T) {
|
||||
app := &testApp{}
|
||||
cc := abciclient.NewLocalCreator(app)
|
||||
proxyApp := proxy.NewAppConns(cc)
|
||||
err := proxyApp.Start()
|
||||
require.Nil(t, err)
|
||||
defer proxyApp.Stop() //nolint:errcheck // ignore for tests
|
||||
|
||||
state, stateDB, _ := makeState(1, 1)
|
||||
stateStore := sm.NewStore(stateDB)
|
||||
blockStore := store.NewBlockStore(dbm.NewMemDB())
|
||||
|
||||
blockExec := sm.NewBlockExecutor(
|
||||
stateStore,
|
||||
log.TestingLogger(),
|
||||
proxyApp.Consensus(),
|
||||
mmock.Mempool{},
|
||||
sm.EmptyEvidencePool{},
|
||||
blockStore,
|
||||
)
|
||||
|
||||
eventBus := types.NewEventBus()
|
||||
err = eventBus.Start()
|
||||
require.NoError(t, err)
|
||||
defer eventBus.Stop() //nolint:errcheck // ignore for tests
|
||||
|
||||
blockExec.SetEventBus(eventBus)
|
||||
|
||||
updatesSub, err := eventBus.Subscribe(
|
||||
context.Background(),
|
||||
"TestEndBlockValidatorUpdates",
|
||||
types.EventQueryValidatorSetUpdates,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
block := sf.MakeBlock(state, 1, new(types.Commit))
|
||||
blockID := types.BlockID{Hash: block.Hash(), PartSetHeader: block.MakePartSet(testPartSize).Header()}
|
||||
|
||||
pubkey := ed25519.GenPrivKey().PubKey()
|
||||
pk, err := cryptoenc.PubKeyToProto(pubkey)
|
||||
require.NoError(t, err)
|
||||
app.ValidatorUpdates = []abci.ValidatorUpdate{
|
||||
{PubKey: pk, Power: 10},
|
||||
}
|
||||
|
||||
state, err = blockExec.ApplyBlock(state, blockID, block)
|
||||
require.Nil(t, err)
|
||||
// test new validator was added to NextValidators
|
||||
if assert.Equal(t, state.Validators.Size()+1, state.NextValidators.Size()) {
|
||||
idx, _ := state.NextValidators.GetByAddress(pubkey.Address())
|
||||
if idx < 0 {
|
||||
t.Fatalf("can't find address %v in the set %v", pubkey.Address(), state.NextValidators)
|
||||
}
|
||||
}
|
||||
|
||||
// test we threw an event
|
||||
select {
|
||||
case msg := <-updatesSub.Out():
|
||||
event, ok := msg.Data().(types.EventDataValidatorSetUpdates)
|
||||
require.True(t, ok, "Expected event of type EventDataValidatorSetUpdates, got %T", msg.Data())
|
||||
if assert.NotEmpty(t, event.ValidatorUpdates) {
|
||||
assert.Equal(t, pubkey, event.ValidatorUpdates[0].PubKey)
|
||||
assert.EqualValues(t, 10, event.ValidatorUpdates[0].VotingPower)
|
||||
}
|
||||
case <-updatesSub.Canceled():
|
||||
t.Fatalf("updatesSub was canceled (reason: %v)", updatesSub.Err())
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatal("Did not receive EventValidatorSetUpdates within 1 sec.")
|
||||
}
|
||||
}
|
||||
|
||||
// TestEndBlockValidatorUpdatesResultingInEmptySet checks that processing validator updates that
|
||||
// would result in empty set causes no panic, an error is raised and NextValidators is not updated
|
||||
func TestEndBlockValidatorUpdatesResultingInEmptySet(t *testing.T) {
|
||||
app := &testApp{}
|
||||
cc := abciclient.NewLocalCreator(app)
|
||||
proxyApp := proxy.NewAppConns(cc)
|
||||
err := proxyApp.Start()
|
||||
require.Nil(t, err)
|
||||
defer proxyApp.Stop() //nolint:errcheck // ignore for tests
|
||||
|
||||
state, stateDB, _ := makeState(1, 1)
|
||||
stateStore := sm.NewStore(stateDB)
|
||||
blockStore := store.NewBlockStore(dbm.NewMemDB())
|
||||
blockExec := sm.NewBlockExecutor(
|
||||
stateStore,
|
||||
log.TestingLogger(),
|
||||
proxyApp.Consensus(),
|
||||
mmock.Mempool{},
|
||||
sm.EmptyEvidencePool{},
|
||||
blockStore,
|
||||
)
|
||||
|
||||
block := sf.MakeBlock(state, 1, new(types.Commit))
|
||||
blockID := types.BlockID{Hash: block.Hash(), PartSetHeader: block.MakePartSet(testPartSize).Header()}
|
||||
|
||||
vp, err := cryptoenc.PubKeyToProto(state.Validators.Validators[0].PubKey)
|
||||
require.NoError(t, err)
|
||||
// Remove the only validator
|
||||
app.ValidatorUpdates = []abci.ValidatorUpdate{
|
||||
{PubKey: vp, Power: 0},
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() { state, err = blockExec.ApplyBlock(state, blockID, block) })
|
||||
assert.NotNil(t, err)
|
||||
assert.NotEmpty(t, state.NextValidators.Validators)
|
||||
}
|
||||
|
||||
func makeBlockID(hash []byte, partSetSize uint32, partSetHash []byte) types.BlockID {
|
||||
var (
|
||||
h = make([]byte, tmhash.Size)
|
||||
psH = make([]byte, tmhash.Size)
|
||||
)
|
||||
copy(h, hash)
|
||||
copy(psH, partSetHash)
|
||||
return types.BlockID{
|
||||
Hash: h,
|
||||
PartSetHeader: types.PartSetHeader{
|
||||
Total: partSetSize,
|
||||
Hash: psH,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
tmstate "github.com/tendermint/tendermint/proto/tendermint/state"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
//
|
||||
// TODO: Remove dependence on all entities exported from this file.
|
||||
//
|
||||
// Every entity exported here is dependent on a private entity from the `state`
|
||||
// package. Currently, these functions are only made available to tests in the
|
||||
// `state_test` package, but we should not be relying on them for our testing.
|
||||
// Instead, we should be exclusively relying on exported entities for our
|
||||
// testing, and should be refactoring exported entities to make them more
|
||||
// easily testable from outside of the package.
|
||||
//
|
||||
|
||||
// UpdateState is an alias for updateState exported from execution.go,
|
||||
// exclusively and explicitly for testing.
|
||||
func UpdateState(
|
||||
state State,
|
||||
blockID types.BlockID,
|
||||
header *types.Header,
|
||||
abciResponses *tmstate.ABCIResponses,
|
||||
validatorUpdates []*types.Validator,
|
||||
) (State, error) {
|
||||
return updateState(state, blockID, header, abciResponses, validatorUpdates)
|
||||
}
|
||||
|
||||
// ValidateValidatorUpdates is an alias for validateValidatorUpdates exported
|
||||
// from execution.go, exclusively and explicitly for testing.
|
||||
func ValidateValidatorUpdates(abciUpdates []abci.ValidatorUpdate, params types.ValidatorParams) error {
|
||||
return validateValidatorUpdates(abciUpdates, params)
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
package state_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
|
||||
abciclient "github.com/tendermint/tendermint/abci/client"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
"github.com/tendermint/tendermint/crypto/ed25519"
|
||||
cryptoenc "github.com/tendermint/tendermint/crypto/encoding"
|
||||
"github.com/tendermint/tendermint/internal/proxy"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
sf "github.com/tendermint/tendermint/internal/state/test/factory"
|
||||
"github.com/tendermint/tendermint/internal/test/factory"
|
||||
tmrand "github.com/tendermint/tendermint/libs/rand"
|
||||
tmtime "github.com/tendermint/tendermint/libs/time"
|
||||
tmstate "github.com/tendermint/tendermint/proto/tendermint/state"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
type paramsChangeTestCase struct {
|
||||
height int64
|
||||
params types.ConsensusParams
|
||||
}
|
||||
|
||||
func newTestApp() proxy.AppConns {
|
||||
app := &testApp{}
|
||||
cc := abciclient.NewLocalCreator(app)
|
||||
return proxy.NewAppConns(cc)
|
||||
}
|
||||
|
||||
func makeAndCommitGoodBlock(
|
||||
state sm.State,
|
||||
height int64,
|
||||
lastCommit *types.Commit,
|
||||
proposerAddr []byte,
|
||||
blockExec *sm.BlockExecutor,
|
||||
privVals map[string]types.PrivValidator,
|
||||
evidence []types.Evidence) (sm.State, types.BlockID, *types.Commit, error) {
|
||||
// A good block passes
|
||||
state, blockID, err := makeAndApplyGoodBlock(state, height, lastCommit, proposerAddr, blockExec, evidence)
|
||||
if err != nil {
|
||||
return state, types.BlockID{}, nil, err
|
||||
}
|
||||
|
||||
// Simulate a lastCommit for this block from all validators for the next height
|
||||
commit, err := makeValidCommit(height, blockID, state.Validators, privVals)
|
||||
if err != nil {
|
||||
return state, types.BlockID{}, nil, err
|
||||
}
|
||||
return state, blockID, commit, nil
|
||||
}
|
||||
|
||||
func makeAndApplyGoodBlock(state sm.State, height int64, lastCommit *types.Commit, proposerAddr []byte,
|
||||
blockExec *sm.BlockExecutor, evidence []types.Evidence) (sm.State, types.BlockID, error) {
|
||||
block, _ := state.MakeBlock(height, factory.MakeTenTxs(height), lastCommit, evidence, proposerAddr)
|
||||
if err := blockExec.ValidateBlock(state, block); err != nil {
|
||||
return state, types.BlockID{}, err
|
||||
}
|
||||
blockID := types.BlockID{Hash: block.Hash(),
|
||||
PartSetHeader: types.PartSetHeader{Total: 3, Hash: tmrand.Bytes(32)}}
|
||||
state, err := blockExec.ApplyBlock(state, blockID, block)
|
||||
if err != nil {
|
||||
return state, types.BlockID{}, err
|
||||
}
|
||||
return state, blockID, nil
|
||||
}
|
||||
|
||||
func makeValidCommit(
|
||||
height int64,
|
||||
blockID types.BlockID,
|
||||
vals *types.ValidatorSet,
|
||||
privVals map[string]types.PrivValidator,
|
||||
) (*types.Commit, error) {
|
||||
sigs := make([]types.CommitSig, 0)
|
||||
for i := 0; i < vals.Size(); i++ {
|
||||
_, val := vals.GetByIndex(int32(i))
|
||||
vote, err := factory.MakeVote(privVals[val.Address.String()], chainID, int32(i), height, 0, 2, blockID, time.Now())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sigs = append(sigs, vote.CommitSig())
|
||||
}
|
||||
return types.NewCommit(height, 0, blockID, sigs), nil
|
||||
}
|
||||
|
||||
func makeState(nVals, height int) (sm.State, dbm.DB, map[string]types.PrivValidator) {
|
||||
vals := make([]types.GenesisValidator, nVals)
|
||||
privVals := make(map[string]types.PrivValidator, nVals)
|
||||
for i := 0; i < nVals; i++ {
|
||||
secret := []byte(fmt.Sprintf("test%d", i))
|
||||
pk := ed25519.GenPrivKeyFromSecret(secret)
|
||||
valAddr := pk.PubKey().Address()
|
||||
vals[i] = types.GenesisValidator{
|
||||
Address: valAddr,
|
||||
PubKey: pk.PubKey(),
|
||||
Power: 1000,
|
||||
Name: fmt.Sprintf("test%d", i),
|
||||
}
|
||||
privVals[valAddr.String()] = types.NewMockPVWithParams(pk, false, false)
|
||||
}
|
||||
s, _ := sm.MakeGenesisState(&types.GenesisDoc{
|
||||
ChainID: chainID,
|
||||
Validators: vals,
|
||||
AppHash: nil,
|
||||
})
|
||||
|
||||
stateDB := dbm.NewMemDB()
|
||||
stateStore := sm.NewStore(stateDB)
|
||||
if err := stateStore.Save(s); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for i := 1; i < height; i++ {
|
||||
s.LastBlockHeight++
|
||||
s.LastValidators = s.Validators.Copy()
|
||||
if err := stateStore.Save(s); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
return s, stateDB, privVals
|
||||
}
|
||||
|
||||
func genValSet(size int) *types.ValidatorSet {
|
||||
vals := make([]*types.Validator, size)
|
||||
for i := 0; i < size; i++ {
|
||||
vals[i] = types.NewValidator(ed25519.GenPrivKey().PubKey(), 10)
|
||||
}
|
||||
return types.NewValidatorSet(vals)
|
||||
}
|
||||
|
||||
func makeHeaderPartsResponsesValPubKeyChange(
|
||||
state sm.State,
|
||||
pubkey crypto.PubKey,
|
||||
) (types.Header, types.BlockID, *tmstate.ABCIResponses) {
|
||||
|
||||
block := sf.MakeBlock(state, state.LastBlockHeight+1, new(types.Commit))
|
||||
abciResponses := &tmstate.ABCIResponses{
|
||||
BeginBlock: &abci.ResponseBeginBlock{},
|
||||
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
|
||||
}
|
||||
// If the pubkey is new, remove the old and add the new.
|
||||
_, val := state.NextValidators.GetByIndex(0)
|
||||
if !bytes.Equal(pubkey.Bytes(), val.PubKey.Bytes()) {
|
||||
vPbPk, err := cryptoenc.PubKeyToProto(val.PubKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
pbPk, err := cryptoenc.PubKeyToProto(pubkey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
abciResponses.EndBlock = &abci.ResponseEndBlock{
|
||||
ValidatorUpdates: []abci.ValidatorUpdate{
|
||||
{PubKey: vPbPk, Power: 0},
|
||||
{PubKey: pbPk, Power: 10},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return block.Header, types.BlockID{Hash: block.Hash(), PartSetHeader: types.PartSetHeader{}}, abciResponses
|
||||
}
|
||||
|
||||
func makeHeaderPartsResponsesValPowerChange(
|
||||
state sm.State,
|
||||
power int64,
|
||||
) (types.Header, types.BlockID, *tmstate.ABCIResponses) {
|
||||
|
||||
block := sf.MakeBlock(state, state.LastBlockHeight+1, new(types.Commit))
|
||||
abciResponses := &tmstate.ABCIResponses{
|
||||
BeginBlock: &abci.ResponseBeginBlock{},
|
||||
EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil},
|
||||
}
|
||||
|
||||
// If the pubkey is new, remove the old and add the new.
|
||||
_, val := state.NextValidators.GetByIndex(0)
|
||||
if val.VotingPower != power {
|
||||
vPbPk, err := cryptoenc.PubKeyToProto(val.PubKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
abciResponses.EndBlock = &abci.ResponseEndBlock{
|
||||
ValidatorUpdates: []abci.ValidatorUpdate{
|
||||
{PubKey: vPbPk, Power: power},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return block.Header, types.BlockID{Hash: block.Hash(), PartSetHeader: types.PartSetHeader{}}, abciResponses
|
||||
}
|
||||
|
||||
func makeHeaderPartsResponsesParams(
|
||||
state sm.State,
|
||||
params *types.ConsensusParams,
|
||||
) (types.Header, types.BlockID, *tmstate.ABCIResponses) {
|
||||
|
||||
block := sf.MakeBlock(state, state.LastBlockHeight+1, new(types.Commit))
|
||||
pbParams := params.ToProto()
|
||||
abciResponses := &tmstate.ABCIResponses{
|
||||
BeginBlock: &abci.ResponseBeginBlock{},
|
||||
EndBlock: &abci.ResponseEndBlock{ConsensusParamUpdates: &pbParams},
|
||||
}
|
||||
return block.Header, types.BlockID{Hash: block.Hash(), PartSetHeader: types.PartSetHeader{}}, abciResponses
|
||||
}
|
||||
|
||||
func randomGenesisDoc() *types.GenesisDoc {
|
||||
pubkey := ed25519.GenPrivKey().PubKey()
|
||||
return &types.GenesisDoc{
|
||||
GenesisTime: tmtime.Now(),
|
||||
ChainID: "abc",
|
||||
Validators: []types.GenesisValidator{
|
||||
{
|
||||
Address: pubkey.Address(),
|
||||
PubKey: pubkey,
|
||||
Power: 10,
|
||||
Name: "myval",
|
||||
},
|
||||
},
|
||||
ConsensusParams: types.DefaultConsensusParams(),
|
||||
}
|
||||
}
|
||||
|
||||
// used for testing by state store
|
||||
func makeRandomStateFromValidatorSet(
|
||||
lastValSet *types.ValidatorSet,
|
||||
height, lastHeightValidatorsChanged int64,
|
||||
) sm.State {
|
||||
return sm.State{
|
||||
LastBlockHeight: height - 1,
|
||||
NextValidators: lastValSet.CopyIncrementProposerPriority(2),
|
||||
Validators: lastValSet.CopyIncrementProposerPriority(1),
|
||||
LastValidators: lastValSet.Copy(),
|
||||
LastHeightConsensusParamsChanged: height,
|
||||
ConsensusParams: *types.DefaultConsensusParams(),
|
||||
LastHeightValidatorsChanged: lastHeightValidatorsChanged,
|
||||
InitialHeight: 1,
|
||||
}
|
||||
}
|
||||
|
||||
func makeRandomStateFromConsensusParams(consensusParams *types.ConsensusParams,
|
||||
height, lastHeightConsensusParamsChanged int64) sm.State {
|
||||
val, _ := factory.RandValidator(true, 10)
|
||||
valSet := types.NewValidatorSet([]*types.Validator{val})
|
||||
return sm.State{
|
||||
LastBlockHeight: height - 1,
|
||||
ConsensusParams: *consensusParams,
|
||||
LastHeightConsensusParamsChanged: lastHeightConsensusParamsChanged,
|
||||
NextValidators: valSet.CopyIncrementProposerPriority(2),
|
||||
Validators: valSet.CopyIncrementProposerPriority(1),
|
||||
LastValidators: valSet.Copy(),
|
||||
LastHeightValidatorsChanged: height,
|
||||
InitialHeight: 1,
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
|
||||
type testApp struct {
|
||||
abci.BaseApplication
|
||||
|
||||
CommitVotes []abci.VoteInfo
|
||||
ByzantineValidators []abci.Evidence
|
||||
ValidatorUpdates []abci.ValidatorUpdate
|
||||
}
|
||||
|
||||
var _ abci.Application = (*testApp)(nil)
|
||||
|
||||
func (app *testApp) Info(req abci.RequestInfo) (resInfo abci.ResponseInfo) {
|
||||
return abci.ResponseInfo{}
|
||||
}
|
||||
|
||||
func (app *testApp) BeginBlock(req abci.RequestBeginBlock) abci.ResponseBeginBlock {
|
||||
app.CommitVotes = req.LastCommitInfo.Votes
|
||||
app.ByzantineValidators = req.ByzantineValidators
|
||||
return abci.ResponseBeginBlock{}
|
||||
}
|
||||
|
||||
func (app *testApp) EndBlock(req abci.RequestEndBlock) abci.ResponseEndBlock {
|
||||
return abci.ResponseEndBlock{
|
||||
ValidatorUpdates: app.ValidatorUpdates,
|
||||
ConsensusParamUpdates: &tmproto.ConsensusParams{
|
||||
Version: &tmproto.VersionParams{
|
||||
AppVersion: 1}}}
|
||||
}
|
||||
|
||||
func (app *testApp) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliverTx {
|
||||
return abci.ResponseDeliverTx{Events: []abci.Event{}}
|
||||
}
|
||||
|
||||
func (app *testApp) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx {
|
||||
return abci.ResponseCheckTx{}
|
||||
}
|
||||
|
||||
func (app *testApp) Commit() abci.ResponseCommit {
|
||||
return abci.ResponseCommit{RetainHeight: 1}
|
||||
}
|
||||
|
||||
func (app *testApp) Query(reqQuery abci.RequestQuery) (resQuery abci.ResponseQuery) {
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,493 @@
|
||||
package kv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/google/orderedcode"
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/internal/state/indexer"
|
||||
"github.com/tendermint/tendermint/libs/pubsub/query"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
var _ indexer.BlockIndexer = (*BlockerIndexer)(nil)
|
||||
|
||||
// BlockerIndexer implements a block indexer, indexing BeginBlock and EndBlock
|
||||
// events with an underlying KV store. Block events are indexed by their height,
|
||||
// such that matching search criteria returns the respective block height(s).
|
||||
type BlockerIndexer struct {
|
||||
store dbm.DB
|
||||
}
|
||||
|
||||
func New(store dbm.DB) *BlockerIndexer {
|
||||
return &BlockerIndexer{
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
// Has returns true if the given height has been indexed. An error is returned
|
||||
// upon database query failure.
|
||||
func (idx *BlockerIndexer) Has(height int64) (bool, error) {
|
||||
key, err := heightKey(height)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to create block height index key: %w", err)
|
||||
}
|
||||
|
||||
return idx.store.Has(key)
|
||||
}
|
||||
|
||||
// Index indexes BeginBlock and EndBlock events for a given block by its height.
|
||||
// The following is indexed:
|
||||
//
|
||||
// primary key: encode(block.height | height) => encode(height)
|
||||
// BeginBlock events: encode(eventType.eventAttr|eventValue|height|begin_block) => encode(height)
|
||||
// EndBlock events: encode(eventType.eventAttr|eventValue|height|end_block) => encode(height)
|
||||
func (idx *BlockerIndexer) Index(bh types.EventDataNewBlockHeader) error {
|
||||
batch := idx.store.NewBatch()
|
||||
defer batch.Close()
|
||||
|
||||
height := bh.Header.Height
|
||||
|
||||
// 1. index by height
|
||||
key, err := heightKey(height)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create block height index key: %w", err)
|
||||
}
|
||||
if err := batch.Set(key, int64ToBytes(height)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. index BeginBlock events
|
||||
if err := idx.indexEvents(batch, bh.ResultBeginBlock.Events, "begin_block", height); err != nil {
|
||||
return fmt.Errorf("failed to index BeginBlock events: %w", err)
|
||||
}
|
||||
|
||||
// 3. index EndBlock events
|
||||
if err := idx.indexEvents(batch, bh.ResultEndBlock.Events, "end_block", height); err != nil {
|
||||
return fmt.Errorf("failed to index EndBlock events: %w", err)
|
||||
}
|
||||
|
||||
return batch.WriteSync()
|
||||
}
|
||||
|
||||
// Search performs a query for block heights that match a given BeginBlock
|
||||
// and Endblock event search criteria. The given query can match against zero,
|
||||
// one or more block heights. In the case of height queries, i.e. block.height=H,
|
||||
// if the height is indexed, that height alone will be returned. An error and
|
||||
// nil slice is returned. Otherwise, a non-nil slice and nil error is returned.
|
||||
func (idx *BlockerIndexer) Search(ctx context.Context, q *query.Query) ([]int64, error) {
|
||||
results := make([]int64, 0)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return results, nil
|
||||
|
||||
default:
|
||||
}
|
||||
|
||||
conditions, err := q.Conditions()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse query conditions: %w", err)
|
||||
}
|
||||
|
||||
// If there is an exact height query, return the result immediately
|
||||
// (if it exists).
|
||||
height, ok := lookForHeight(conditions)
|
||||
if ok {
|
||||
ok, err := idx.Has(height)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ok {
|
||||
return []int64{height}, nil
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
var heightsInitialized bool
|
||||
filteredHeights := make(map[string][]byte)
|
||||
|
||||
// conditions to skip because they're handled before "everything else"
|
||||
skipIndexes := make([]int, 0)
|
||||
|
||||
// Extract ranges. If both upper and lower bounds exist, it's better to get
|
||||
// them in order as to not iterate over kvs that are not within range.
|
||||
ranges, rangeIndexes := indexer.LookForRanges(conditions)
|
||||
if len(ranges) > 0 {
|
||||
skipIndexes = append(skipIndexes, rangeIndexes...)
|
||||
|
||||
for _, qr := range ranges {
|
||||
prefix, err := orderedcode.Append(nil, qr.Key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create prefix key: %w", err)
|
||||
}
|
||||
|
||||
if !heightsInitialized {
|
||||
filteredHeights, err = idx.matchRange(ctx, qr, prefix, filteredHeights, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
heightsInitialized = true
|
||||
|
||||
// Ignore any remaining conditions if the first condition resulted in no
|
||||
// matches (assuming implicit AND operand).
|
||||
if len(filteredHeights) == 0 {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
filteredHeights, err = idx.matchRange(ctx, qr, prefix, filteredHeights, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for all other conditions
|
||||
for i, c := range conditions {
|
||||
if intInSlice(i, skipIndexes) {
|
||||
continue
|
||||
}
|
||||
|
||||
startKey, err := orderedcode.Append(nil, c.CompositeKey, fmt.Sprintf("%v", c.Operand))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !heightsInitialized {
|
||||
filteredHeights, err = idx.match(ctx, c, startKey, filteredHeights, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
heightsInitialized = true
|
||||
|
||||
// Ignore any remaining conditions if the first condition resulted in no
|
||||
// matches (assuming implicit AND operand).
|
||||
if len(filteredHeights) == 0 {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
filteredHeights, err = idx.match(ctx, c, startKey, filteredHeights, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fetch matching heights
|
||||
results = make([]int64, 0, len(filteredHeights))
|
||||
heights:
|
||||
for _, hBz := range filteredHeights {
|
||||
h := int64FromBytes(hBz)
|
||||
|
||||
ok, err := idx.Has(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
results = append(results, h)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break heights
|
||||
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(results, func(i, j int) bool { return results[i] < results[j] })
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// matchRange returns all matching block heights that match a given QueryRange
|
||||
// and start key. An already filtered result (filteredHeights) is provided such
|
||||
// that any non-intersecting matches are removed.
|
||||
//
|
||||
// NOTE: The provided filteredHeights may be empty if no previous condition has
|
||||
// matched.
|
||||
func (idx *BlockerIndexer) matchRange(
|
||||
ctx context.Context,
|
||||
qr indexer.QueryRange,
|
||||
startKey []byte,
|
||||
filteredHeights map[string][]byte,
|
||||
firstRun bool,
|
||||
) (map[string][]byte, error) {
|
||||
|
||||
// A previous match was attempted but resulted in no matches, so we return
|
||||
// no matches (assuming AND operand).
|
||||
if !firstRun && len(filteredHeights) == 0 {
|
||||
return filteredHeights, nil
|
||||
}
|
||||
|
||||
tmpHeights := make(map[string][]byte)
|
||||
lowerBound := qr.LowerBoundValue()
|
||||
upperBound := qr.UpperBoundValue()
|
||||
|
||||
it, err := dbm.IteratePrefix(idx.store, startKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create prefix iterator: %w", err)
|
||||
}
|
||||
defer it.Close()
|
||||
|
||||
iter:
|
||||
for ; it.Valid(); it.Next() {
|
||||
var (
|
||||
eventValue string
|
||||
err error
|
||||
)
|
||||
|
||||
if qr.Key == types.BlockHeightKey {
|
||||
eventValue, err = parseValueFromPrimaryKey(it.Key())
|
||||
} else {
|
||||
eventValue, err = parseValueFromEventKey(it.Key())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := qr.AnyBound().(int64); ok {
|
||||
v, err := strconv.ParseInt(eventValue, 10, 64)
|
||||
if err != nil {
|
||||
continue iter
|
||||
}
|
||||
|
||||
include := true
|
||||
if lowerBound != nil && v < lowerBound.(int64) {
|
||||
include = false
|
||||
}
|
||||
|
||||
if upperBound != nil && v > upperBound.(int64) {
|
||||
include = false
|
||||
}
|
||||
|
||||
if include {
|
||||
tmpHeights[string(it.Value())] = it.Value()
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break iter
|
||||
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
if err := it.Error(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(tmpHeights) == 0 || firstRun {
|
||||
// Either:
|
||||
//
|
||||
// 1. Regardless if a previous match was attempted, which may have had
|
||||
// results, but no match was found for the current condition, then we
|
||||
// return no matches (assuming AND operand).
|
||||
//
|
||||
// 2. A previous match was not attempted, so we return all results.
|
||||
return tmpHeights, nil
|
||||
}
|
||||
|
||||
// Remove/reduce matches in filteredHashes that were not found in this
|
||||
// match (tmpHashes).
|
||||
for k := range filteredHeights {
|
||||
if tmpHeights[k] == nil {
|
||||
delete(filteredHeights, k)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break
|
||||
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredHeights, nil
|
||||
}
|
||||
|
||||
// match returns all matching heights that meet a given query condition and start
|
||||
// key. An already filtered result (filteredHeights) is provided such that any
|
||||
// non-intersecting matches are removed.
|
||||
//
|
||||
// NOTE: The provided filteredHeights may be empty if no previous condition has
|
||||
// matched.
|
||||
func (idx *BlockerIndexer) match(
|
||||
ctx context.Context,
|
||||
c query.Condition,
|
||||
startKeyBz []byte,
|
||||
filteredHeights map[string][]byte,
|
||||
firstRun bool,
|
||||
) (map[string][]byte, error) {
|
||||
|
||||
// A previous match was attempted but resulted in no matches, so we return
|
||||
// no matches (assuming AND operand).
|
||||
if !firstRun && len(filteredHeights) == 0 {
|
||||
return filteredHeights, nil
|
||||
}
|
||||
|
||||
tmpHeights := make(map[string][]byte)
|
||||
|
||||
switch {
|
||||
case c.Op == query.OpEqual:
|
||||
it, err := dbm.IteratePrefix(idx.store, startKeyBz)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create prefix iterator: %w", err)
|
||||
}
|
||||
defer it.Close()
|
||||
|
||||
for ; it.Valid(); it.Next() {
|
||||
tmpHeights[string(it.Value())] = it.Value()
|
||||
|
||||
if err := ctx.Err(); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err := it.Error(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case c.Op == query.OpExists:
|
||||
prefix, err := orderedcode.Append(nil, c.CompositeKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
it, err := dbm.IteratePrefix(idx.store, prefix)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create prefix iterator: %w", err)
|
||||
}
|
||||
defer it.Close()
|
||||
|
||||
iterExists:
|
||||
for ; it.Valid(); it.Next() {
|
||||
tmpHeights[string(it.Value())] = it.Value()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break iterExists
|
||||
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
if err := it.Error(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case c.Op == query.OpContains:
|
||||
prefix, err := orderedcode.Append(nil, c.CompositeKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
it, err := dbm.IteratePrefix(idx.store, prefix)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create prefix iterator: %w", err)
|
||||
}
|
||||
defer it.Close()
|
||||
|
||||
iterContains:
|
||||
for ; it.Valid(); it.Next() {
|
||||
eventValue, err := parseValueFromEventKey(it.Key())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(eventValue, c.Operand.(string)) {
|
||||
tmpHeights[string(it.Value())] = it.Value()
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break iterContains
|
||||
|
||||
default:
|
||||
}
|
||||
}
|
||||
if err := it.Error(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, errors.New("other operators should be handled already")
|
||||
}
|
||||
|
||||
if len(tmpHeights) == 0 || firstRun {
|
||||
// Either:
|
||||
//
|
||||
// 1. Regardless if a previous match was attempted, which may have had
|
||||
// results, but no match was found for the current condition, then we
|
||||
// return no matches (assuming AND operand).
|
||||
//
|
||||
// 2. A previous match was not attempted, so we return all results.
|
||||
return tmpHeights, nil
|
||||
}
|
||||
|
||||
// Remove/reduce matches in filteredHeights that were not found in this
|
||||
// match (tmpHeights).
|
||||
for k := range filteredHeights {
|
||||
if tmpHeights[k] == nil {
|
||||
delete(filteredHeights, k)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break
|
||||
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredHeights, nil
|
||||
}
|
||||
|
||||
func (idx *BlockerIndexer) indexEvents(batch dbm.Batch, events []abci.Event, typ string, height int64) error {
|
||||
heightBz := int64ToBytes(height)
|
||||
|
||||
for _, event := range events {
|
||||
// only index events with a non-empty type
|
||||
if len(event.Type) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, attr := range event.Attributes {
|
||||
if len(attr.Key) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// index iff the event specified index:true and it's not a reserved event
|
||||
compositeKey := fmt.Sprintf("%s.%s", event.Type, attr.Key)
|
||||
if compositeKey == types.BlockHeightKey {
|
||||
return fmt.Errorf("event type and attribute key \"%s\" is reserved; please use a different key", compositeKey)
|
||||
}
|
||||
|
||||
if attr.GetIndex() {
|
||||
key, err := eventKey(compositeKey, typ, attr.Value, height)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create block index key: %w", err)
|
||||
}
|
||||
|
||||
if err := batch.Set(key, heightBz); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package kv_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
blockidxkv "github.com/tendermint/tendermint/internal/state/indexer/block/kv"
|
||||
"github.com/tendermint/tendermint/libs/pubsub/query"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
db "github.com/tendermint/tm-db"
|
||||
)
|
||||
|
||||
func TestBlockIndexer(t *testing.T) {
|
||||
store := db.NewPrefixDB(db.NewMemDB(), []byte("block_events"))
|
||||
indexer := blockidxkv.New(store)
|
||||
|
||||
require.NoError(t, indexer.Index(types.EventDataNewBlockHeader{
|
||||
Header: types.Header{Height: 1},
|
||||
ResultBeginBlock: abci.ResponseBeginBlock{
|
||||
Events: []abci.Event{
|
||||
{
|
||||
Type: "begin_event",
|
||||
Attributes: []abci.EventAttribute{
|
||||
{
|
||||
Key: "proposer",
|
||||
Value: "FCAA001",
|
||||
Index: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ResultEndBlock: abci.ResponseEndBlock{
|
||||
Events: []abci.Event{
|
||||
{
|
||||
Type: "end_event",
|
||||
Attributes: []abci.EventAttribute{
|
||||
{
|
||||
Key: "foo",
|
||||
Value: "100",
|
||||
Index: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
for i := 2; i < 12; i++ {
|
||||
var index bool
|
||||
if i%2 == 0 {
|
||||
index = true
|
||||
}
|
||||
|
||||
require.NoError(t, indexer.Index(types.EventDataNewBlockHeader{
|
||||
Header: types.Header{Height: int64(i)},
|
||||
ResultBeginBlock: abci.ResponseBeginBlock{
|
||||
Events: []abci.Event{
|
||||
{
|
||||
Type: "begin_event",
|
||||
Attributes: []abci.EventAttribute{
|
||||
{
|
||||
Key: "proposer",
|
||||
Value: "FCAA001",
|
||||
Index: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ResultEndBlock: abci.ResponseEndBlock{
|
||||
Events: []abci.Event{
|
||||
{
|
||||
Type: "end_event",
|
||||
Attributes: []abci.EventAttribute{
|
||||
{
|
||||
Key: "foo",
|
||||
Value: fmt.Sprintf("%d", i),
|
||||
Index: index,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
q *query.Query
|
||||
results []int64
|
||||
}{
|
||||
"block.height = 100": {
|
||||
q: query.MustParse("block.height = 100"),
|
||||
results: []int64{},
|
||||
},
|
||||
"block.height = 5": {
|
||||
q: query.MustParse("block.height = 5"),
|
||||
results: []int64{5},
|
||||
},
|
||||
"begin_event.key1 = 'value1'": {
|
||||
q: query.MustParse("begin_event.key1 = 'value1'"),
|
||||
results: []int64{},
|
||||
},
|
||||
"begin_event.proposer = 'FCAA001'": {
|
||||
q: query.MustParse("begin_event.proposer = 'FCAA001'"),
|
||||
results: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11},
|
||||
},
|
||||
"end_event.foo <= 5": {
|
||||
q: query.MustParse("end_event.foo <= 5"),
|
||||
results: []int64{2, 4},
|
||||
},
|
||||
"end_event.foo >= 100": {
|
||||
q: query.MustParse("end_event.foo >= 100"),
|
||||
results: []int64{1},
|
||||
},
|
||||
"block.height > 2 AND end_event.foo <= 8": {
|
||||
q: query.MustParse("block.height > 2 AND end_event.foo <= 8"),
|
||||
results: []int64{4, 6, 8},
|
||||
},
|
||||
"begin_event.proposer CONTAINS 'FFFFFFF'": {
|
||||
q: query.MustParse("begin_event.proposer CONTAINS 'FFFFFFF'"),
|
||||
results: []int64{},
|
||||
},
|
||||
"begin_event.proposer CONTAINS 'FCAA001'": {
|
||||
q: query.MustParse("begin_event.proposer CONTAINS 'FCAA001'"),
|
||||
results: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
results, err := indexer.Search(context.Background(), tc.q)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.results, results)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package kv
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/orderedcode"
|
||||
"github.com/tendermint/tendermint/libs/pubsub/query"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
func intInSlice(a int, list []int) bool {
|
||||
for _, b := range list {
|
||||
if b == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func int64FromBytes(bz []byte) int64 {
|
||||
v, _ := binary.Varint(bz)
|
||||
return v
|
||||
}
|
||||
|
||||
func int64ToBytes(i int64) []byte {
|
||||
buf := make([]byte, binary.MaxVarintLen64)
|
||||
n := binary.PutVarint(buf, i)
|
||||
return buf[:n]
|
||||
}
|
||||
|
||||
func heightKey(height int64) ([]byte, error) {
|
||||
return orderedcode.Append(
|
||||
nil,
|
||||
types.BlockHeightKey,
|
||||
height,
|
||||
)
|
||||
}
|
||||
|
||||
func eventKey(compositeKey, typ, eventValue string, height int64) ([]byte, error) {
|
||||
return orderedcode.Append(
|
||||
nil,
|
||||
compositeKey,
|
||||
eventValue,
|
||||
height,
|
||||
typ,
|
||||
)
|
||||
}
|
||||
|
||||
func parseValueFromPrimaryKey(key []byte) (string, error) {
|
||||
var (
|
||||
compositeKey string
|
||||
height int64
|
||||
)
|
||||
|
||||
remaining, err := orderedcode.Parse(string(key), &compositeKey, &height)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse event key: %w", err)
|
||||
}
|
||||
|
||||
if len(remaining) != 0 {
|
||||
return "", fmt.Errorf("unexpected remainder in key: %s", remaining)
|
||||
}
|
||||
|
||||
return strconv.FormatInt(height, 10), nil
|
||||
}
|
||||
|
||||
func parseValueFromEventKey(key []byte) (string, error) {
|
||||
var (
|
||||
compositeKey, typ, eventValue string
|
||||
height int64
|
||||
)
|
||||
|
||||
remaining, err := orderedcode.Parse(string(key), &compositeKey, &eventValue, &height, &typ)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse event key: %w", err)
|
||||
}
|
||||
|
||||
if len(remaining) != 0 {
|
||||
return "", fmt.Errorf("unexpected remainder in key: %s", remaining)
|
||||
}
|
||||
|
||||
return eventValue, nil
|
||||
}
|
||||
|
||||
func lookForHeight(conditions []query.Condition) (int64, bool) {
|
||||
for _, c := range conditions {
|
||||
if c.CompositeKey == types.BlockHeightKey && c.Op == query.OpEqual {
|
||||
return c.Operand.(int64), true
|
||||
}
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package null
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/tendermint/tendermint/internal/state/indexer"
|
||||
"github.com/tendermint/tendermint/libs/pubsub/query"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
var _ indexer.BlockIndexer = (*BlockerIndexer)(nil)
|
||||
|
||||
// TxIndex implements a no-op block indexer.
|
||||
type BlockerIndexer struct{}
|
||||
|
||||
func (idx *BlockerIndexer) Has(height int64) (bool, error) {
|
||||
return false, errors.New(`indexing is disabled (set 'tx_index = "kv"' in config)`)
|
||||
}
|
||||
|
||||
func (idx *BlockerIndexer) Index(types.EventDataNewBlockHeader) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (idx *BlockerIndexer) Search(ctx context.Context, q *query.Query) ([]int64, error) {
|
||||
return []int64{}, nil
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
Package indexer defines Tendermint's block and transaction event indexing logic.
|
||||
|
||||
Tendermint supports two primary means of block and transaction event indexing:
|
||||
|
||||
1. A key-value sink via an embedded database with a proprietary query language.
|
||||
2. A Postgres-based sink.
|
||||
|
||||
An ABCI application can emit events during block and transaction execution in the form
|
||||
|
||||
<abci.Event.Type>.<abci.EventAttributeKey>=<abci.EventAttributeValue>
|
||||
|
||||
for example "transfer.amount=10000".
|
||||
|
||||
An operator can enable one or both of the supported indexing sinks via the
|
||||
'tx-index.indexer' Tendermint configuration.
|
||||
|
||||
Example:
|
||||
|
||||
[tx-index]
|
||||
indexer = ["kv", "psql"]
|
||||
|
||||
If an operator wants to completely disable indexing, they may simply just provide
|
||||
the "null" sink option in the configuration. All other sinks will be ignored if
|
||||
"null" is provided.
|
||||
|
||||
If indexing is enabled, the indexer.Service will iterate over all enabled sinks
|
||||
and invoke block and transaction indexing via the appropriate IndexBlockEvents
|
||||
and IndexTxEvents methods.
|
||||
|
||||
Note, the "kv" sink is considered deprecated and its query functionality is very
|
||||
limited, but does allow users to directly query for block and transaction events
|
||||
against Tendermint's RPC. Instead, operators are encouraged to use the "psql"
|
||||
indexing sink when more complex queries are required and for reliability purposes
|
||||
as PostgreSQL can scale.
|
||||
|
||||
Prior to starting Tendermint with the "psql" indexing sink enabled, operators
|
||||
must ensure the following:
|
||||
|
||||
1. The "psql" indexing sink is provided in Tendermint's configuration.
|
||||
2. A 'tx-index.psql-conn' value is provided that contains the PostgreSQL connection URI.
|
||||
3. The block and transaction event schemas have been created in the PostgreSQL database.
|
||||
|
||||
Tendermint provides the block and transaction event schemas in the following
|
||||
path: state/indexer/sink/psql/schema.sql
|
||||
|
||||
To create the schema in a PostgreSQL database, perform the schema query
|
||||
manually or invoke schema creation via the CLI:
|
||||
|
||||
$ psql <flags> -f state/indexer/sink/psql/schema.sql
|
||||
|
||||
The "psql" indexing sink prohibits queries via RPC. When using a PostgreSQL sink,
|
||||
queries can and should be made directly against the database using SQL.
|
||||
|
||||
The following are some example SQL queries against the database schema:
|
||||
|
||||
* Query for all transaction events for a given transaction hash:
|
||||
|
||||
SELECT * FROM tx_events WHERE hash = '3E7D1F...';
|
||||
|
||||
* Query for all transaction events for a given block height:
|
||||
|
||||
SELECT * FROM tx_events WHERE height = 25;
|
||||
|
||||
* Query for transaction events that have a given type (i.e. value wildcard):
|
||||
|
||||
SELECT * FROM tx_events WHERE key LIKE '%transfer.recipient%';
|
||||
|
||||
Note that if a complete abci.TxResult is needed, you will need to join "tx_events" with
|
||||
"tx_results" via a foreign key, to obtain contains the raw protobuf-encoded abci.TxResult.
|
||||
*/
|
||||
package indexer
|
||||
@@ -0,0 +1,56 @@
|
||||
package indexer
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/libs/pubsub/query"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
type EventSinkType string
|
||||
|
||||
const (
|
||||
NULL EventSinkType = "null"
|
||||
KV EventSinkType = "kv"
|
||||
PSQL EventSinkType = "psql"
|
||||
)
|
||||
|
||||
//go:generate ../../scripts/mockery_generate.sh EventSink
|
||||
|
||||
// EventSink interface is defined the APIs for the IndexerService to interact with the data store,
|
||||
// including the block/transaction indexing and the search functions.
|
||||
//
|
||||
// The IndexerService will accept a list of one or more EventSink types. During the OnStart method
|
||||
// it will call the appropriate APIs on each EventSink to index both block and transaction events.
|
||||
type EventSink interface {
|
||||
|
||||
// IndexBlockEvents indexes the blockheader.
|
||||
IndexBlockEvents(types.EventDataNewBlockHeader) error
|
||||
|
||||
// IndexTxEvents indexes the given result of transactions. To call it with multi transactions,
|
||||
// must guarantee the index of given transactions are in order.
|
||||
IndexTxEvents([]*abci.TxResult) error
|
||||
|
||||
// SearchBlockEvents provides the block search by given query conditions. This function only
|
||||
// supported by the kvEventSink.
|
||||
SearchBlockEvents(context.Context, *query.Query) ([]int64, error)
|
||||
|
||||
// SearchTxEvents provides the transaction search by given query conditions. This function only
|
||||
// supported by the kvEventSink.
|
||||
SearchTxEvents(context.Context, *query.Query) ([]*abci.TxResult, error)
|
||||
|
||||
// GetTxByHash provides the transaction search by given transaction hash. This function only
|
||||
// supported by the kvEventSink.
|
||||
GetTxByHash([]byte) (*abci.TxResult, error)
|
||||
|
||||
// HasBlock provides the transaction search by given transaction hash. This function only
|
||||
// supported by the kvEventSink.
|
||||
HasBlock(int64) (bool, error)
|
||||
|
||||
// Type checks the eventsink structure type.
|
||||
Type() EventSinkType
|
||||
|
||||
// Stop will close the data store connection, if the eventsink supports it.
|
||||
Stop() error
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package indexer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/libs/pubsub/query"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
// TxIndexer interface defines methods to index and search transactions.
|
||||
type TxIndexer interface {
|
||||
// Index analyzes, indexes and stores transactions. For indexing multiple
|
||||
// Transacions must guarantee the Index of the TxResult is in order.
|
||||
// See Batch struct.
|
||||
Index(results []*abci.TxResult) error
|
||||
|
||||
// Get returns the transaction specified by hash or nil if the transaction is not indexed
|
||||
// or stored.
|
||||
Get(hash []byte) (*abci.TxResult, error)
|
||||
|
||||
// Search allows you to query for transactions.
|
||||
Search(ctx context.Context, q *query.Query) ([]*abci.TxResult, error)
|
||||
}
|
||||
|
||||
// BlockIndexer defines an interface contract for indexing block events.
|
||||
type BlockIndexer interface {
|
||||
// Has returns true if the given height has been indexed. An error is returned
|
||||
// upon database query failure.
|
||||
Has(height int64) (bool, error)
|
||||
|
||||
// Index indexes BeginBlock and EndBlock events for a given block by its height.
|
||||
Index(types.EventDataNewBlockHeader) error
|
||||
|
||||
// Search performs a query for block heights that match a given BeginBlock
|
||||
// and Endblock event search criteria.
|
||||
Search(ctx context.Context, q *query.Query) ([]int64, error)
|
||||
}
|
||||
|
||||
// Batch groups together multiple Index operations to be performed at the same time.
|
||||
// NOTE: Batch is NOT thread-safe and must not be modified after starting its execution.
|
||||
type Batch struct {
|
||||
Ops []*abci.TxResult
|
||||
}
|
||||
|
||||
// NewBatch creates a new Batch.
|
||||
func NewBatch(n int64) *Batch {
|
||||
return &Batch{
|
||||
Ops: make([]*abci.TxResult, n),
|
||||
}
|
||||
}
|
||||
|
||||
// Add or update an entry for the given result.Index.
|
||||
func (b *Batch) Add(result *abci.TxResult) error {
|
||||
b.Ops[result.Index] = result
|
||||
return nil
|
||||
}
|
||||
|
||||
// Size returns the total number of operations inside the batch.
|
||||
func (b *Batch) Size() int {
|
||||
return len(b.Ops)
|
||||
}
|
||||
|
||||
// ErrorEmptyHash indicates empty hash
|
||||
var ErrorEmptyHash = errors.New("transaction hash cannot be empty")
|
||||
@@ -0,0 +1,137 @@
|
||||
package indexer
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/tendermint/tendermint/libs/service"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
// XXX/TODO: These types should be moved to the indexer package.
|
||||
|
||||
const (
|
||||
subscriber = "IndexerService"
|
||||
)
|
||||
|
||||
// Service connects event bus, transaction and block indexers together in
|
||||
// order to index transactions and blocks coming from the event bus.
|
||||
type Service struct {
|
||||
service.BaseService
|
||||
|
||||
eventSinks []EventSink
|
||||
eventBus *types.EventBus
|
||||
}
|
||||
|
||||
// NewIndexerService returns a new service instance.
|
||||
func NewIndexerService(es []EventSink, eventBus *types.EventBus) *Service {
|
||||
|
||||
is := &Service{eventSinks: es, eventBus: eventBus}
|
||||
is.BaseService = *service.NewBaseService(nil, "IndexerService", is)
|
||||
return is
|
||||
}
|
||||
|
||||
// OnStart implements service.Service by subscribing for all transactions
|
||||
// and indexing them by events.
|
||||
func (is *Service) OnStart() error {
|
||||
// Use SubscribeUnbuffered here to ensure both subscriptions does not get
|
||||
// canceled due to not pulling messages fast enough. Cause this might
|
||||
// sometimes happen when there are no other subscribers.
|
||||
blockHeadersSub, err := is.eventBus.SubscribeUnbuffered(
|
||||
context.Background(),
|
||||
subscriber,
|
||||
types.EventQueryNewBlockHeader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
txsSub, err := is.eventBus.SubscribeUnbuffered(context.Background(), subscriber, types.EventQueryTx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-blockHeadersSub.Canceled():
|
||||
return
|
||||
case msg := <-blockHeadersSub.Out():
|
||||
|
||||
eventDataHeader := msg.Data().(types.EventDataNewBlockHeader)
|
||||
height := eventDataHeader.Header.Height
|
||||
batch := NewBatch(eventDataHeader.NumTxs)
|
||||
|
||||
for i := int64(0); i < eventDataHeader.NumTxs; i++ {
|
||||
msg2 := <-txsSub.Out()
|
||||
txResult := msg2.Data().(types.EventDataTx).TxResult
|
||||
|
||||
if err = batch.Add(&txResult); err != nil {
|
||||
is.Logger.Error(
|
||||
"failed to add tx to batch",
|
||||
"height", height,
|
||||
"index", txResult.Index,
|
||||
"err", err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if !IndexingEnabled(is.eventSinks) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, sink := range is.eventSinks {
|
||||
if err := sink.IndexBlockEvents(eventDataHeader); err != nil {
|
||||
is.Logger.Error("failed to index block", "height", height, "err", err)
|
||||
} else {
|
||||
is.Logger.Debug("indexed block", "height", height, "sink", sink.Type())
|
||||
}
|
||||
|
||||
if len(batch.Ops) > 0 {
|
||||
err := sink.IndexTxEvents(batch.Ops)
|
||||
if err != nil {
|
||||
is.Logger.Error("failed to index block txs", "height", height, "err", err)
|
||||
} else {
|
||||
is.Logger.Debug("indexed txs", "height", height, "sink", sink.Type())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnStop implements service.Service by unsubscribing from all transactions and
|
||||
// close the eventsink.
|
||||
func (is *Service) OnStop() {
|
||||
if is.eventBus.IsRunning() {
|
||||
_ = is.eventBus.UnsubscribeAll(context.Background(), subscriber)
|
||||
}
|
||||
|
||||
for _, sink := range is.eventSinks {
|
||||
if err := sink.Stop(); err != nil {
|
||||
is.Logger.Error("failed to close eventsink", "eventsink", sink.Type(), "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// KVSinkEnabled returns the given eventSinks is containing KVEventSink.
|
||||
func KVSinkEnabled(sinks []EventSink) bool {
|
||||
for _, sink := range sinks {
|
||||
if sink.Type() == KV {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IndexingEnabled returns the given eventSinks is supporting the indexing services.
|
||||
func IndexingEnabled(sinks []EventSink) bool {
|
||||
for _, sink := range sinks {
|
||||
if sink.Type() == KV || sink.Type() == PSQL {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package indexer_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/adlio/schema"
|
||||
_ "github.com/lib/pq"
|
||||
dockertest "github.com/ory/dockertest"
|
||||
"github.com/ory/dockertest/docker"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
indexer "github.com/tendermint/tendermint/internal/state/indexer"
|
||||
kv "github.com/tendermint/tendermint/internal/state/indexer/sink/kv"
|
||||
psql "github.com/tendermint/tendermint/internal/state/indexer/sink/psql"
|
||||
tmlog "github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
db "github.com/tendermint/tm-db"
|
||||
)
|
||||
|
||||
var psqldb *sql.DB
|
||||
var resource *dockertest.Resource
|
||||
var pSink indexer.EventSink
|
||||
|
||||
var (
|
||||
user = "postgres"
|
||||
password = "secret"
|
||||
port = "5432"
|
||||
dsn = "postgres://%s:%s@localhost:%s/%s?sslmode=disable"
|
||||
dbName = "postgres"
|
||||
)
|
||||
|
||||
func TestIndexerServiceIndexesBlocks(t *testing.T) {
|
||||
// event bus
|
||||
eventBus := types.NewEventBus()
|
||||
eventBus.SetLogger(tmlog.TestingLogger())
|
||||
err := eventBus.Start()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
if err := eventBus.Stop(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
|
||||
assert.False(t, indexer.KVSinkEnabled([]indexer.EventSink{}))
|
||||
assert.False(t, indexer.IndexingEnabled([]indexer.EventSink{}))
|
||||
|
||||
// event sink setup
|
||||
pool, err := setupDB(t)
|
||||
assert.Nil(t, err)
|
||||
|
||||
store := db.NewMemDB()
|
||||
eventSinks := []indexer.EventSink{kv.NewEventSink(store), pSink}
|
||||
assert.True(t, indexer.KVSinkEnabled(eventSinks))
|
||||
assert.True(t, indexer.IndexingEnabled(eventSinks))
|
||||
|
||||
service := indexer.NewIndexerService(eventSinks, eventBus)
|
||||
service.SetLogger(tmlog.TestingLogger())
|
||||
err = service.Start()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
if err := service.Stop(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
|
||||
// publish block with txs
|
||||
err = eventBus.PublishEventNewBlockHeader(types.EventDataNewBlockHeader{
|
||||
Header: types.Header{Height: 1},
|
||||
NumTxs: int64(2),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
txResult1 := &abci.TxResult{
|
||||
Height: 1,
|
||||
Index: uint32(0),
|
||||
Tx: types.Tx("foo"),
|
||||
Result: abci.ResponseDeliverTx{Code: 0},
|
||||
}
|
||||
err = eventBus.PublishEventTx(types.EventDataTx{TxResult: *txResult1})
|
||||
require.NoError(t, err)
|
||||
txResult2 := &abci.TxResult{
|
||||
Height: 1,
|
||||
Index: uint32(1),
|
||||
Tx: types.Tx("bar"),
|
||||
Result: abci.ResponseDeliverTx{Code: 0},
|
||||
}
|
||||
err = eventBus.PublishEventTx(types.EventDataTx{TxResult: *txResult2})
|
||||
require.NoError(t, err)
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
res, err := eventSinks[0].GetTxByHash(types.Tx("foo").Hash())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, txResult1, res)
|
||||
|
||||
ok, err := eventSinks[0].HasBlock(1)
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
|
||||
res, err = eventSinks[0].GetTxByHash(types.Tx("bar").Hash())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, txResult2, res)
|
||||
|
||||
assert.Nil(t, teardown(t, pool))
|
||||
}
|
||||
|
||||
func readSchema() ([]*schema.Migration, error) {
|
||||
filename := "./sink/psql/schema.sql"
|
||||
contents, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read sql file from '%s': %w", filename, err)
|
||||
}
|
||||
|
||||
mg := &schema.Migration{}
|
||||
mg.ID = time.Now().Local().String() + " db schema"
|
||||
mg.Script = string(contents)
|
||||
return append([]*schema.Migration{}, mg), nil
|
||||
}
|
||||
|
||||
func resetDB(t *testing.T) {
|
||||
q := "DROP TABLE IF EXISTS block_events,tx_events,tx_results"
|
||||
_, err := psqldb.Exec(q)
|
||||
assert.Nil(t, err)
|
||||
|
||||
q = "DROP TYPE IF EXISTS block_event_type"
|
||||
_, err = psqldb.Exec(q)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func setupDB(t *testing.T) (*dockertest.Pool, error) {
|
||||
t.Helper()
|
||||
pool, err := dockertest.NewPool(os.Getenv("DOCKER_URL"))
|
||||
assert.Nil(t, err)
|
||||
|
||||
resource, err = pool.RunWithOptions(&dockertest.RunOptions{
|
||||
Repository: "postgres",
|
||||
Tag: "13",
|
||||
Env: []string{
|
||||
"POSTGRES_USER=" + user,
|
||||
"POSTGRES_PASSWORD=" + password,
|
||||
"POSTGRES_DB=" + dbName,
|
||||
"listen_addresses = '*'",
|
||||
},
|
||||
ExposedPorts: []string{port},
|
||||
}, func(config *docker.HostConfig) {
|
||||
// set AutoRemove to true so that stopped container goes away by itself
|
||||
config.AutoRemove = true
|
||||
config.RestartPolicy = docker.RestartPolicy{
|
||||
Name: "no",
|
||||
}
|
||||
})
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Set the container to expire in a minute to avoid orphaned containers
|
||||
// hanging around
|
||||
_ = resource.Expire(60)
|
||||
|
||||
conn := fmt.Sprintf(dsn, user, password, resource.GetPort(port+"/tcp"), dbName)
|
||||
|
||||
assert.NoError(t, pool.Retry(func() error {
|
||||
sink, err := psql.NewEventSink(conn, "test-chainID")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pSink = sink
|
||||
psqldb = sink.DB()
|
||||
return psqldb.Ping()
|
||||
}))
|
||||
|
||||
resetDB(t)
|
||||
|
||||
sm, err := readSchema()
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = schema.NewMigrator().Apply(psqldb, sm)
|
||||
assert.Nil(t, err)
|
||||
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
func teardown(t *testing.T, pool *dockertest.Pool) error {
|
||||
t.Helper()
|
||||
// When you're done, kill and remove the container
|
||||
assert.Nil(t, pool.Purge(resource))
|
||||
return psqldb.Close()
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// Code generated by mockery. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
indexer "github.com/tendermint/tendermint/internal/state/indexer"
|
||||
|
||||
query "github.com/tendermint/tendermint/libs/pubsub/query"
|
||||
|
||||
tenderminttypes "github.com/tendermint/tendermint/types"
|
||||
|
||||
types "github.com/tendermint/tendermint/abci/types"
|
||||
)
|
||||
|
||||
// EventSink is an autogenerated mock type for the EventSink type
|
||||
type EventSink struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// GetTxByHash provides a mock function with given fields: _a0
|
||||
func (_m *EventSink) GetTxByHash(_a0 []byte) (*types.TxResult, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 *types.TxResult
|
||||
if rf, ok := ret.Get(0).(func([]byte) *types.TxResult); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*types.TxResult)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func([]byte) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// HasBlock provides a mock function with given fields: _a0
|
||||
func (_m *EventSink) HasBlock(_a0 int64) (bool, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(int64) bool); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(int64) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// IndexBlockEvents provides a mock function with given fields: _a0
|
||||
func (_m *EventSink) IndexBlockEvents(_a0 tenderminttypes.EventDataNewBlockHeader) error {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(tenderminttypes.EventDataNewBlockHeader) error); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// IndexTxEvents provides a mock function with given fields: _a0
|
||||
func (_m *EventSink) IndexTxEvents(_a0 []*types.TxResult) error {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func([]*types.TxResult) error); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SearchBlockEvents provides a mock function with given fields: _a0, _a1
|
||||
func (_m *EventSink) SearchBlockEvents(_a0 context.Context, _a1 *query.Query) ([]int64, error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
var r0 []int64
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *query.Query) []int64); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]int64)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *query.Query) error); ok {
|
||||
r1 = rf(_a0, _a1)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SearchTxEvents provides a mock function with given fields: _a0, _a1
|
||||
func (_m *EventSink) SearchTxEvents(_a0 context.Context, _a1 *query.Query) ([]*types.TxResult, error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
var r0 []*types.TxResult
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *query.Query) []*types.TxResult); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*types.TxResult)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *query.Query) error); ok {
|
||||
r1 = rf(_a0, _a1)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Stop provides a mock function with given fields:
|
||||
func (_m *EventSink) Stop() error {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Type provides a mock function with given fields:
|
||||
func (_m *EventSink) Type() indexer.EventSinkType {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 indexer.EventSinkType
|
||||
if rf, ok := ret.Get(0).(func() indexer.EventSinkType); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(indexer.EventSinkType)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package indexer
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/tendermint/tendermint/libs/pubsub/query"
|
||||
)
|
||||
|
||||
// QueryRanges defines a mapping between a composite event key and a QueryRange.
|
||||
//
|
||||
// e.g.account.number => queryRange{lowerBound: 1, upperBound: 5}
|
||||
type QueryRanges map[string]QueryRange
|
||||
|
||||
// QueryRange defines a range within a query condition.
|
||||
type QueryRange struct {
|
||||
LowerBound interface{} // int || time.Time
|
||||
UpperBound interface{} // int || time.Time
|
||||
Key string
|
||||
IncludeLowerBound bool
|
||||
IncludeUpperBound bool
|
||||
}
|
||||
|
||||
// AnyBound returns either the lower bound if non-nil, otherwise the upper bound.
|
||||
func (qr QueryRange) AnyBound() interface{} {
|
||||
if qr.LowerBound != nil {
|
||||
return qr.LowerBound
|
||||
}
|
||||
|
||||
return qr.UpperBound
|
||||
}
|
||||
|
||||
// LowerBoundValue returns the value for the lower bound. If the lower bound is
|
||||
// nil, nil will be returned.
|
||||
func (qr QueryRange) LowerBoundValue() interface{} {
|
||||
if qr.LowerBound == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if qr.IncludeLowerBound {
|
||||
return qr.LowerBound
|
||||
}
|
||||
|
||||
switch t := qr.LowerBound.(type) {
|
||||
case int64:
|
||||
return t + 1
|
||||
|
||||
case time.Time:
|
||||
return t.Unix() + 1
|
||||
|
||||
default:
|
||||
panic("not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
// UpperBoundValue returns the value for the upper bound. If the upper bound is
|
||||
// nil, nil will be returned.
|
||||
func (qr QueryRange) UpperBoundValue() interface{} {
|
||||
if qr.UpperBound == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if qr.IncludeUpperBound {
|
||||
return qr.UpperBound
|
||||
}
|
||||
|
||||
switch t := qr.UpperBound.(type) {
|
||||
case int64:
|
||||
return t - 1
|
||||
|
||||
case time.Time:
|
||||
return t.Unix() - 1
|
||||
|
||||
default:
|
||||
panic("not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
// LookForRanges returns a mapping of QueryRanges and the matching indexes in
|
||||
// the provided query conditions.
|
||||
func LookForRanges(conditions []query.Condition) (ranges QueryRanges, indexes []int) {
|
||||
ranges = make(QueryRanges)
|
||||
for i, c := range conditions {
|
||||
if IsRangeOperation(c.Op) {
|
||||
r, ok := ranges[c.CompositeKey]
|
||||
if !ok {
|
||||
r = QueryRange{Key: c.CompositeKey}
|
||||
}
|
||||
|
||||
switch c.Op {
|
||||
case query.OpGreater:
|
||||
r.LowerBound = c.Operand
|
||||
|
||||
case query.OpGreaterEqual:
|
||||
r.IncludeLowerBound = true
|
||||
r.LowerBound = c.Operand
|
||||
|
||||
case query.OpLess:
|
||||
r.UpperBound = c.Operand
|
||||
|
||||
case query.OpLessEqual:
|
||||
r.IncludeUpperBound = true
|
||||
r.UpperBound = c.Operand
|
||||
}
|
||||
|
||||
ranges[c.CompositeKey] = r
|
||||
indexes = append(indexes, i)
|
||||
}
|
||||
}
|
||||
|
||||
return ranges, indexes
|
||||
}
|
||||
|
||||
// IsRangeOperation returns a boolean signifying if a query Operator is a range
|
||||
// operation or not.
|
||||
func IsRangeOperation(op query.Operator) bool {
|
||||
switch op {
|
||||
case query.OpGreater, query.OpGreaterEqual, query.OpLess, query.OpLessEqual:
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package kv
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/internal/state/indexer"
|
||||
kvb "github.com/tendermint/tendermint/internal/state/indexer/block/kv"
|
||||
kvt "github.com/tendermint/tendermint/internal/state/indexer/tx/kv"
|
||||
"github.com/tendermint/tendermint/libs/pubsub/query"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
)
|
||||
|
||||
var _ indexer.EventSink = (*EventSink)(nil)
|
||||
|
||||
// The EventSink is an aggregator for redirecting the call path of the tx/block kvIndexer.
|
||||
// For the implementation details please see the kv.go in the indexer/block and indexer/tx folder.
|
||||
type EventSink struct {
|
||||
txi *kvt.TxIndex
|
||||
bi *kvb.BlockerIndexer
|
||||
}
|
||||
|
||||
func NewEventSink(store dbm.DB) indexer.EventSink {
|
||||
return &EventSink{
|
||||
txi: kvt.NewTxIndex(store),
|
||||
bi: kvb.New(store),
|
||||
}
|
||||
}
|
||||
|
||||
func (kves *EventSink) Type() indexer.EventSinkType {
|
||||
return indexer.KV
|
||||
}
|
||||
|
||||
func (kves *EventSink) IndexBlockEvents(bh types.EventDataNewBlockHeader) error {
|
||||
return kves.bi.Index(bh)
|
||||
}
|
||||
|
||||
func (kves *EventSink) IndexTxEvents(results []*abci.TxResult) error {
|
||||
return kves.txi.Index(results)
|
||||
}
|
||||
|
||||
func (kves *EventSink) SearchBlockEvents(ctx context.Context, q *query.Query) ([]int64, error) {
|
||||
return kves.bi.Search(ctx, q)
|
||||
}
|
||||
|
||||
func (kves *EventSink) SearchTxEvents(ctx context.Context, q *query.Query) ([]*abci.TxResult, error) {
|
||||
return kves.txi.Search(ctx, q)
|
||||
}
|
||||
|
||||
func (kves *EventSink) GetTxByHash(hash []byte) (*abci.TxResult, error) {
|
||||
return kves.txi.Get(hash)
|
||||
}
|
||||
|
||||
func (kves *EventSink) HasBlock(h int64) (bool, error) {
|
||||
return kves.bi.Has(h)
|
||||
}
|
||||
|
||||
func (kves *EventSink) Stop() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
package kv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/internal/state/indexer"
|
||||
kvtx "github.com/tendermint/tendermint/internal/state/indexer/tx/kv"
|
||||
"github.com/tendermint/tendermint/libs/pubsub/query"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
db "github.com/tendermint/tm-db"
|
||||
)
|
||||
|
||||
func TestType(t *testing.T) {
|
||||
kvSink := NewEventSink(db.NewMemDB())
|
||||
assert.Equal(t, indexer.KV, kvSink.Type())
|
||||
}
|
||||
|
||||
func TestStop(t *testing.T) {
|
||||
kvSink := NewEventSink(db.NewMemDB())
|
||||
assert.Nil(t, kvSink.Stop())
|
||||
}
|
||||
|
||||
func TestBlockFuncs(t *testing.T) {
|
||||
store := db.NewPrefixDB(db.NewMemDB(), []byte("block_events"))
|
||||
indexer := NewEventSink(store)
|
||||
|
||||
require.NoError(t, indexer.IndexBlockEvents(types.EventDataNewBlockHeader{
|
||||
Header: types.Header{Height: 1},
|
||||
ResultBeginBlock: abci.ResponseBeginBlock{
|
||||
Events: []abci.Event{
|
||||
{
|
||||
Type: "begin_event",
|
||||
Attributes: []abci.EventAttribute{
|
||||
{
|
||||
Key: "proposer",
|
||||
Value: "FCAA001",
|
||||
Index: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ResultEndBlock: abci.ResponseEndBlock{
|
||||
Events: []abci.Event{
|
||||
{
|
||||
Type: "end_event",
|
||||
Attributes: []abci.EventAttribute{
|
||||
{
|
||||
Key: "foo",
|
||||
Value: "100",
|
||||
Index: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
b, e := indexer.HasBlock(1)
|
||||
assert.Nil(t, e)
|
||||
assert.True(t, b)
|
||||
|
||||
for i := 2; i < 12; i++ {
|
||||
var index bool
|
||||
if i%2 == 0 {
|
||||
index = true
|
||||
}
|
||||
|
||||
require.NoError(t, indexer.IndexBlockEvents(types.EventDataNewBlockHeader{
|
||||
Header: types.Header{Height: int64(i)},
|
||||
ResultBeginBlock: abci.ResponseBeginBlock{
|
||||
Events: []abci.Event{
|
||||
{
|
||||
Type: "begin_event",
|
||||
Attributes: []abci.EventAttribute{
|
||||
{
|
||||
Key: "proposer",
|
||||
Value: "FCAA001",
|
||||
Index: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ResultEndBlock: abci.ResponseEndBlock{
|
||||
Events: []abci.Event{
|
||||
{
|
||||
Type: "end_event",
|
||||
Attributes: []abci.EventAttribute{
|
||||
{
|
||||
Key: "foo",
|
||||
Value: fmt.Sprintf("%d", i),
|
||||
Index: index,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
q *query.Query
|
||||
results []int64
|
||||
}{
|
||||
"block.height = 100": {
|
||||
q: query.MustParse("block.height = 100"),
|
||||
results: []int64{},
|
||||
},
|
||||
"block.height = 5": {
|
||||
q: query.MustParse("block.height = 5"),
|
||||
results: []int64{5},
|
||||
},
|
||||
"begin_event.key1 = 'value1'": {
|
||||
q: query.MustParse("begin_event.key1 = 'value1'"),
|
||||
results: []int64{},
|
||||
},
|
||||
"begin_event.proposer = 'FCAA001'": {
|
||||
q: query.MustParse("begin_event.proposer = 'FCAA001'"),
|
||||
results: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11},
|
||||
},
|
||||
"end_event.foo <= 5": {
|
||||
q: query.MustParse("end_event.foo <= 5"),
|
||||
results: []int64{2, 4},
|
||||
},
|
||||
"end_event.foo >= 100": {
|
||||
q: query.MustParse("end_event.foo >= 100"),
|
||||
results: []int64{1},
|
||||
},
|
||||
"block.height > 2 AND end_event.foo <= 8": {
|
||||
q: query.MustParse("block.height > 2 AND end_event.foo <= 8"),
|
||||
results: []int64{4, 6, 8},
|
||||
},
|
||||
"begin_event.proposer CONTAINS 'FFFFFFF'": {
|
||||
q: query.MustParse("begin_event.proposer CONTAINS 'FFFFFFF'"),
|
||||
results: []int64{},
|
||||
},
|
||||
"begin_event.proposer CONTAINS 'FCAA001'": {
|
||||
q: query.MustParse("begin_event.proposer CONTAINS 'FCAA001'"),
|
||||
results: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
results, err := indexer.SearchBlockEvents(context.Background(), tc.q)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.results, results)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTxSearchWithCancelation(t *testing.T) {
|
||||
indexer := NewEventSink(db.NewMemDB())
|
||||
|
||||
txResult := txResultWithEvents([]abci.Event{
|
||||
{Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "1", Index: true}}},
|
||||
{Type: "account", Attributes: []abci.EventAttribute{{Key: "owner", Value: "Ivan", Index: true}}},
|
||||
{Type: "", Attributes: []abci.EventAttribute{{Key: "not_allowed", Value: "Vlad", Index: true}}},
|
||||
})
|
||||
err := indexer.IndexTxEvents([]*abci.TxResult{txResult})
|
||||
require.NoError(t, err)
|
||||
|
||||
r, e := indexer.GetTxByHash(types.Tx("HELLO WORLD").Hash())
|
||||
assert.Nil(t, e)
|
||||
assert.Equal(t, r, txResult)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
results, err := indexer.SearchTxEvents(ctx, query.MustParse("account.number = 1"))
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, results)
|
||||
}
|
||||
|
||||
func TestTxSearchDeprecatedIndexing(t *testing.T) {
|
||||
esdb := db.NewMemDB()
|
||||
indexer := NewEventSink(esdb)
|
||||
|
||||
// index tx using events indexing (composite key)
|
||||
txResult1 := txResultWithEvents([]abci.Event{
|
||||
{Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "1", Index: true}}},
|
||||
})
|
||||
hash1 := types.Tx(txResult1.Tx).Hash()
|
||||
|
||||
err := indexer.IndexTxEvents([]*abci.TxResult{txResult1})
|
||||
require.NoError(t, err)
|
||||
|
||||
// index tx also using deprecated indexing (event as key)
|
||||
txResult2 := txResultWithEvents(nil)
|
||||
txResult2.Tx = types.Tx("HELLO WORLD 2")
|
||||
|
||||
hash2 := types.Tx(txResult2.Tx).Hash()
|
||||
b := esdb.NewBatch()
|
||||
|
||||
rawBytes, err := proto.Marshal(txResult2)
|
||||
require.NoError(t, err)
|
||||
|
||||
depKey := []byte(fmt.Sprintf("%s/%s/%d/%d",
|
||||
"sender",
|
||||
"addr1",
|
||||
txResult2.Height,
|
||||
txResult2.Index,
|
||||
))
|
||||
|
||||
err = b.Set(depKey, hash2)
|
||||
require.NoError(t, err)
|
||||
err = b.Set(kvtx.KeyFromHeight(txResult2), hash2)
|
||||
require.NoError(t, err)
|
||||
err = b.Set(hash2, rawBytes)
|
||||
require.NoError(t, err)
|
||||
err = b.Write()
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
q string
|
||||
results []*abci.TxResult
|
||||
}{
|
||||
// search by hash
|
||||
{fmt.Sprintf("tx.hash = '%X'", hash1), []*abci.TxResult{txResult1}},
|
||||
// search by hash
|
||||
{fmt.Sprintf("tx.hash = '%X'", hash2), []*abci.TxResult{txResult2}},
|
||||
// search by exact match (one key)
|
||||
{"account.number = 1", []*abci.TxResult{txResult1}},
|
||||
{"account.number >= 1 AND account.number <= 5", []*abci.TxResult{txResult1}},
|
||||
// search by range (lower bound)
|
||||
{"account.number >= 1", []*abci.TxResult{txResult1}},
|
||||
// search by range (upper bound)
|
||||
{"account.number <= 5", []*abci.TxResult{txResult1}},
|
||||
// search using not allowed key
|
||||
{"not_allowed = 'boom'", []*abci.TxResult{}},
|
||||
// search for not existing tx result
|
||||
{"account.number >= 2 AND account.number <= 5", []*abci.TxResult{}},
|
||||
// search using not existing key
|
||||
{"account.date >= TIME 2013-05-03T14:45:00Z", []*abci.TxResult{}},
|
||||
// search by deprecated key
|
||||
{"sender = 'addr1'", []*abci.TxResult{txResult2}},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.q, func(t *testing.T) {
|
||||
results, err := indexer.SearchTxEvents(ctx, query.MustParse(tc.q))
|
||||
require.NoError(t, err)
|
||||
for _, txr := range results {
|
||||
for _, tr := range tc.results {
|
||||
assert.True(t, proto.Equal(tr, txr))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTxSearchOneTxWithMultipleSameTagsButDifferentValues(t *testing.T) {
|
||||
indexer := NewEventSink(db.NewMemDB())
|
||||
|
||||
txResult := txResultWithEvents([]abci.Event{
|
||||
{Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "1", Index: true}}},
|
||||
{Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "2", Index: true}}},
|
||||
})
|
||||
|
||||
err := indexer.IndexTxEvents([]*abci.TxResult{txResult})
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
results, err := indexer.SearchTxEvents(ctx, query.MustParse("account.number >= 1"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, results, 1)
|
||||
for _, txr := range results {
|
||||
assert.True(t, proto.Equal(txResult, txr))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTxSearchMultipleTxs(t *testing.T) {
|
||||
indexer := NewEventSink(db.NewMemDB())
|
||||
|
||||
// indexed first, but bigger height (to test the order of transactions)
|
||||
txResult := txResultWithEvents([]abci.Event{
|
||||
{Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "1", Index: true}}},
|
||||
})
|
||||
|
||||
txResult.Tx = types.Tx("Bob's account")
|
||||
txResult.Height = 2
|
||||
txResult.Index = 1
|
||||
err := indexer.IndexTxEvents([]*abci.TxResult{txResult})
|
||||
require.NoError(t, err)
|
||||
|
||||
// indexed second, but smaller height (to test the order of transactions)
|
||||
txResult2 := txResultWithEvents([]abci.Event{
|
||||
{Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "2", Index: true}}},
|
||||
})
|
||||
txResult2.Tx = types.Tx("Alice's account")
|
||||
txResult2.Height = 1
|
||||
txResult2.Index = 2
|
||||
|
||||
err = indexer.IndexTxEvents([]*abci.TxResult{txResult2})
|
||||
require.NoError(t, err)
|
||||
|
||||
// indexed third (to test the order of transactions)
|
||||
txResult3 := txResultWithEvents([]abci.Event{
|
||||
{Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "3", Index: true}}},
|
||||
})
|
||||
txResult3.Tx = types.Tx("Jack's account")
|
||||
txResult3.Height = 1
|
||||
txResult3.Index = 1
|
||||
err = indexer.IndexTxEvents([]*abci.TxResult{txResult3})
|
||||
require.NoError(t, err)
|
||||
|
||||
// indexed fourth (to test we don't include txs with similar events)
|
||||
// https://github.com/tendermint/tendermint/issues/2908
|
||||
txResult4 := txResultWithEvents([]abci.Event{
|
||||
{Type: "account", Attributes: []abci.EventAttribute{{Key: "number.id", Value: "1", Index: true}}},
|
||||
})
|
||||
txResult4.Tx = types.Tx("Mike's account")
|
||||
txResult4.Height = 2
|
||||
txResult4.Index = 2
|
||||
err = indexer.IndexTxEvents([]*abci.TxResult{txResult4})
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
results, err := indexer.SearchTxEvents(ctx, query.MustParse("account.number >= 1"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
require.Len(t, results, 3)
|
||||
}
|
||||
|
||||
func txResultWithEvents(events []abci.Event) *abci.TxResult {
|
||||
tx := types.Tx("HELLO WORLD")
|
||||
return &abci.TxResult{
|
||||
Height: 1,
|
||||
Index: 0,
|
||||
Tx: tx,
|
||||
Result: abci.ResponseDeliverTx{
|
||||
Data: []byte{0},
|
||||
Code: abci.CodeTypeOK,
|
||||
Log: "",
|
||||
Events: events,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package null
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/internal/state/indexer"
|
||||
"github.com/tendermint/tendermint/libs/pubsub/query"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
var _ indexer.EventSink = (*EventSink)(nil)
|
||||
|
||||
// EventSink implements a no-op indexer.
|
||||
type EventSink struct{}
|
||||
|
||||
func NewEventSink() indexer.EventSink {
|
||||
return &EventSink{}
|
||||
}
|
||||
|
||||
func (nes *EventSink) Type() indexer.EventSinkType {
|
||||
return indexer.NULL
|
||||
}
|
||||
|
||||
func (nes *EventSink) IndexBlockEvents(bh types.EventDataNewBlockHeader) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (nes *EventSink) IndexTxEvents(results []*abci.TxResult) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (nes *EventSink) SearchBlockEvents(ctx context.Context, q *query.Query) ([]int64, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (nes *EventSink) SearchTxEvents(ctx context.Context, q *query.Query) ([]*abci.TxResult, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (nes *EventSink) GetTxByHash(hash []byte) (*abci.TxResult, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (nes *EventSink) HasBlock(h int64) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (nes *EventSink) Stop() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package null
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tendermint/tendermint/internal/state/indexer"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
func TestNullEventSink(t *testing.T) {
|
||||
nullIndexer := NewEventSink()
|
||||
|
||||
assert.Nil(t, nullIndexer.IndexTxEvents(nil))
|
||||
assert.Nil(t, nullIndexer.IndexBlockEvents(types.EventDataNewBlockHeader{}))
|
||||
val1, err1 := nullIndexer.SearchBlockEvents(context.TODO(), nil)
|
||||
assert.Nil(t, val1)
|
||||
assert.Nil(t, err1)
|
||||
val2, err2 := nullIndexer.SearchTxEvents(context.TODO(), nil)
|
||||
assert.Nil(t, val2)
|
||||
assert.Nil(t, err2)
|
||||
val3, err3 := nullIndexer.GetTxByHash(nil)
|
||||
assert.Nil(t, val3)
|
||||
assert.Nil(t, err3)
|
||||
val4, err4 := nullIndexer.HasBlock(0)
|
||||
assert.False(t, val4)
|
||||
assert.Nil(t, err4)
|
||||
}
|
||||
|
||||
func TestType(t *testing.T) {
|
||||
nullIndexer := NewEventSink()
|
||||
assert.Equal(t, indexer.NULL, nullIndexer.Type())
|
||||
}
|
||||
|
||||
func TestStop(t *testing.T) {
|
||||
nullIndexer := NewEventSink()
|
||||
assert.Nil(t, nullIndexer.Stop())
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
// Package psql implements an event sink backed by a PostgreSQL database.
|
||||
package psql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/internal/state/indexer"
|
||||
"github.com/tendermint/tendermint/libs/pubsub/query"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
const (
|
||||
tableBlocks = "blocks"
|
||||
tableTxResults = "tx_results"
|
||||
tableEvents = "events"
|
||||
tableAttributes = "attributes"
|
||||
driverName = "postgres"
|
||||
)
|
||||
|
||||
// EventSink is an indexer backend providing the tx/block index services. This
|
||||
// implementation stores records in a PostgreSQL database using the schema
|
||||
// defined in state/indexer/sink/psql/schema.sql.
|
||||
type EventSink struct {
|
||||
store *sql.DB
|
||||
chainID string
|
||||
}
|
||||
|
||||
// NewEventSink constructs an event sink associated with the PostgreSQL
|
||||
// database specified by connStr. Events written to the sink are attributed to
|
||||
// the specified chainID.
|
||||
func NewEventSink(connStr, chainID string) (*EventSink, error) {
|
||||
db, err := sql.Open(driverName, connStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &EventSink{
|
||||
store: db,
|
||||
chainID: chainID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DB returns the underlying Postgres connection used by the sink.
|
||||
// This is exported to support testing.
|
||||
func (es *EventSink) DB() *sql.DB { return es.store }
|
||||
|
||||
// Type returns the structure type for this sink, which is Postgres.
|
||||
func (es *EventSink) Type() indexer.EventSinkType { return indexer.PSQL }
|
||||
|
||||
// runInTransaction executes query in a fresh database transaction.
|
||||
// If query reports an error, the transaction is rolled back and the
|
||||
// error from query is reported to the caller.
|
||||
// Otherwise, the result of committing the transaction is returned.
|
||||
func runInTransaction(db *sql.DB, query func(*sql.Tx) error) error {
|
||||
dbtx, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := query(dbtx); err != nil {
|
||||
_ = dbtx.Rollback() // report the initial error, not the rollback
|
||||
return err
|
||||
}
|
||||
return dbtx.Commit()
|
||||
}
|
||||
|
||||
// queryWithID executes the specified SQL query with the given arguments,
|
||||
// expecting a single-row, single-column result containing an ID. If the query
|
||||
// succeeds, the ID from the result is returned.
|
||||
func queryWithID(tx *sql.Tx, query string, args ...interface{}) (uint32, error) {
|
||||
var id uint32
|
||||
if err := tx.QueryRow(query, args...).Scan(&id); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// insertEvents inserts a slice of events and any indexed attributes of those
|
||||
// events into the database associated with dbtx.
|
||||
//
|
||||
// If txID > 0, the event is attributed to the Tendermint transaction with that
|
||||
// ID; otherwise it is recorded as a block event.
|
||||
func insertEvents(dbtx *sql.Tx, blockID, txID uint32, evts []abci.Event) error {
|
||||
// Populate the transaction ID field iff one is defined (> 0).
|
||||
var txIDArg interface{}
|
||||
if txID > 0 {
|
||||
txIDArg = txID
|
||||
}
|
||||
|
||||
// Add each event to the events table, and retrieve its row ID to use when
|
||||
// adding any attributes the event provides.
|
||||
for _, evt := range evts {
|
||||
// Skip events with an empty type.
|
||||
if evt.Type == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
eid, err := queryWithID(dbtx, `
|
||||
INSERT INTO `+tableEvents+` (block_id, tx_id, type) VALUES ($1, $2, $3)
|
||||
RETURNING rowid;
|
||||
`, blockID, txIDArg, evt.Type)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add any attributes flagged for indexing.
|
||||
for _, attr := range evt.Attributes {
|
||||
if !attr.Index {
|
||||
continue
|
||||
}
|
||||
compositeKey := evt.Type + "." + attr.Key
|
||||
if _, err := dbtx.Exec(`
|
||||
INSERT INTO `+tableAttributes+` (event_id, key, composite_key, value)
|
||||
VALUES ($1, $2, $3, $4);
|
||||
`, eid, attr.Key, compositeKey, attr.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// makeIndexedEvent constructs an event from the specified composite key and
|
||||
// value. If the key has the form "type.name", the event will have a single
|
||||
// attribute with that name and the value; otherwise the event will have only
|
||||
// a type and no attributes.
|
||||
func makeIndexedEvent(compositeKey, value string) abci.Event {
|
||||
i := strings.Index(compositeKey, ".")
|
||||
if i < 0 {
|
||||
return abci.Event{Type: compositeKey}
|
||||
}
|
||||
return abci.Event{Type: compositeKey[:i], Attributes: []abci.EventAttribute{
|
||||
{Key: compositeKey[i+1:], Value: value, Index: true},
|
||||
}}
|
||||
}
|
||||
|
||||
// IndexBlockEvents indexes the specified block header, part of the
|
||||
// indexer.EventSink interface.
|
||||
func (es *EventSink) IndexBlockEvents(h types.EventDataNewBlockHeader) error {
|
||||
ts := time.Now().UTC()
|
||||
|
||||
return runInTransaction(es.store, func(dbtx *sql.Tx) error {
|
||||
// Add the block to the blocks table and report back its row ID for use
|
||||
// in indexing the events for the block.
|
||||
blockID, err := queryWithID(dbtx, `
|
||||
INSERT INTO `+tableBlocks+` (height, chain_id, created_at)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT DO NOTHING
|
||||
RETURNING rowid;
|
||||
`, h.Header.Height, es.chainID, ts)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil // we already saw this block; quietly succeed
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("indexing block header: %w", err)
|
||||
}
|
||||
|
||||
// Insert the special block meta-event for height.
|
||||
if err := insertEvents(dbtx, blockID, 0, []abci.Event{
|
||||
makeIndexedEvent(types.BlockHeightKey, fmt.Sprint(h.Header.Height)),
|
||||
}); err != nil {
|
||||
return fmt.Errorf("block meta-events: %w", err)
|
||||
}
|
||||
// Insert all the block events. Order is important here,
|
||||
if err := insertEvents(dbtx, blockID, 0, h.ResultBeginBlock.Events); err != nil {
|
||||
return fmt.Errorf("begin-block events: %w", err)
|
||||
}
|
||||
if err := insertEvents(dbtx, blockID, 0, h.ResultEndBlock.Events); err != nil {
|
||||
return fmt.Errorf("end-block events: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (es *EventSink) IndexTxEvents(txrs []*abci.TxResult) error {
|
||||
ts := time.Now().UTC()
|
||||
|
||||
for _, txr := range txrs {
|
||||
// Encode the result message in protobuf wire format for indexing.
|
||||
resultData, err := proto.Marshal(txr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling tx_result: %w", err)
|
||||
}
|
||||
|
||||
// Index the hash of the underlying transaction as a hex string.
|
||||
txHash := fmt.Sprintf("%X", types.Tx(txr.Tx).Hash())
|
||||
|
||||
if err := runInTransaction(es.store, func(dbtx *sql.Tx) error {
|
||||
// Find the block associated with this transaction. The block header
|
||||
// must have been indexed prior to the transactions belonging to it.
|
||||
blockID, err := queryWithID(dbtx, `
|
||||
SELECT rowid FROM `+tableBlocks+` WHERE height = $1 AND chain_id = $2;
|
||||
`, txr.Height, es.chainID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding block ID: %w", err)
|
||||
}
|
||||
|
||||
// Insert a record for this tx_result and capture its ID for indexing events.
|
||||
txID, err := queryWithID(dbtx, `
|
||||
INSERT INTO `+tableTxResults+` (block_id, index, created_at, tx_hash, tx_result)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT DO NOTHING
|
||||
RETURNING rowid;
|
||||
`, blockID, txr.Index, ts, txHash, resultData)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil // we already saw this transaction; quietly succeed
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("indexing tx_result: %w", err)
|
||||
}
|
||||
|
||||
// Insert the special transaction meta-events for hash and height.
|
||||
if err := insertEvents(dbtx, blockID, txID, []abci.Event{
|
||||
makeIndexedEvent(types.TxHashKey, txHash),
|
||||
makeIndexedEvent(types.TxHeightKey, fmt.Sprint(txr.Height)),
|
||||
}); err != nil {
|
||||
return fmt.Errorf("indexing transaction meta-events: %w", err)
|
||||
}
|
||||
// Index any events packaged with the transaction.
|
||||
if err := insertEvents(dbtx, blockID, txID, txr.Result.Events); err != nil {
|
||||
return fmt.Errorf("indexing transaction events: %w", err)
|
||||
}
|
||||
return nil
|
||||
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SearchBlockEvents is not implemented by this sink, and reports an error for all queries.
|
||||
func (es *EventSink) SearchBlockEvents(ctx context.Context, q *query.Query) ([]int64, error) {
|
||||
return nil, errors.New("block search is not supported via the postgres event sink")
|
||||
}
|
||||
|
||||
// SearchTxEvents is not implemented by this sink, and reports an error for all queries.
|
||||
func (es *EventSink) SearchTxEvents(ctx context.Context, q *query.Query) ([]*abci.TxResult, error) {
|
||||
return nil, errors.New("tx search is not supported via the postgres event sink")
|
||||
}
|
||||
|
||||
// GetTxByHash is not implemented by this sink, and reports an error for all queries.
|
||||
func (es *EventSink) GetTxByHash(hash []byte) (*abci.TxResult, error) {
|
||||
return nil, errors.New("getTxByHash is not supported via the postgres event sink")
|
||||
}
|
||||
|
||||
// HasBlock is not implemented by this sink, and reports an error for all queries.
|
||||
func (es *EventSink) HasBlock(h int64) (bool, error) {
|
||||
return false, errors.New("hasBlock is not supported via the postgres event sink")
|
||||
}
|
||||
|
||||
// Stop closes the underlying PostgreSQL database.
|
||||
func (es *EventSink) Stop() error { return es.store.Close() }
|
||||
@@ -0,0 +1,344 @@
|
||||
package psql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/adlio/schema"
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/ory/dockertest"
|
||||
"github.com/ory/dockertest/docker"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/internal/state/indexer"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
|
||||
// Register the Postgres database driver.
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
// Verify that the type satisfies the EventSink interface.
|
||||
var _ indexer.EventSink = (*EventSink)(nil)
|
||||
|
||||
var (
|
||||
doPauseAtExit = flag.Bool("pause-at-exit", false,
|
||||
"If true, pause the test until interrupted at shutdown, to allow debugging")
|
||||
|
||||
// A hook that test cases can call to obtain the shared database instance
|
||||
// used for testing the sink. This is initialized in TestMain (see below).
|
||||
testDB func() *sql.DB
|
||||
)
|
||||
|
||||
const (
|
||||
user = "postgres"
|
||||
password = "secret"
|
||||
port = "5432"
|
||||
dsn = "postgres://%s:%s@localhost:%s/%s?sslmode=disable"
|
||||
dbName = "postgres"
|
||||
chainID = "test-chainID"
|
||||
|
||||
viewBlockEvents = "block_events"
|
||||
viewTxEvents = "tx_events"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
flag.Parse()
|
||||
|
||||
// Set up docker and start a container running PostgreSQL.
|
||||
pool, err := dockertest.NewPool(os.Getenv("DOCKER_URL"))
|
||||
if err != nil {
|
||||
log.Fatalf("Creating docker pool: %v", err)
|
||||
}
|
||||
|
||||
resource, err := pool.RunWithOptions(&dockertest.RunOptions{
|
||||
Repository: "postgres",
|
||||
Tag: "13",
|
||||
Env: []string{
|
||||
"POSTGRES_USER=" + user,
|
||||
"POSTGRES_PASSWORD=" + password,
|
||||
"POSTGRES_DB=" + dbName,
|
||||
"listen_addresses = '*'",
|
||||
},
|
||||
ExposedPorts: []string{port},
|
||||
}, func(config *docker.HostConfig) {
|
||||
// set AutoRemove to true so that stopped container goes away by itself
|
||||
config.AutoRemove = true
|
||||
config.RestartPolicy = docker.RestartPolicy{
|
||||
Name: "no",
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Starting docker pool: %v", err)
|
||||
}
|
||||
|
||||
if *doPauseAtExit {
|
||||
log.Print("Pause at exit is enabled, containers will not expire")
|
||||
} else {
|
||||
const expireSeconds = 60
|
||||
_ = resource.Expire(expireSeconds)
|
||||
log.Printf("Container expiration set to %d seconds", expireSeconds)
|
||||
}
|
||||
|
||||
// Connect to the database, clear any leftover data, and install the
|
||||
// indexing schema.
|
||||
conn := fmt.Sprintf(dsn, user, password, resource.GetPort(port+"/tcp"), dbName)
|
||||
var db *sql.DB
|
||||
|
||||
if err := pool.Retry(func() error {
|
||||
sink, err := NewEventSink(conn, chainID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db = sink.DB() // set global for test use
|
||||
return db.Ping()
|
||||
}); err != nil {
|
||||
log.Fatalf("Connecting to database: %v", err)
|
||||
}
|
||||
|
||||
if err := resetDatabase(db); err != nil {
|
||||
log.Fatalf("Flushing database: %v", err)
|
||||
}
|
||||
|
||||
sm, err := readSchema()
|
||||
if err != nil {
|
||||
log.Fatalf("Reading schema: %v", err)
|
||||
} else if err := schema.NewMigrator().Apply(db, sm); err != nil {
|
||||
log.Fatalf("Applying schema: %v", err)
|
||||
}
|
||||
|
||||
// Set up the hook for tests to get the shared database handle.
|
||||
testDB = func() *sql.DB { return db }
|
||||
|
||||
// Run the selected test cases.
|
||||
code := m.Run()
|
||||
|
||||
// Clean up and shut down the database container.
|
||||
if *doPauseAtExit {
|
||||
log.Print("Testing complete, pausing for inspection. Send SIGINT to resume teardown")
|
||||
waitForInterrupt()
|
||||
log.Print("(resuming)")
|
||||
}
|
||||
log.Print("Shutting down database")
|
||||
if err := pool.Purge(resource); err != nil {
|
||||
log.Printf("WARNING: Purging pool failed: %v", err)
|
||||
}
|
||||
if err := db.Close(); err != nil {
|
||||
log.Printf("WARNING: Closing database failed: %v", err)
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func TestType(t *testing.T) {
|
||||
psqlSink := &EventSink{store: testDB(), chainID: chainID}
|
||||
assert.Equal(t, indexer.PSQL, psqlSink.Type())
|
||||
}
|
||||
|
||||
func TestIndexing(t *testing.T) {
|
||||
t.Run("IndexBlockEvents", func(t *testing.T) {
|
||||
indexer := &EventSink{store: testDB(), chainID: chainID}
|
||||
require.NoError(t, indexer.IndexBlockEvents(newTestBlockHeader()))
|
||||
|
||||
verifyBlock(t, 1)
|
||||
verifyBlock(t, 2)
|
||||
|
||||
verifyNotImplemented(t, "hasBlock", func() (bool, error) { return indexer.HasBlock(1) })
|
||||
verifyNotImplemented(t, "hasBlock", func() (bool, error) { return indexer.HasBlock(2) })
|
||||
|
||||
verifyNotImplemented(t, "block search", func() (bool, error) {
|
||||
v, err := indexer.SearchBlockEvents(context.Background(), nil)
|
||||
return v != nil, err
|
||||
})
|
||||
|
||||
require.NoError(t, verifyTimeStamp(tableBlocks))
|
||||
|
||||
// Attempting to reindex the same events should gracefully succeed.
|
||||
require.NoError(t, indexer.IndexBlockEvents(newTestBlockHeader()))
|
||||
})
|
||||
|
||||
t.Run("IndexTxEvents", func(t *testing.T) {
|
||||
indexer := &EventSink{store: testDB(), chainID: chainID}
|
||||
|
||||
txResult := txResultWithEvents([]abci.Event{
|
||||
makeIndexedEvent("account.number", "1"),
|
||||
makeIndexedEvent("account.owner", "Ivan"),
|
||||
makeIndexedEvent("account.owner", "Yulieta"),
|
||||
|
||||
{Type: "", Attributes: []abci.EventAttribute{{Key: "not_allowed", Value: "Vlad", Index: true}}},
|
||||
})
|
||||
require.NoError(t, indexer.IndexTxEvents([]*abci.TxResult{txResult}))
|
||||
|
||||
txr, err := loadTxResult(types.Tx(txResult.Tx).Hash())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, txResult, txr)
|
||||
|
||||
require.NoError(t, verifyTimeStamp(tableTxResults))
|
||||
require.NoError(t, verifyTimeStamp(viewTxEvents))
|
||||
|
||||
verifyNotImplemented(t, "getTxByHash", func() (bool, error) {
|
||||
txr, err := indexer.GetTxByHash(types.Tx(txResult.Tx).Hash())
|
||||
return txr != nil, err
|
||||
})
|
||||
verifyNotImplemented(t, "tx search", func() (bool, error) {
|
||||
txr, err := indexer.SearchTxEvents(context.Background(), nil)
|
||||
return txr != nil, err
|
||||
})
|
||||
|
||||
// try to insert the duplicate tx events.
|
||||
err = indexer.IndexTxEvents([]*abci.TxResult{txResult})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStop(t *testing.T) {
|
||||
indexer := &EventSink{store: testDB()}
|
||||
require.NoError(t, indexer.Stop())
|
||||
}
|
||||
|
||||
// newTestBlockHeader constructs a fresh copy of a block header containing
|
||||
// known test values to exercise the indexer.
|
||||
func newTestBlockHeader() types.EventDataNewBlockHeader {
|
||||
return types.EventDataNewBlockHeader{
|
||||
Header: types.Header{Height: 1},
|
||||
ResultBeginBlock: abci.ResponseBeginBlock{
|
||||
Events: []abci.Event{
|
||||
makeIndexedEvent("begin_event.proposer", "FCAA001"),
|
||||
makeIndexedEvent("thingy.whatzit", "O.O"),
|
||||
},
|
||||
},
|
||||
ResultEndBlock: abci.ResponseEndBlock{
|
||||
Events: []abci.Event{
|
||||
makeIndexedEvent("end_event.foo", "100"),
|
||||
makeIndexedEvent("thingy.whatzit", "-.O"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// readSchema loads the indexing database schema file
|
||||
func readSchema() ([]*schema.Migration, error) {
|
||||
const filename = "schema.sql"
|
||||
contents, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read sql file from '%s': %w", filename, err)
|
||||
}
|
||||
|
||||
return []*schema.Migration{{
|
||||
ID: time.Now().Local().String() + " db schema",
|
||||
Script: string(contents),
|
||||
}}, nil
|
||||
}
|
||||
|
||||
// resetDB drops all the data from the test database.
|
||||
func resetDatabase(db *sql.DB) error {
|
||||
_, err := db.Exec(`DROP TABLE IF EXISTS blocks,tx_results,events,attributes CASCADE;`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dropping tables: %v", err)
|
||||
}
|
||||
_, err = db.Exec(`DROP VIEW IF EXISTS event_attributes,block_events,tx_events CASCADE;`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dropping views: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// txResultWithEvents constructs a fresh transaction result with fixed values
|
||||
// for testing, that includes the specified events.
|
||||
func txResultWithEvents(events []abci.Event) *abci.TxResult {
|
||||
return &abci.TxResult{
|
||||
Height: 1,
|
||||
Index: 0,
|
||||
Tx: types.Tx("HELLO WORLD"),
|
||||
Result: abci.ResponseDeliverTx{
|
||||
Data: []byte{0},
|
||||
Code: abci.CodeTypeOK,
|
||||
Log: "",
|
||||
Events: events,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func loadTxResult(hash []byte) (*abci.TxResult, error) {
|
||||
hashString := fmt.Sprintf("%X", hash)
|
||||
var resultData []byte
|
||||
if err := testDB().QueryRow(`
|
||||
SELECT tx_result FROM `+tableTxResults+` WHERE tx_hash = $1;
|
||||
`, hashString).Scan(&resultData); err != nil {
|
||||
return nil, fmt.Errorf("lookup transaction for hash %q failed: %v", hashString, err)
|
||||
}
|
||||
|
||||
txr := new(abci.TxResult)
|
||||
if err := proto.Unmarshal(resultData, txr); err != nil {
|
||||
return nil, fmt.Errorf("unmarshaling txr: %v", err)
|
||||
}
|
||||
|
||||
return txr, nil
|
||||
}
|
||||
|
||||
func verifyTimeStamp(tableName string) error {
|
||||
return testDB().QueryRow(fmt.Sprintf(`
|
||||
SELECT DISTINCT %[1]s.created_at
|
||||
FROM %[1]s
|
||||
WHERE %[1]s.created_at >= $1;
|
||||
`, tableName), time.Now().Add(-2*time.Second)).Err()
|
||||
}
|
||||
|
||||
func verifyBlock(t *testing.T, height int64) {
|
||||
// Check that the blocks table contains an entry for this height.
|
||||
if err := testDB().QueryRow(`
|
||||
SELECT height FROM `+tableBlocks+` WHERE height = $1;
|
||||
`, height).Err(); err == sql.ErrNoRows {
|
||||
t.Errorf("No block found for height=%d", height)
|
||||
} else if err != nil {
|
||||
t.Fatalf("Database query failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify the presence of begin_block and end_block events.
|
||||
if err := testDB().QueryRow(`
|
||||
SELECT type, height, chain_id FROM `+viewBlockEvents+`
|
||||
WHERE height = $1 AND type = $2 AND chain_id = $3;
|
||||
`, height, types.EventTypeBeginBlock, chainID).Err(); err == sql.ErrNoRows {
|
||||
t.Errorf("No %q event found for height=%d", types.EventTypeBeginBlock, height)
|
||||
} else if err != nil {
|
||||
t.Fatalf("Database query failed: %v", err)
|
||||
}
|
||||
|
||||
if err := testDB().QueryRow(`
|
||||
SELECT type, height, chain_id FROM `+viewBlockEvents+`
|
||||
WHERE height = $1 AND type = $2 AND chain_id = $3;
|
||||
`, height, types.EventTypeEndBlock, chainID).Err(); err == sql.ErrNoRows {
|
||||
t.Errorf("No %q event found for height=%d", types.EventTypeEndBlock, height)
|
||||
} else if err != nil {
|
||||
t.Fatalf("Database query failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// verifyNotImplemented calls f and verifies that it returns both a
|
||||
// false-valued flag and a non-nil error whose string matching the expected
|
||||
// "not supported" message with label prefixed.
|
||||
func verifyNotImplemented(t *testing.T, label string, f func() (bool, error)) {
|
||||
t.Helper()
|
||||
t.Logf("Verifying that %q reports it is not implemented", label)
|
||||
|
||||
want := label + " is not supported via the postgres event sink"
|
||||
ok, err := f()
|
||||
assert.False(t, ok)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, want, err.Error())
|
||||
}
|
||||
|
||||
// waitForInterrupt blocks until a SIGINT is received by the process.
|
||||
func waitForInterrupt() {
|
||||
ch := make(chan os.Signal, 1)
|
||||
signal.Notify(ch, os.Interrupt)
|
||||
<-ch
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
This file defines the database schema for the PostgresQL ("psql") event sink
|
||||
implementation in Tendermint. The operator must create a database and install
|
||||
this schema before using the database to index events.
|
||||
*/
|
||||
|
||||
-- The blocks table records metadata about each block.
|
||||
-- The block record does not include its events or transactions (see tx_results).
|
||||
CREATE TABLE blocks (
|
||||
rowid BIGSERIAL PRIMARY KEY,
|
||||
|
||||
height BIGINT NOT NULL,
|
||||
chain_id VARCHAR NOT NULL,
|
||||
|
||||
-- When this block header was logged into the sink, in UTC.
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
|
||||
UNIQUE (height, chain_id)
|
||||
);
|
||||
|
||||
-- Index blocks by height and chain, since we need to resolve block IDs when
|
||||
-- indexing transaction records and transaction events.
|
||||
CREATE INDEX idx_blocks_height_chain ON blocks(height, chain_id);
|
||||
|
||||
-- The tx_results table records metadata about transaction results. Note that
|
||||
-- the events from a transaction are stored separately.
|
||||
CREATE TABLE tx_results (
|
||||
rowid BIGSERIAL PRIMARY KEY,
|
||||
|
||||
-- The block to which this transaction belongs.
|
||||
block_id BIGINT NOT NULL REFERENCES blocks(rowid),
|
||||
-- The sequential index of the transaction within the block.
|
||||
index INTEGER NOT NULL,
|
||||
-- When this result record was logged into the sink, in UTC.
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
-- The hex-encoded hash of the transaction.
|
||||
tx_hash VARCHAR NOT NULL,
|
||||
-- The protobuf wire encoding of the TxResult message.
|
||||
tx_result BYTEA NOT NULL,
|
||||
|
||||
UNIQUE (block_id, index)
|
||||
);
|
||||
|
||||
-- The events table records events. All events (both block and transaction) are
|
||||
-- associated with a block ID; transaction events also have a transaction ID.
|
||||
CREATE TABLE events (
|
||||
rowid BIGSERIAL PRIMARY KEY,
|
||||
|
||||
-- The block and transaction this event belongs to.
|
||||
-- If tx_id is NULL, this is a block event.
|
||||
block_id BIGINT NOT NULL REFERENCES blocks(rowid),
|
||||
tx_id BIGINT NULL REFERENCES tx_results(rowid),
|
||||
|
||||
-- The application-defined type label for the event.
|
||||
type VARCHAR NOT NULL
|
||||
);
|
||||
|
||||
-- The attributes table records event attributes.
|
||||
CREATE TABLE attributes (
|
||||
event_id BIGINT NOT NULL REFERENCES events(rowid),
|
||||
key VARCHAR NOT NULL, -- bare key
|
||||
composite_key VARCHAR NOT NULL, -- composed type.key
|
||||
value VARCHAR NULL,
|
||||
|
||||
UNIQUE (event_id, key)
|
||||
);
|
||||
|
||||
-- A joined view of events and their attributes. Events that do not have any
|
||||
-- attributes are represented as a single row with empty key and value fields.
|
||||
CREATE VIEW event_attributes AS
|
||||
SELECT block_id, tx_id, type, key, composite_key, value
|
||||
FROM events LEFT JOIN attributes ON (events.rowid = attributes.event_id);
|
||||
|
||||
-- A joined view of all block events (those having tx_id NULL).
|
||||
CREATE VIEW block_events AS
|
||||
SELECT blocks.rowid as block_id, height, chain_id, type, key, composite_key, value
|
||||
FROM blocks JOIN event_attributes ON (blocks.rowid = event_attributes.block_id)
|
||||
WHERE event_attributes.tx_id IS NULL;
|
||||
|
||||
-- A joined view of all transaction events.
|
||||
CREATE VIEW tx_events AS
|
||||
SELECT height, index, chain_id, type, key, composite_key, value, tx_results.created_at
|
||||
FROM blocks JOIN tx_results ON (blocks.rowid = tx_results.block_id)
|
||||
JOIN event_attributes ON (tx_results.rowid = event_attributes.tx_id)
|
||||
WHERE event_attributes.tx_id IS NOT NULL;
|
||||
@@ -0,0 +1,65 @@
|
||||
package sink
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/tendermint/tendermint/config"
|
||||
"github.com/tendermint/tendermint/internal/state/indexer"
|
||||
"github.com/tendermint/tendermint/internal/state/indexer/sink/kv"
|
||||
"github.com/tendermint/tendermint/internal/state/indexer/sink/null"
|
||||
"github.com/tendermint/tendermint/internal/state/indexer/sink/psql"
|
||||
)
|
||||
|
||||
// EventSinksFromConfig constructs a slice of indexer.EventSink using the provided
|
||||
// configuration.
|
||||
//
|
||||
//nolint:lll
|
||||
func EventSinksFromConfig(cfg *config.Config, dbProvider config.DBProvider, chainID string) ([]indexer.EventSink, error) {
|
||||
if len(cfg.TxIndex.Indexer) == 0 {
|
||||
return []indexer.EventSink{null.NewEventSink()}, nil
|
||||
}
|
||||
|
||||
// check for duplicated sinks
|
||||
sinks := map[string]struct{}{}
|
||||
for _, s := range cfg.TxIndex.Indexer {
|
||||
sl := strings.ToLower(s)
|
||||
if _, ok := sinks[sl]; ok {
|
||||
return nil, errors.New("found duplicated sinks, please check the tx-index section in the config.toml")
|
||||
}
|
||||
sinks[sl] = struct{}{}
|
||||
}
|
||||
eventSinks := []indexer.EventSink{}
|
||||
for k := range sinks {
|
||||
switch indexer.EventSinkType(k) {
|
||||
case indexer.NULL:
|
||||
// When we see null in the config, the eventsinks will be reset with the
|
||||
// nullEventSink.
|
||||
return []indexer.EventSink{null.NewEventSink()}, nil
|
||||
|
||||
case indexer.KV:
|
||||
store, err := dbProvider(&config.DBContext{ID: "tx_index", Config: cfg})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eventSinks = append(eventSinks, kv.NewEventSink(store))
|
||||
|
||||
case indexer.PSQL:
|
||||
conn := cfg.TxIndex.PsqlConn
|
||||
if conn == "" {
|
||||
return nil, errors.New("the psql connection settings cannot be empty")
|
||||
}
|
||||
|
||||
es, err := psql.NewEventSink(conn, chainID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
eventSinks = append(eventSinks, es)
|
||||
default:
|
||||
return nil, errors.New("unsupported event sink type")
|
||||
}
|
||||
}
|
||||
return eventSinks, nil
|
||||
|
||||
}
|
||||
@@ -0,0 +1,590 @@
|
||||
package kv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/google/orderedcode"
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
indexer "github.com/tendermint/tendermint/internal/state/indexer"
|
||||
"github.com/tendermint/tendermint/libs/pubsub/query"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
var _ indexer.TxIndexer = (*TxIndex)(nil)
|
||||
|
||||
// TxIndex is the simplest possible indexer
|
||||
// It is backed by two kv stores:
|
||||
// 1. txhash - result (primary key)
|
||||
// 2. event - txhash (secondary key)
|
||||
type TxIndex struct {
|
||||
store dbm.DB
|
||||
}
|
||||
|
||||
// NewTxIndex creates new KV indexer.
|
||||
func NewTxIndex(store dbm.DB) *TxIndex {
|
||||
return &TxIndex{
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
// Get gets transaction from the TxIndex storage and returns it or nil if the
|
||||
// transaction is not found.
|
||||
func (txi *TxIndex) Get(hash []byte) (*abci.TxResult, error) {
|
||||
if len(hash) == 0 {
|
||||
return nil, indexer.ErrorEmptyHash
|
||||
}
|
||||
|
||||
rawBytes, err := txi.store.Get(primaryKey(hash))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if rawBytes == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
txResult := new(abci.TxResult)
|
||||
err = proto.Unmarshal(rawBytes, txResult)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading TxResult: %v", err)
|
||||
}
|
||||
|
||||
return txResult, nil
|
||||
}
|
||||
|
||||
// Index indexes transactions using the given list of events. Each key
|
||||
// that indexed from the tx's events is a composite of the event type and the
|
||||
// respective attribute's key delimited by a "." (eg. "account.number").
|
||||
// Any event with an empty type is not indexed.
|
||||
func (txi *TxIndex) Index(results []*abci.TxResult) error {
|
||||
b := txi.store.NewBatch()
|
||||
defer b.Close()
|
||||
|
||||
for _, result := range results {
|
||||
hash := types.Tx(result.Tx).Hash()
|
||||
|
||||
// index tx by events
|
||||
err := txi.indexEvents(result, hash, b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// index by height (always)
|
||||
err = b.Set(KeyFromHeight(result), hash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rawBytes, err := proto.Marshal(result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// index by hash (always)
|
||||
err = b.Set(primaryKey(hash), rawBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return b.WriteSync()
|
||||
}
|
||||
|
||||
func (txi *TxIndex) indexEvents(result *abci.TxResult, hash []byte, store dbm.Batch) error {
|
||||
for _, event := range result.Result.Events {
|
||||
// only index events with a non-empty type
|
||||
if len(event.Type) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, attr := range event.Attributes {
|
||||
if len(attr.Key) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// index if `index: true` is set
|
||||
compositeTag := fmt.Sprintf("%s.%s", event.Type, attr.Key)
|
||||
// ensure event does not conflict with a reserved prefix key
|
||||
if compositeTag == types.TxHashKey || compositeTag == types.TxHeightKey {
|
||||
return fmt.Errorf("event type and attribute key \"%s\" is reserved; please use a different key", compositeTag)
|
||||
}
|
||||
if attr.GetIndex() {
|
||||
err := store.Set(keyFromEvent(compositeTag, attr.Value, result), hash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Search performs a search using the given query.
|
||||
//
|
||||
// It breaks the query into conditions (like "tx.height > 5"). For each
|
||||
// condition, it queries the DB index. One special use cases here: (1) if
|
||||
// "tx.hash" is found, it returns tx result for it (2) for range queries it is
|
||||
// better for the client to provide both lower and upper bounds, so we are not
|
||||
// performing a full scan. Results from querying indexes are then intersected
|
||||
// and returned to the caller, in no particular order.
|
||||
//
|
||||
// Search will exit early and return any result fetched so far,
|
||||
// when a message is received on the context chan.
|
||||
func (txi *TxIndex) Search(ctx context.Context, q *query.Query) ([]*abci.TxResult, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return make([]*abci.TxResult, 0), nil
|
||||
|
||||
default:
|
||||
}
|
||||
|
||||
var hashesInitialized bool
|
||||
filteredHashes := make(map[string][]byte)
|
||||
|
||||
// get a list of conditions (like "tx.height > 5")
|
||||
conditions, err := q.Conditions()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error during parsing conditions from query: %w", err)
|
||||
}
|
||||
|
||||
// if there is a hash condition, return the result immediately
|
||||
hash, ok, err := lookForHash(conditions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error during searching for a hash in the query: %w", err)
|
||||
} else if ok {
|
||||
res, err := txi.Get(hash)
|
||||
switch {
|
||||
case err != nil:
|
||||
return []*abci.TxResult{}, fmt.Errorf("error while retrieving the result: %w", err)
|
||||
case res == nil:
|
||||
return []*abci.TxResult{}, nil
|
||||
default:
|
||||
return []*abci.TxResult{res}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// conditions to skip because they're handled before "everything else"
|
||||
skipIndexes := make([]int, 0)
|
||||
|
||||
// extract ranges
|
||||
// if both upper and lower bounds exist, it's better to get them in order not
|
||||
// no iterate over kvs that are not within range.
|
||||
ranges, rangeIndexes := indexer.LookForRanges(conditions)
|
||||
if len(ranges) > 0 {
|
||||
skipIndexes = append(skipIndexes, rangeIndexes...)
|
||||
|
||||
for _, qr := range ranges {
|
||||
if !hashesInitialized {
|
||||
filteredHashes = txi.matchRange(ctx, qr, prefixFromCompositeKey(qr.Key), filteredHashes, true)
|
||||
hashesInitialized = true
|
||||
|
||||
// Ignore any remaining conditions if the first condition resulted
|
||||
// in no matches (assuming implicit AND operand).
|
||||
if len(filteredHashes) == 0 {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
filteredHashes = txi.matchRange(ctx, qr, prefixFromCompositeKey(qr.Key), filteredHashes, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if there is a height condition ("tx.height=3"), extract it
|
||||
height := lookForHeight(conditions)
|
||||
|
||||
// for all other conditions
|
||||
for i, c := range conditions {
|
||||
if intInSlice(i, skipIndexes) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !hashesInitialized {
|
||||
filteredHashes = txi.match(ctx, c, prefixForCondition(c, height), filteredHashes, true)
|
||||
hashesInitialized = true
|
||||
|
||||
// Ignore any remaining conditions if the first condition resulted
|
||||
// in no matches (assuming implicit AND operand).
|
||||
if len(filteredHashes) == 0 {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
filteredHashes = txi.match(ctx, c, prefixForCondition(c, height), filteredHashes, false)
|
||||
}
|
||||
}
|
||||
|
||||
results := make([]*abci.TxResult, 0, len(filteredHashes))
|
||||
hashes:
|
||||
for _, h := range filteredHashes {
|
||||
res, err := txi.Get(h)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get Tx{%X}: %w", h, err)
|
||||
}
|
||||
results = append(results, res)
|
||||
|
||||
// Potentially exit early.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break hashes
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func lookForHash(conditions []query.Condition) (hash []byte, ok bool, err error) {
|
||||
for _, c := range conditions {
|
||||
if c.CompositeKey == types.TxHashKey {
|
||||
decoded, err := hex.DecodeString(c.Operand.(string))
|
||||
return decoded, true, err
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// lookForHeight returns a height if there is an "height=X" condition.
|
||||
func lookForHeight(conditions []query.Condition) (height int64) {
|
||||
for _, c := range conditions {
|
||||
if c.CompositeKey == types.TxHeightKey && c.Op == query.OpEqual {
|
||||
return c.Operand.(int64)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// match returns all matching txs by hash that meet a given condition and start
|
||||
// key. An already filtered result (filteredHashes) is provided such that any
|
||||
// non-intersecting matches are removed.
|
||||
//
|
||||
// NOTE: filteredHashes may be empty if no previous condition has matched.
|
||||
func (txi *TxIndex) match(
|
||||
ctx context.Context,
|
||||
c query.Condition,
|
||||
startKeyBz []byte,
|
||||
filteredHashes map[string][]byte,
|
||||
firstRun bool,
|
||||
) map[string][]byte {
|
||||
// A previous match was attempted but resulted in no matches, so we return
|
||||
// no matches (assuming AND operand).
|
||||
if !firstRun && len(filteredHashes) == 0 {
|
||||
return filteredHashes
|
||||
}
|
||||
|
||||
tmpHashes := make(map[string][]byte)
|
||||
|
||||
switch {
|
||||
case c.Op == query.OpEqual:
|
||||
it, err := dbm.IteratePrefix(txi.store, startKeyBz)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer it.Close()
|
||||
|
||||
iterEqual:
|
||||
for ; it.Valid(); it.Next() {
|
||||
tmpHashes[string(it.Value())] = it.Value()
|
||||
|
||||
// Potentially exit early.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break iterEqual
|
||||
default:
|
||||
}
|
||||
}
|
||||
if err := it.Error(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
case c.Op == query.OpExists:
|
||||
// XXX: can't use startKeyBz here because c.Operand is nil
|
||||
// (e.g. "account.owner/<nil>/" won't match w/ a single row)
|
||||
it, err := dbm.IteratePrefix(txi.store, prefixFromCompositeKey(c.CompositeKey))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer it.Close()
|
||||
|
||||
iterExists:
|
||||
for ; it.Valid(); it.Next() {
|
||||
tmpHashes[string(it.Value())] = it.Value()
|
||||
|
||||
// Potentially exit early.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break iterExists
|
||||
default:
|
||||
}
|
||||
}
|
||||
if err := it.Error(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
case c.Op == query.OpContains:
|
||||
// XXX: startKey does not apply here.
|
||||
// For example, if startKey = "account.owner/an/" and search query = "account.owner CONTAINS an"
|
||||
// we can't iterate with prefix "account.owner/an/" because we might miss keys like "account.owner/Ulan/"
|
||||
it, err := dbm.IteratePrefix(txi.store, prefixFromCompositeKey(c.CompositeKey))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer it.Close()
|
||||
|
||||
iterContains:
|
||||
for ; it.Valid(); it.Next() {
|
||||
value, err := parseValueFromKey(it.Key())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(value, c.Operand.(string)) {
|
||||
tmpHashes[string(it.Value())] = it.Value()
|
||||
}
|
||||
|
||||
// Potentially exit early.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break iterContains
|
||||
default:
|
||||
}
|
||||
}
|
||||
if err := it.Error(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
default:
|
||||
panic("other operators should be handled already")
|
||||
}
|
||||
|
||||
if len(tmpHashes) == 0 || firstRun {
|
||||
// Either:
|
||||
//
|
||||
// 1. Regardless if a previous match was attempted, which may have had
|
||||
// results, but no match was found for the current condition, then we
|
||||
// return no matches (assuming AND operand).
|
||||
//
|
||||
// 2. A previous match was not attempted, so we return all results.
|
||||
return tmpHashes
|
||||
}
|
||||
|
||||
// Remove/reduce matches in filteredHashes that were not found in this
|
||||
// match (tmpHashes).
|
||||
for k := range filteredHashes {
|
||||
if tmpHashes[k] == nil {
|
||||
delete(filteredHashes, k)
|
||||
|
||||
// Potentially exit early.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredHashes
|
||||
}
|
||||
|
||||
// matchRange returns all matching txs by hash that meet a given queryRange and
|
||||
// start key. An already filtered result (filteredHashes) is provided such that
|
||||
// any non-intersecting matches are removed.
|
||||
//
|
||||
// NOTE: filteredHashes may be empty if no previous condition has matched.
|
||||
func (txi *TxIndex) matchRange(
|
||||
ctx context.Context,
|
||||
qr indexer.QueryRange,
|
||||
startKey []byte,
|
||||
filteredHashes map[string][]byte,
|
||||
firstRun bool,
|
||||
) map[string][]byte {
|
||||
// A previous match was attempted but resulted in no matches, so we return
|
||||
// no matches (assuming AND operand).
|
||||
if !firstRun && len(filteredHashes) == 0 {
|
||||
return filteredHashes
|
||||
}
|
||||
|
||||
tmpHashes := make(map[string][]byte)
|
||||
lowerBound := qr.LowerBoundValue()
|
||||
upperBound := qr.UpperBoundValue()
|
||||
|
||||
it, err := dbm.IteratePrefix(txi.store, startKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer it.Close()
|
||||
|
||||
iter:
|
||||
for ; it.Valid(); it.Next() {
|
||||
value, err := parseValueFromKey(it.Key())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := qr.AnyBound().(int64); ok {
|
||||
v, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil {
|
||||
continue iter
|
||||
}
|
||||
|
||||
include := true
|
||||
if lowerBound != nil && v < lowerBound.(int64) {
|
||||
include = false
|
||||
}
|
||||
|
||||
if upperBound != nil && v > upperBound.(int64) {
|
||||
include = false
|
||||
}
|
||||
|
||||
if include {
|
||||
tmpHashes[string(it.Value())] = it.Value()
|
||||
}
|
||||
|
||||
// XXX: passing time in a ABCI Events is not yet implemented
|
||||
// case time.Time:
|
||||
// v := strconv.ParseInt(extractValueFromKey(it.Key()), 10, 64)
|
||||
// if v == r.upperBound {
|
||||
// break
|
||||
// }
|
||||
}
|
||||
|
||||
// Potentially exit early.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break iter
|
||||
default:
|
||||
}
|
||||
}
|
||||
if err := it.Error(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if len(tmpHashes) == 0 || firstRun {
|
||||
// Either:
|
||||
//
|
||||
// 1. Regardless if a previous match was attempted, which may have had
|
||||
// results, but no match was found for the current condition, then we
|
||||
// return no matches (assuming AND operand).
|
||||
//
|
||||
// 2. A previous match was not attempted, so we return all results.
|
||||
return tmpHashes
|
||||
}
|
||||
|
||||
// Remove/reduce matches in filteredHashes that were not found in this
|
||||
// match (tmpHashes).
|
||||
for k := range filteredHashes {
|
||||
if tmpHashes[k] == nil {
|
||||
delete(filteredHashes, k)
|
||||
|
||||
// Potentially exit early.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredHashes
|
||||
}
|
||||
|
||||
// ########################## Keys #############################
|
||||
//
|
||||
// The indexer has two types of kv stores:
|
||||
// 1. txhash - result (primary key)
|
||||
// 2. event - txhash (secondary key)
|
||||
//
|
||||
// The event key can be decomposed into 4 parts.
|
||||
// 1. A composite key which can be any string.
|
||||
// Usually something like "tx.height" or "account.owner"
|
||||
// 2. A value. That corresponds to the key. In the above
|
||||
// example the value could be "5" or "Ivan"
|
||||
// 3. The height of the Tx that aligns with the key and value.
|
||||
// 4. The index of the Tx that aligns with the key and value
|
||||
|
||||
// the hash/primary key
|
||||
func primaryKey(hash []byte) []byte {
|
||||
key, err := orderedcode.Append(
|
||||
nil,
|
||||
types.TxHashKey,
|
||||
string(hash),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// The event/secondary key
|
||||
func secondaryKey(compositeKey, value string, height int64, index uint32) []byte {
|
||||
key, err := orderedcode.Append(
|
||||
nil,
|
||||
compositeKey,
|
||||
value,
|
||||
height,
|
||||
int64(index),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// parseValueFromKey parses an event key and extracts out the value, returning an error if one arises.
|
||||
// This will also involve ensuring that the key has the correct format.
|
||||
// CONTRACT: function doesn't check that the prefix is correct. This should have already been done by the iterator
|
||||
func parseValueFromKey(key []byte) (string, error) {
|
||||
var (
|
||||
compositeKey, value string
|
||||
height, index int64
|
||||
)
|
||||
remaining, err := orderedcode.Parse(string(key), &compositeKey, &value, &height, &index)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(remaining) != 0 {
|
||||
return "", fmt.Errorf("unexpected remainder in key: %s", remaining)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func keyFromEvent(compositeKey string, value string, result *abci.TxResult) []byte {
|
||||
return secondaryKey(compositeKey, value, result.Height, result.Index)
|
||||
}
|
||||
|
||||
func KeyFromHeight(result *abci.TxResult) []byte {
|
||||
return secondaryKey(types.TxHeightKey, fmt.Sprintf("%d", result.Height), result.Height, result.Index)
|
||||
}
|
||||
|
||||
// Prefixes: these represent an initial part of the key and are used by iterators to iterate over a small
|
||||
// section of the kv store during searches.
|
||||
|
||||
func prefixFromCompositeKey(compositeKey string) []byte {
|
||||
key, err := orderedcode.Append(nil, compositeKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func prefixFromCompositeKeyAndValue(compositeKey, value string) []byte {
|
||||
key, err := orderedcode.Append(nil, compositeKey, value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// a small utility function for getting a keys prefix based on a condition and a height
|
||||
func prefixForCondition(c query.Condition, height int64) []byte {
|
||||
key := prefixFromCompositeKeyAndValue(c.CompositeKey, fmt.Sprintf("%v", c.Operand))
|
||||
if height > 0 {
|
||||
var err error
|
||||
key, err = orderedcode.Append(key, height)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
return key
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package kv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/libs/pubsub/query"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
func BenchmarkTxSearch(b *testing.B) {
|
||||
dbDir, err := ioutil.TempDir("", "benchmark_tx_search_test")
|
||||
if err != nil {
|
||||
b.Errorf("failed to create temporary directory: %s", err)
|
||||
}
|
||||
|
||||
db, err := dbm.NewGoLevelDB("benchmark_tx_search_test", dbDir)
|
||||
if err != nil {
|
||||
b.Errorf("failed to create database: %s", err)
|
||||
}
|
||||
|
||||
indexer := NewTxIndex(db)
|
||||
|
||||
for i := 0; i < 35000; i++ {
|
||||
events := []abci.Event{
|
||||
{
|
||||
Type: "transfer",
|
||||
Attributes: []abci.EventAttribute{
|
||||
{Key: "address", Value: fmt.Sprintf("address_%d", i%100), Index: true},
|
||||
{Key: "amount", Value: "50", Index: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
txBz := make([]byte, 8)
|
||||
if _, err := rand.Read(txBz); err != nil {
|
||||
b.Errorf("failed produce random bytes: %s", err)
|
||||
}
|
||||
|
||||
txResult := &abci.TxResult{
|
||||
Height: int64(i),
|
||||
Index: 0,
|
||||
Tx: types.Tx(string(txBz)),
|
||||
Result: abci.ResponseDeliverTx{
|
||||
Data: []byte{0},
|
||||
Code: abci.CodeTypeOK,
|
||||
Log: "",
|
||||
Events: events,
|
||||
},
|
||||
}
|
||||
|
||||
if err := indexer.Index([]*abci.TxResult{txResult}); err != nil {
|
||||
b.Errorf("failed to index tx: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
txQuery := query.MustParse("transfer.address = 'address_43' AND transfer.amount = 50")
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := indexer.Search(ctx, txQuery); err != nil {
|
||||
b.Errorf("failed to query for txs: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
package kv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
db "github.com/tendermint/tm-db"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
indexer "github.com/tendermint/tendermint/internal/state/indexer"
|
||||
"github.com/tendermint/tendermint/libs/pubsub/query"
|
||||
tmrand "github.com/tendermint/tendermint/libs/rand"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
func TestTxIndex(t *testing.T) {
|
||||
txIndexer := NewTxIndex(db.NewMemDB())
|
||||
|
||||
tx := types.Tx("HELLO WORLD")
|
||||
txResult := &abci.TxResult{
|
||||
Height: 1,
|
||||
Index: 0,
|
||||
Tx: tx,
|
||||
Result: abci.ResponseDeliverTx{
|
||||
Data: []byte{0},
|
||||
Code: abci.CodeTypeOK, Log: "", Events: nil,
|
||||
},
|
||||
}
|
||||
hash := tx.Hash()
|
||||
|
||||
batch := indexer.NewBatch(1)
|
||||
if err := batch.Add(txResult); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err := txIndexer.Index(batch.Ops)
|
||||
require.NoError(t, err)
|
||||
|
||||
loadedTxResult, err := txIndexer.Get(hash)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, proto.Equal(txResult, loadedTxResult))
|
||||
|
||||
tx2 := types.Tx("BYE BYE WORLD")
|
||||
txResult2 := &abci.TxResult{
|
||||
Height: 1,
|
||||
Index: 0,
|
||||
Tx: tx2,
|
||||
Result: abci.ResponseDeliverTx{
|
||||
Data: []byte{0},
|
||||
Code: abci.CodeTypeOK, Log: "", Events: nil,
|
||||
},
|
||||
}
|
||||
hash2 := tx2.Hash()
|
||||
|
||||
err = txIndexer.Index([]*abci.TxResult{txResult2})
|
||||
require.NoError(t, err)
|
||||
|
||||
loadedTxResult2, err := txIndexer.Get(hash2)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, proto.Equal(txResult2, loadedTxResult2))
|
||||
}
|
||||
|
||||
func TestTxSearch(t *testing.T) {
|
||||
indexer := NewTxIndex(db.NewMemDB())
|
||||
|
||||
txResult := txResultWithEvents([]abci.Event{
|
||||
{Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "1", Index: true}}},
|
||||
{Type: "account", Attributes: []abci.EventAttribute{{Key: "owner", Value: "Ivan", Index: true}}},
|
||||
{Type: "", Attributes: []abci.EventAttribute{{Key: "not_allowed", Value: "Vlad", Index: true}}},
|
||||
})
|
||||
hash := types.Tx(txResult.Tx).Hash()
|
||||
|
||||
err := indexer.Index([]*abci.TxResult{txResult})
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
q string
|
||||
resultsLength int
|
||||
}{
|
||||
// search by hash
|
||||
{fmt.Sprintf("tx.hash = '%X'", hash), 1},
|
||||
// search by exact match (one key)
|
||||
{"account.number = 1", 1},
|
||||
// search by exact match (two keys)
|
||||
{"account.number = 1 AND account.owner = 'Ivan'", 1},
|
||||
// search by exact match (two keys)
|
||||
{"account.number = 1 AND account.owner = 'Vlad'", 0},
|
||||
{"account.owner = 'Vlad' AND account.number = 1", 0},
|
||||
{"account.number >= 1 AND account.owner = 'Vlad'", 0},
|
||||
{"account.owner = 'Vlad' AND account.number >= 1", 0},
|
||||
{"account.number <= 0", 0},
|
||||
{"account.number <= 0 AND account.owner = 'Ivan'", 0},
|
||||
// search using a prefix of the stored value
|
||||
{"account.owner = 'Iv'", 0},
|
||||
// search by range
|
||||
{"account.number >= 1 AND account.number <= 5", 1},
|
||||
// search by range (lower bound)
|
||||
{"account.number >= 1", 1},
|
||||
// search by range (upper bound)
|
||||
{"account.number <= 5", 1},
|
||||
// search using not allowed key
|
||||
{"not_allowed = 'boom'", 0},
|
||||
// search for not existing tx result
|
||||
{"account.number >= 2 AND account.number <= 5", 0},
|
||||
// search using not existing key
|
||||
{"account.date >= TIME 2013-05-03T14:45:00Z", 0},
|
||||
// search using CONTAINS
|
||||
{"account.owner CONTAINS 'an'", 1},
|
||||
// search for non existing value using CONTAINS
|
||||
{"account.owner CONTAINS 'Vlad'", 0},
|
||||
// search using the wrong key (of numeric type) using CONTAINS
|
||||
{"account.number CONTAINS 'Iv'", 0},
|
||||
// search using EXISTS
|
||||
{"account.number EXISTS", 1},
|
||||
// search using EXISTS for non existing key
|
||||
{"account.date EXISTS", 0},
|
||||
// search using height
|
||||
{"account.number = 1 AND tx.height = 1", 1},
|
||||
// search using incorrect height
|
||||
{"account.number = 1 AND tx.height = 3", 0},
|
||||
// search using height only
|
||||
{"tx.height = 1", 1},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.q, func(t *testing.T) {
|
||||
results, err := indexer.Search(ctx, query.MustParse(tc.q))
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, results, tc.resultsLength)
|
||||
if tc.resultsLength > 0 {
|
||||
for _, txr := range results {
|
||||
assert.True(t, proto.Equal(txResult, txr))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTxSearchWithCancelation(t *testing.T) {
|
||||
indexer := NewTxIndex(db.NewMemDB())
|
||||
|
||||
txResult := txResultWithEvents([]abci.Event{
|
||||
{Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "1", Index: true}}},
|
||||
{Type: "account", Attributes: []abci.EventAttribute{{Key: "owner", Value: "Ivan", Index: true}}},
|
||||
{Type: "", Attributes: []abci.EventAttribute{{Key: "not_allowed", Value: "Vlad", Index: true}}},
|
||||
})
|
||||
err := indexer.Index([]*abci.TxResult{txResult})
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
results, err := indexer.Search(ctx, query.MustParse("account.number = 1"))
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, results)
|
||||
}
|
||||
|
||||
func TestTxSearchDeprecatedIndexing(t *testing.T) {
|
||||
indexer := NewTxIndex(db.NewMemDB())
|
||||
|
||||
// index tx using events indexing (composite key)
|
||||
txResult1 := txResultWithEvents([]abci.Event{
|
||||
{Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "1", Index: true}}},
|
||||
})
|
||||
hash1 := types.Tx(txResult1.Tx).Hash()
|
||||
|
||||
err := indexer.Index([]*abci.TxResult{txResult1})
|
||||
require.NoError(t, err)
|
||||
|
||||
// index tx also using deprecated indexing (event as key)
|
||||
txResult2 := txResultWithEvents(nil)
|
||||
txResult2.Tx = types.Tx("HELLO WORLD 2")
|
||||
|
||||
hash2 := types.Tx(txResult2.Tx).Hash()
|
||||
b := indexer.store.NewBatch()
|
||||
|
||||
rawBytes, err := proto.Marshal(txResult2)
|
||||
require.NoError(t, err)
|
||||
|
||||
depKey := []byte(fmt.Sprintf("%s/%s/%d/%d",
|
||||
"sender",
|
||||
"addr1",
|
||||
txResult2.Height,
|
||||
txResult2.Index,
|
||||
))
|
||||
|
||||
err = b.Set(depKey, hash2)
|
||||
require.NoError(t, err)
|
||||
err = b.Set(KeyFromHeight(txResult2), hash2)
|
||||
require.NoError(t, err)
|
||||
err = b.Set(hash2, rawBytes)
|
||||
require.NoError(t, err)
|
||||
err = b.Write()
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
q string
|
||||
results []*abci.TxResult
|
||||
}{
|
||||
// search by hash
|
||||
{fmt.Sprintf("tx.hash = '%X'", hash1), []*abci.TxResult{txResult1}},
|
||||
// search by hash
|
||||
{fmt.Sprintf("tx.hash = '%X'", hash2), []*abci.TxResult{txResult2}},
|
||||
// search by exact match (one key)
|
||||
{"account.number = 1", []*abci.TxResult{txResult1}},
|
||||
{"account.number >= 1 AND account.number <= 5", []*abci.TxResult{txResult1}},
|
||||
// search by range (lower bound)
|
||||
{"account.number >= 1", []*abci.TxResult{txResult1}},
|
||||
// search by range (upper bound)
|
||||
{"account.number <= 5", []*abci.TxResult{txResult1}},
|
||||
// search using not allowed key
|
||||
{"not_allowed = 'boom'", []*abci.TxResult{}},
|
||||
// search for not existing tx result
|
||||
{"account.number >= 2 AND account.number <= 5", []*abci.TxResult{}},
|
||||
// search using not existing key
|
||||
{"account.date >= TIME 2013-05-03T14:45:00Z", []*abci.TxResult{}},
|
||||
// search by deprecated key
|
||||
{"sender = 'addr1'", []*abci.TxResult{txResult2}},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.q, func(t *testing.T) {
|
||||
results, err := indexer.Search(ctx, query.MustParse(tc.q))
|
||||
require.NoError(t, err)
|
||||
for _, txr := range results {
|
||||
for _, tr := range tc.results {
|
||||
assert.True(t, proto.Equal(tr, txr))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTxSearchOneTxWithMultipleSameTagsButDifferentValues(t *testing.T) {
|
||||
indexer := NewTxIndex(db.NewMemDB())
|
||||
|
||||
txResult := txResultWithEvents([]abci.Event{
|
||||
{Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "1", Index: true}}},
|
||||
{Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "2", Index: true}}},
|
||||
})
|
||||
|
||||
err := indexer.Index([]*abci.TxResult{txResult})
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
results, err := indexer.Search(ctx, query.MustParse("account.number >= 1"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, results, 1)
|
||||
for _, txr := range results {
|
||||
assert.True(t, proto.Equal(txResult, txr))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTxSearchMultipleTxs(t *testing.T) {
|
||||
indexer := NewTxIndex(db.NewMemDB())
|
||||
|
||||
// indexed first, but bigger height (to test the order of transactions)
|
||||
txResult := txResultWithEvents([]abci.Event{
|
||||
{Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "1", Index: true}}},
|
||||
})
|
||||
|
||||
txResult.Tx = types.Tx("Bob's account")
|
||||
txResult.Height = 2
|
||||
txResult.Index = 1
|
||||
err := indexer.Index([]*abci.TxResult{txResult})
|
||||
require.NoError(t, err)
|
||||
|
||||
// indexed second, but smaller height (to test the order of transactions)
|
||||
txResult2 := txResultWithEvents([]abci.Event{
|
||||
{Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "2", Index: true}}},
|
||||
})
|
||||
txResult2.Tx = types.Tx("Alice's account")
|
||||
txResult2.Height = 1
|
||||
txResult2.Index = 2
|
||||
|
||||
err = indexer.Index([]*abci.TxResult{txResult2})
|
||||
require.NoError(t, err)
|
||||
|
||||
// indexed third (to test the order of transactions)
|
||||
txResult3 := txResultWithEvents([]abci.Event{
|
||||
{Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "3", Index: true}}},
|
||||
})
|
||||
txResult3.Tx = types.Tx("Jack's account")
|
||||
txResult3.Height = 1
|
||||
txResult3.Index = 1
|
||||
err = indexer.Index([]*abci.TxResult{txResult3})
|
||||
require.NoError(t, err)
|
||||
|
||||
// indexed fourth (to test we don't include txs with similar events)
|
||||
// https://github.com/tendermint/tendermint/issues/2908
|
||||
txResult4 := txResultWithEvents([]abci.Event{
|
||||
{Type: "account", Attributes: []abci.EventAttribute{{Key: "number.id", Value: "1", Index: true}}},
|
||||
})
|
||||
txResult4.Tx = types.Tx("Mike's account")
|
||||
txResult4.Height = 2
|
||||
txResult4.Index = 2
|
||||
err = indexer.Index([]*abci.TxResult{txResult4})
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
results, err := indexer.Search(ctx, query.MustParse("account.number >= 1"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
require.Len(t, results, 3)
|
||||
}
|
||||
|
||||
func txResultWithEvents(events []abci.Event) *abci.TxResult {
|
||||
tx := types.Tx("HELLO WORLD")
|
||||
return &abci.TxResult{
|
||||
Height: 1,
|
||||
Index: 0,
|
||||
Tx: tx,
|
||||
Result: abci.ResponseDeliverTx{
|
||||
Data: []byte{0},
|
||||
Code: abci.CodeTypeOK,
|
||||
Log: "",
|
||||
Events: events,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func benchmarkTxIndex(txsCount int64, b *testing.B) {
|
||||
dir, err := ioutil.TempDir("", "tx_index_db")
|
||||
require.NoError(b, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
store, err := db.NewDB("tx_index", "goleveldb", dir)
|
||||
require.NoError(b, err)
|
||||
txIndexer := NewTxIndex(store)
|
||||
|
||||
batch := indexer.NewBatch(txsCount)
|
||||
txIndex := uint32(0)
|
||||
for i := int64(0); i < txsCount; i++ {
|
||||
tx := tmrand.Bytes(250)
|
||||
txResult := &abci.TxResult{
|
||||
Height: 1,
|
||||
Index: txIndex,
|
||||
Tx: tx,
|
||||
Result: abci.ResponseDeliverTx{
|
||||
Data: []byte{0},
|
||||
Code: abci.CodeTypeOK,
|
||||
Log: "",
|
||||
Events: []abci.Event{},
|
||||
},
|
||||
}
|
||||
if err := batch.Add(txResult); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
txIndex++
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
err = txIndexer.Index(batch.Ops)
|
||||
}
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTxIndex1(b *testing.B) { benchmarkTxIndex(1, b) }
|
||||
func BenchmarkTxIndex500(b *testing.B) { benchmarkTxIndex(500, b) }
|
||||
func BenchmarkTxIndex1000(b *testing.B) { benchmarkTxIndex(1000, b) }
|
||||
func BenchmarkTxIndex2000(b *testing.B) { benchmarkTxIndex(2000, b) }
|
||||
func BenchmarkTxIndex10000(b *testing.B) { benchmarkTxIndex(10000, b) }
|
||||
@@ -0,0 +1,11 @@
|
||||
package kv
|
||||
|
||||
// IntInSlice returns true if a is found in the list.
|
||||
func intInSlice(a int, list []int) bool {
|
||||
for _, b := range list {
|
||||
if b == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package kv
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIntInSlice(t *testing.T) {
|
||||
assert.True(t, intInSlice(1, []int{1, 2, 3}))
|
||||
assert.False(t, intInSlice(4, []int{1, 2, 3}))
|
||||
assert.True(t, intInSlice(0, []int{0}))
|
||||
assert.False(t, intInSlice(0, []int{}))
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package null
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/internal/state/indexer"
|
||||
"github.com/tendermint/tendermint/libs/pubsub/query"
|
||||
)
|
||||
|
||||
var _ indexer.TxIndexer = (*TxIndex)(nil)
|
||||
|
||||
// TxIndex acts as a /dev/null.
|
||||
type TxIndex struct{}
|
||||
|
||||
// Get on a TxIndex is disabled and panics when invoked.
|
||||
func (txi *TxIndex) Get(hash []byte) (*abci.TxResult, error) {
|
||||
return nil, errors.New(`indexing is disabled (set 'tx_index = "kv"' in config)`)
|
||||
}
|
||||
|
||||
// AddBatch is a noop and always returns nil.
|
||||
func (txi *TxIndex) AddBatch(batch *indexer.Batch) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Index is a noop and always returns nil.
|
||||
func (txi *TxIndex) Index(results []*abci.TxResult) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (txi *TxIndex) Search(ctx context.Context, q *query.Query) ([]*abci.TxResult, error) {
|
||||
return []*abci.TxResult{}, nil
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"github.com/go-kit/kit/metrics"
|
||||
"github.com/go-kit/kit/metrics/discard"
|
||||
"github.com/go-kit/kit/metrics/prometheus"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
// MetricsSubsystem is a subsystem shared by all metrics exposed by this
|
||||
// package.
|
||||
MetricsSubsystem = "state"
|
||||
)
|
||||
|
||||
// Metrics contains metrics exposed by this package.
|
||||
type Metrics struct {
|
||||
// Time between BeginBlock and EndBlock.
|
||||
BlockProcessingTime metrics.Histogram
|
||||
}
|
||||
|
||||
// PrometheusMetrics returns Metrics build using Prometheus client library.
|
||||
// Optionally, labels can be provided along with their values ("foo",
|
||||
// "fooValue").
|
||||
func PrometheusMetrics(namespace string, labelsAndValues ...string) *Metrics {
|
||||
labels := []string{}
|
||||
for i := 0; i < len(labelsAndValues); i += 2 {
|
||||
labels = append(labels, labelsAndValues[i])
|
||||
}
|
||||
return &Metrics{
|
||||
BlockProcessingTime: prometheus.NewHistogramFrom(stdprometheus.HistogramOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: MetricsSubsystem,
|
||||
Name: "block_processing_time",
|
||||
Help: "Time between BeginBlock and EndBlock in ms.",
|
||||
Buckets: stdprometheus.LinearBuckets(1, 10, 10),
|
||||
}, labels).With(labelsAndValues...),
|
||||
}
|
||||
}
|
||||
|
||||
// NopMetrics returns no-op Metrics.
|
||||
func NopMetrics() *Metrics {
|
||||
return &Metrics{
|
||||
BlockProcessingTime: discard.NewHistogram(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
// Code generated by mockery. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
types "github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
// BlockStore is an autogenerated mock type for the BlockStore type
|
||||
type BlockStore struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Base provides a mock function with given fields:
|
||||
func (_m *BlockStore) Base() int64 {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 int64
|
||||
if rf, ok := ret.Get(0).(func() int64); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Height provides a mock function with given fields:
|
||||
func (_m *BlockStore) Height() int64 {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 int64
|
||||
if rf, ok := ret.Get(0).(func() int64); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// LoadBaseMeta provides a mock function with given fields:
|
||||
func (_m *BlockStore) LoadBaseMeta() *types.BlockMeta {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 *types.BlockMeta
|
||||
if rf, ok := ret.Get(0).(func() *types.BlockMeta); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*types.BlockMeta)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// LoadBlock provides a mock function with given fields: height
|
||||
func (_m *BlockStore) LoadBlock(height int64) *types.Block {
|
||||
ret := _m.Called(height)
|
||||
|
||||
var r0 *types.Block
|
||||
if rf, ok := ret.Get(0).(func(int64) *types.Block); ok {
|
||||
r0 = rf(height)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*types.Block)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// LoadBlockByHash provides a mock function with given fields: hash
|
||||
func (_m *BlockStore) LoadBlockByHash(hash []byte) *types.Block {
|
||||
ret := _m.Called(hash)
|
||||
|
||||
var r0 *types.Block
|
||||
if rf, ok := ret.Get(0).(func([]byte) *types.Block); ok {
|
||||
r0 = rf(hash)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*types.Block)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// LoadBlockCommit provides a mock function with given fields: height
|
||||
func (_m *BlockStore) LoadBlockCommit(height int64) *types.Commit {
|
||||
ret := _m.Called(height)
|
||||
|
||||
var r0 *types.Commit
|
||||
if rf, ok := ret.Get(0).(func(int64) *types.Commit); ok {
|
||||
r0 = rf(height)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*types.Commit)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// LoadBlockMeta provides a mock function with given fields: height
|
||||
func (_m *BlockStore) LoadBlockMeta(height int64) *types.BlockMeta {
|
||||
ret := _m.Called(height)
|
||||
|
||||
var r0 *types.BlockMeta
|
||||
if rf, ok := ret.Get(0).(func(int64) *types.BlockMeta); ok {
|
||||
r0 = rf(height)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*types.BlockMeta)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// LoadBlockPart provides a mock function with given fields: height, index
|
||||
func (_m *BlockStore) LoadBlockPart(height int64, index int) *types.Part {
|
||||
ret := _m.Called(height, index)
|
||||
|
||||
var r0 *types.Part
|
||||
if rf, ok := ret.Get(0).(func(int64, int) *types.Part); ok {
|
||||
r0 = rf(height, index)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*types.Part)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// LoadSeenCommit provides a mock function with given fields:
|
||||
func (_m *BlockStore) LoadSeenCommit() *types.Commit {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 *types.Commit
|
||||
if rf, ok := ret.Get(0).(func() *types.Commit); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*types.Commit)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// PruneBlocks provides a mock function with given fields: height
|
||||
func (_m *BlockStore) PruneBlocks(height int64) (uint64, error) {
|
||||
ret := _m.Called(height)
|
||||
|
||||
var r0 uint64
|
||||
if rf, ok := ret.Get(0).(func(int64) uint64); ok {
|
||||
r0 = rf(height)
|
||||
} else {
|
||||
r0 = ret.Get(0).(uint64)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(int64) error); ok {
|
||||
r1 = rf(height)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SaveBlock provides a mock function with given fields: block, blockParts, seenCommit
|
||||
func (_m *BlockStore) SaveBlock(block *types.Block, blockParts *types.PartSet, seenCommit *types.Commit) {
|
||||
_m.Called(block, blockParts, seenCommit)
|
||||
}
|
||||
|
||||
// Size provides a mock function with given fields:
|
||||
func (_m *BlockStore) Size() int64 {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 int64
|
||||
if rf, ok := ret.Get(0).(func() int64); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// Code generated by mockery 2.7.5. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
indexer "github.com/tendermint/tendermint/internal/state/indexer"
|
||||
|
||||
query "github.com/tendermint/tendermint/libs/pubsub/query"
|
||||
|
||||
tenderminttypes "github.com/tendermint/tendermint/types"
|
||||
|
||||
types "github.com/tendermint/tendermint/abci/types"
|
||||
)
|
||||
|
||||
// EventSink is an autogenerated mock type for the EventSink type
|
||||
type EventSink struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// GetTxByHash provides a mock function with given fields: _a0
|
||||
func (_m *EventSink) GetTxByHash(_a0 []byte) (*types.TxResult, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 *types.TxResult
|
||||
if rf, ok := ret.Get(0).(func([]byte) *types.TxResult); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*types.TxResult)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func([]byte) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// HasBlock provides a mock function with given fields: _a0
|
||||
func (_m *EventSink) HasBlock(_a0 int64) (bool, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(int64) bool); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(int64) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// IndexBlockEvents provides a mock function with given fields: _a0
|
||||
func (_m *EventSink) IndexBlockEvents(_a0 tenderminttypes.EventDataNewBlockHeader) error {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(tenderminttypes.EventDataNewBlockHeader) error); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// IndexTxEvents provides a mock function with given fields: _a0
|
||||
func (_m *EventSink) IndexTxEvents(_a0 []*types.TxResult) error {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func([]*types.TxResult) error); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SearchBlockEvents provides a mock function with given fields: _a0, _a1
|
||||
func (_m *EventSink) SearchBlockEvents(_a0 context.Context, _a1 *query.Query) ([]int64, error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
var r0 []int64
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *query.Query) []int64); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]int64)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *query.Query) error); ok {
|
||||
r1 = rf(_a0, _a1)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SearchTxEvents provides a mock function with given fields: _a0, _a1
|
||||
func (_m *EventSink) SearchTxEvents(_a0 context.Context, _a1 *query.Query) ([]*types.TxResult, error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
var r0 []*types.TxResult
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *query.Query) []*types.TxResult); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*types.TxResult)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *query.Query) error); ok {
|
||||
r1 = rf(_a0, _a1)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Stop provides a mock function with given fields:
|
||||
func (_m *EventSink) Stop() error {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Type provides a mock function with given fields:
|
||||
func (_m *EventSink) Type() indexer.EventSinkType {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 indexer.EventSinkType
|
||||
if rf, ok := ret.Get(0).(func() indexer.EventSinkType); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(indexer.EventSinkType)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Code generated by mockery. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
state "github.com/tendermint/tendermint/internal/state"
|
||||
|
||||
types "github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
// EvidencePool is an autogenerated mock type for the EvidencePool type
|
||||
type EvidencePool struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// AddEvidence provides a mock function with given fields: _a0
|
||||
func (_m *EvidencePool) AddEvidence(_a0 types.Evidence) error {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(types.Evidence) error); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// CheckEvidence provides a mock function with given fields: _a0
|
||||
func (_m *EvidencePool) CheckEvidence(_a0 types.EvidenceList) error {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(types.EvidenceList) error); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// PendingEvidence provides a mock function with given fields: maxBytes
|
||||
func (_m *EvidencePool) PendingEvidence(maxBytes int64) ([]types.Evidence, int64) {
|
||||
ret := _m.Called(maxBytes)
|
||||
|
||||
var r0 []types.Evidence
|
||||
if rf, ok := ret.Get(0).(func(int64) []types.Evidence); ok {
|
||||
r0 = rf(maxBytes)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]types.Evidence)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 int64
|
||||
if rf, ok := ret.Get(1).(func(int64) int64); ok {
|
||||
r1 = rf(maxBytes)
|
||||
} else {
|
||||
r1 = ret.Get(1).(int64)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Update provides a mock function with given fields: _a0, _a1
|
||||
func (_m *EvidencePool) Update(_a0 state.State, _a1 types.EvidenceList) {
|
||||
_m.Called(_a0, _a1)
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// Code generated by mockery. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
state "github.com/tendermint/tendermint/internal/state"
|
||||
|
||||
tendermintstate "github.com/tendermint/tendermint/proto/tendermint/state"
|
||||
|
||||
types "github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
// Store is an autogenerated mock type for the Store type
|
||||
type Store struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Bootstrap provides a mock function with given fields: _a0
|
||||
func (_m *Store) Bootstrap(_a0 state.State) error {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(state.State) error); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Load provides a mock function with given fields:
|
||||
func (_m *Store) Load() (state.State, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 state.State
|
||||
if rf, ok := ret.Get(0).(func() state.State); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(state.State)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// LoadABCIResponses provides a mock function with given fields: _a0
|
||||
func (_m *Store) LoadABCIResponses(_a0 int64) (*tendermintstate.ABCIResponses, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 *tendermintstate.ABCIResponses
|
||||
if rf, ok := ret.Get(0).(func(int64) *tendermintstate.ABCIResponses); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*tendermintstate.ABCIResponses)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(int64) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// LoadConsensusParams provides a mock function with given fields: _a0
|
||||
func (_m *Store) LoadConsensusParams(_a0 int64) (types.ConsensusParams, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 types.ConsensusParams
|
||||
if rf, ok := ret.Get(0).(func(int64) types.ConsensusParams); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Get(0).(types.ConsensusParams)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(int64) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// LoadValidators provides a mock function with given fields: _a0
|
||||
func (_m *Store) LoadValidators(_a0 int64) (*types.ValidatorSet, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 *types.ValidatorSet
|
||||
if rf, ok := ret.Get(0).(func(int64) *types.ValidatorSet); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*types.ValidatorSet)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(int64) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// PruneStates provides a mock function with given fields: _a0
|
||||
func (_m *Store) PruneStates(_a0 int64) error {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(int64) error); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Save provides a mock function with given fields: _a0
|
||||
func (_m *Store) Save(_a0 state.State) error {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(state.State) error); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SaveABCIResponses provides a mock function with given fields: _a0, _a1
|
||||
func (_m *Store) SaveABCIResponses(_a0 int64, _a1 *tendermintstate.ABCIResponses) error {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(int64, *tendermintstate.ABCIResponses) error); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SaveValidatorSets provides a mock function with given fields: _a0, _a1, _a2
|
||||
func (_m *Store) SaveValidatorSets(_a0 int64, _a1 int64, _a2 *types.ValidatorSet) error {
|
||||
ret := _m.Called(_a0, _a1, _a2)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(int64, int64, *types.ValidatorSet) error); ok {
|
||||
r0 = rf(_a0, _a1, _a2)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
//------------------------------------------------------
|
||||
// blockchain services types
|
||||
// NOTE: Interfaces used by RPC must be thread safe!
|
||||
//------------------------------------------------------
|
||||
|
||||
//go:generate ../scripts/mockery_generate.sh BlockStore
|
||||
|
||||
//------------------------------------------------------
|
||||
// blockstore
|
||||
|
||||
// BlockStore defines the interface used by the ConsensusState.
|
||||
type BlockStore interface {
|
||||
Base() int64
|
||||
Height() int64
|
||||
Size() int64
|
||||
|
||||
LoadBaseMeta() *types.BlockMeta
|
||||
LoadBlockMeta(height int64) *types.BlockMeta
|
||||
LoadBlock(height int64) *types.Block
|
||||
|
||||
SaveBlock(block *types.Block, blockParts *types.PartSet, seenCommit *types.Commit)
|
||||
|
||||
PruneBlocks(height int64) (uint64, error)
|
||||
|
||||
LoadBlockByHash(hash []byte) *types.Block
|
||||
LoadBlockPart(height int64, index int) *types.Part
|
||||
|
||||
LoadBlockCommit(height int64) *types.Commit
|
||||
LoadSeenCommit() *types.Commit
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// evidence pool
|
||||
|
||||
//go:generate ../scripts/mockery_generate.sh EvidencePool
|
||||
|
||||
// EvidencePool defines the EvidencePool interface used by State.
|
||||
type EvidencePool interface {
|
||||
PendingEvidence(maxBytes int64) (ev []types.Evidence, size int64)
|
||||
AddEvidence(types.Evidence) error
|
||||
Update(State, types.EvidenceList)
|
||||
CheckEvidence(types.EvidenceList) error
|
||||
}
|
||||
|
||||
// EmptyEvidencePool is an empty implementation of EvidencePool, useful for testing. It also complies
|
||||
// to the consensus evidence pool interface
|
||||
type EmptyEvidencePool struct{}
|
||||
|
||||
func (EmptyEvidencePool) PendingEvidence(maxBytes int64) (ev []types.Evidence, size int64) {
|
||||
return nil, 0
|
||||
}
|
||||
func (EmptyEvidencePool) AddEvidence(types.Evidence) error { return nil }
|
||||
func (EmptyEvidencePool) Update(State, types.EvidenceList) {}
|
||||
func (EmptyEvidencePool) CheckEvidence(evList types.EvidenceList) error { return nil }
|
||||
func (EmptyEvidencePool) ReportConflictingVotes(voteA, voteB *types.Vote) {}
|
||||
@@ -0,0 +1,374 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"time"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
|
||||
tmstate "github.com/tendermint/tendermint/proto/tendermint/state"
|
||||
tmversion "github.com/tendermint/tendermint/proto/tendermint/version"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
"github.com/tendermint/tendermint/version"
|
||||
)
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
type Version struct {
|
||||
Consensus version.Consensus ` json:"consensus"`
|
||||
Software string ` json:"software"`
|
||||
}
|
||||
|
||||
// InitStateVersion sets the Consensus.Block and Software versions,
|
||||
// but leaves the Consensus.App version blank.
|
||||
// The Consensus.App version will be set during the Handshake, once
|
||||
// we hear from the app what protocol version it is running.
|
||||
var InitStateVersion = Version{
|
||||
Consensus: version.Consensus{
|
||||
Block: version.BlockProtocol,
|
||||
App: 0,
|
||||
},
|
||||
Software: version.TMVersion,
|
||||
}
|
||||
|
||||
func (v *Version) ToProto() tmstate.Version {
|
||||
return tmstate.Version{
|
||||
Consensus: tmversion.Consensus{
|
||||
Block: v.Consensus.Block,
|
||||
App: v.Consensus.App,
|
||||
},
|
||||
Software: v.Software,
|
||||
}
|
||||
}
|
||||
|
||||
func VersionFromProto(v tmstate.Version) Version {
|
||||
return Version{
|
||||
Consensus: version.Consensus{
|
||||
Block: v.Consensus.Block,
|
||||
App: v.Consensus.App,
|
||||
},
|
||||
Software: v.Software,
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
// State is a short description of the latest committed block of the Tendermint consensus.
|
||||
// It keeps all information necessary to validate new blocks,
|
||||
// including the last validator set and the consensus params.
|
||||
// All fields are exposed so the struct can be easily serialized,
|
||||
// but none of them should be mutated directly.
|
||||
// Instead, use state.Copy() or updateState(...).
|
||||
// NOTE: not goroutine-safe.
|
||||
type State struct {
|
||||
Version Version
|
||||
|
||||
// immutable
|
||||
ChainID string
|
||||
InitialHeight int64 // should be 1, not 0, when starting from height 1
|
||||
|
||||
// LastBlockHeight=0 at genesis (ie. block(H=0) does not exist)
|
||||
LastBlockHeight int64
|
||||
LastBlockID types.BlockID
|
||||
LastBlockTime time.Time
|
||||
|
||||
// LastValidators is used to validate block.LastCommit.
|
||||
// Validators are persisted to the database separately every time they change,
|
||||
// so we can query for historical validator sets.
|
||||
// Note that if s.LastBlockHeight causes a valset change,
|
||||
// we set s.LastHeightValidatorsChanged = s.LastBlockHeight + 1 + 1
|
||||
// Extra +1 due to nextValSet delay.
|
||||
NextValidators *types.ValidatorSet
|
||||
Validators *types.ValidatorSet
|
||||
LastValidators *types.ValidatorSet
|
||||
LastHeightValidatorsChanged int64
|
||||
|
||||
// Consensus parameters used for validating blocks.
|
||||
// Changes returned by EndBlock and updated after Commit.
|
||||
ConsensusParams types.ConsensusParams
|
||||
LastHeightConsensusParamsChanged int64
|
||||
|
||||
// Merkle root of the results from executing prev block
|
||||
LastResultsHash []byte
|
||||
|
||||
// the latest AppHash we've received from calling abci.Commit()
|
||||
AppHash []byte
|
||||
}
|
||||
|
||||
// Copy makes a copy of the State for mutating.
|
||||
func (state State) Copy() State {
|
||||
|
||||
return State{
|
||||
Version: state.Version,
|
||||
ChainID: state.ChainID,
|
||||
InitialHeight: state.InitialHeight,
|
||||
|
||||
LastBlockHeight: state.LastBlockHeight,
|
||||
LastBlockID: state.LastBlockID,
|
||||
LastBlockTime: state.LastBlockTime,
|
||||
|
||||
NextValidators: state.NextValidators.Copy(),
|
||||
Validators: state.Validators.Copy(),
|
||||
LastValidators: state.LastValidators.Copy(),
|
||||
LastHeightValidatorsChanged: state.LastHeightValidatorsChanged,
|
||||
|
||||
ConsensusParams: state.ConsensusParams,
|
||||
LastHeightConsensusParamsChanged: state.LastHeightConsensusParamsChanged,
|
||||
|
||||
AppHash: state.AppHash,
|
||||
|
||||
LastResultsHash: state.LastResultsHash,
|
||||
}
|
||||
}
|
||||
|
||||
// Equals returns true if the States are identical.
|
||||
func (state State) Equals(state2 State) bool {
|
||||
sbz, s2bz := state.Bytes(), state2.Bytes()
|
||||
return bytes.Equal(sbz, s2bz)
|
||||
}
|
||||
|
||||
// Bytes serializes the State using protobuf.
|
||||
// It panics if either casting to protobuf or serialization fails.
|
||||
func (state State) Bytes() []byte {
|
||||
sm, err := state.ToProto()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
bz, err := proto.Marshal(sm)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bz
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the State is equal to the empty State.
|
||||
func (state State) IsEmpty() bool {
|
||||
return state.Validators == nil // XXX can't compare to Empty
|
||||
}
|
||||
|
||||
// ToProto takes the local state type and returns the equivalent proto type
|
||||
func (state *State) ToProto() (*tmstate.State, error) {
|
||||
if state == nil {
|
||||
return nil, errors.New("state is nil")
|
||||
}
|
||||
|
||||
sm := new(tmstate.State)
|
||||
|
||||
sm.Version = state.Version.ToProto()
|
||||
sm.ChainID = state.ChainID
|
||||
sm.InitialHeight = state.InitialHeight
|
||||
sm.LastBlockHeight = state.LastBlockHeight
|
||||
|
||||
sm.LastBlockID = state.LastBlockID.ToProto()
|
||||
sm.LastBlockTime = state.LastBlockTime
|
||||
vals, err := state.Validators.ToProto()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sm.Validators = vals
|
||||
|
||||
nVals, err := state.NextValidators.ToProto()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sm.NextValidators = nVals
|
||||
|
||||
if state.LastBlockHeight >= 1 { // At Block 1 LastValidators is nil
|
||||
lVals, err := state.LastValidators.ToProto()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sm.LastValidators = lVals
|
||||
}
|
||||
|
||||
sm.LastHeightValidatorsChanged = state.LastHeightValidatorsChanged
|
||||
sm.ConsensusParams = state.ConsensusParams.ToProto()
|
||||
sm.LastHeightConsensusParamsChanged = state.LastHeightConsensusParamsChanged
|
||||
sm.LastResultsHash = state.LastResultsHash
|
||||
sm.AppHash = state.AppHash
|
||||
|
||||
return sm, nil
|
||||
}
|
||||
|
||||
// StateFromProto takes a state proto message & returns the local state type
|
||||
func StateFromProto(pb *tmstate.State) (*State, error) { //nolint:golint
|
||||
if pb == nil {
|
||||
return nil, errors.New("nil State")
|
||||
}
|
||||
|
||||
state := new(State)
|
||||
|
||||
state.Version = VersionFromProto(pb.Version)
|
||||
state.ChainID = pb.ChainID
|
||||
state.InitialHeight = pb.InitialHeight
|
||||
|
||||
bi, err := types.BlockIDFromProto(&pb.LastBlockID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state.LastBlockID = *bi
|
||||
state.LastBlockHeight = pb.LastBlockHeight
|
||||
state.LastBlockTime = pb.LastBlockTime
|
||||
|
||||
vals, err := types.ValidatorSetFromProto(pb.Validators)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state.Validators = vals
|
||||
|
||||
nVals, err := types.ValidatorSetFromProto(pb.NextValidators)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state.NextValidators = nVals
|
||||
|
||||
if state.LastBlockHeight >= 1 { // At Block 1 LastValidators is nil
|
||||
lVals, err := types.ValidatorSetFromProto(pb.LastValidators)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state.LastValidators = lVals
|
||||
} else {
|
||||
state.LastValidators = types.NewValidatorSet(nil)
|
||||
}
|
||||
|
||||
state.LastHeightValidatorsChanged = pb.LastHeightValidatorsChanged
|
||||
state.ConsensusParams = types.ConsensusParamsFromProto(pb.ConsensusParams)
|
||||
state.LastHeightConsensusParamsChanged = pb.LastHeightConsensusParamsChanged
|
||||
state.LastResultsHash = pb.LastResultsHash
|
||||
state.AppHash = pb.AppHash
|
||||
|
||||
return state, nil
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------
|
||||
// Create a block from the latest state
|
||||
|
||||
// MakeBlock builds a block from the current state with the given txs, commit,
|
||||
// and evidence. Note it also takes a proposerAddress because the state does not
|
||||
// track rounds, and hence does not know the correct proposer. TODO: fix this!
|
||||
func (state State) MakeBlock(
|
||||
height int64,
|
||||
txs []types.Tx,
|
||||
commit *types.Commit,
|
||||
evidence []types.Evidence,
|
||||
proposerAddress []byte,
|
||||
) (*types.Block, *types.PartSet) {
|
||||
|
||||
// Build base block with block data.
|
||||
block := types.MakeBlock(height, txs, commit, evidence)
|
||||
|
||||
// Set time.
|
||||
var timestamp time.Time
|
||||
if height == state.InitialHeight {
|
||||
timestamp = state.LastBlockTime // genesis time
|
||||
} else {
|
||||
timestamp = MedianTime(commit, state.LastValidators)
|
||||
}
|
||||
|
||||
// Fill rest of header with state data.
|
||||
block.Header.Populate(
|
||||
state.Version.Consensus, state.ChainID,
|
||||
timestamp, state.LastBlockID,
|
||||
state.Validators.Hash(), state.NextValidators.Hash(),
|
||||
state.ConsensusParams.HashConsensusParams(), state.AppHash, state.LastResultsHash,
|
||||
proposerAddress,
|
||||
)
|
||||
|
||||
return block, block.MakePartSet(types.BlockPartSizeBytes)
|
||||
}
|
||||
|
||||
// MedianTime computes a median time for a given Commit (based on Timestamp field of votes messages) and the
|
||||
// corresponding validator set. The computed time is always between timestamps of
|
||||
// the votes sent by honest processes, i.e., a faulty processes can not arbitrarily increase or decrease the
|
||||
// computed value.
|
||||
func MedianTime(commit *types.Commit, validators *types.ValidatorSet) time.Time {
|
||||
weightedTimes := make([]*weightedTime, len(commit.Signatures))
|
||||
totalVotingPower := int64(0)
|
||||
|
||||
for i, commitSig := range commit.Signatures {
|
||||
if commitSig.Absent() {
|
||||
continue
|
||||
}
|
||||
_, validator := validators.GetByAddress(commitSig.ValidatorAddress)
|
||||
// If there's no condition, TestValidateBlockCommit panics; not needed normally.
|
||||
if validator != nil {
|
||||
totalVotingPower += validator.VotingPower
|
||||
weightedTimes[i] = newWeightedTime(commitSig.Timestamp, validator.VotingPower)
|
||||
}
|
||||
}
|
||||
|
||||
return weightedMedian(weightedTimes, totalVotingPower)
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------
|
||||
// Genesis
|
||||
|
||||
// MakeGenesisStateFromFile reads and unmarshals state from the given
|
||||
// file.
|
||||
//
|
||||
// Used during replay and in tests.
|
||||
func MakeGenesisStateFromFile(genDocFile string) (State, error) {
|
||||
genDoc, err := MakeGenesisDocFromFile(genDocFile)
|
||||
if err != nil {
|
||||
return State{}, err
|
||||
}
|
||||
return MakeGenesisState(genDoc)
|
||||
}
|
||||
|
||||
// MakeGenesisDocFromFile reads and unmarshals genesis doc from the given file.
|
||||
func MakeGenesisDocFromFile(genDocFile string) (*types.GenesisDoc, error) {
|
||||
genDocJSON, err := ioutil.ReadFile(genDocFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't read GenesisDoc file: %v", err)
|
||||
}
|
||||
genDoc, err := types.GenesisDocFromJSON(genDocJSON)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading GenesisDoc: %v", err)
|
||||
}
|
||||
return genDoc, nil
|
||||
}
|
||||
|
||||
// MakeGenesisState creates state from types.GenesisDoc.
|
||||
func MakeGenesisState(genDoc *types.GenesisDoc) (State, error) {
|
||||
err := genDoc.ValidateAndComplete()
|
||||
if err != nil {
|
||||
return State{}, fmt.Errorf("error in genesis doc: %w", err)
|
||||
}
|
||||
|
||||
var validatorSet, nextValidatorSet *types.ValidatorSet
|
||||
if genDoc.Validators == nil || len(genDoc.Validators) == 0 {
|
||||
validatorSet = types.NewValidatorSet(nil)
|
||||
nextValidatorSet = types.NewValidatorSet(nil)
|
||||
} else {
|
||||
validators := make([]*types.Validator, len(genDoc.Validators))
|
||||
for i, val := range genDoc.Validators {
|
||||
validators[i] = types.NewValidator(val.PubKey, val.Power)
|
||||
}
|
||||
validatorSet = types.NewValidatorSet(validators)
|
||||
nextValidatorSet = types.NewValidatorSet(validators).CopyIncrementProposerPriority(1)
|
||||
}
|
||||
|
||||
return State{
|
||||
Version: InitStateVersion,
|
||||
ChainID: genDoc.ChainID,
|
||||
InitialHeight: genDoc.InitialHeight,
|
||||
|
||||
LastBlockHeight: 0,
|
||||
LastBlockID: types.BlockID{},
|
||||
LastBlockTime: genDoc.GenesisTime,
|
||||
|
||||
NextValidators: nextValidatorSet,
|
||||
Validators: validatorSet,
|
||||
LastValidators: types.NewValidatorSet(nil),
|
||||
LastHeightValidatorsChanged: genDoc.InitialHeight,
|
||||
|
||||
ConsensusParams: *genDoc.ConsensusParams,
|
||||
LastHeightConsensusParamsChanged: genDoc.InitialHeight,
|
||||
|
||||
AppHash: genDoc.AppHash,
|
||||
}, nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,665 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/google/orderedcode"
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
tmmath "github.com/tendermint/tendermint/libs/math"
|
||||
tmos "github.com/tendermint/tendermint/libs/os"
|
||||
tmstate "github.com/tendermint/tendermint/proto/tendermint/state"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
const (
|
||||
// persist validators every valSetCheckpointInterval blocks to avoid
|
||||
// LoadValidators taking too much time.
|
||||
// https://github.com/tendermint/tendermint/pull/3438
|
||||
// 100000 results in ~ 100ms to get 100 validators (see BenchmarkLoadValidators)
|
||||
valSetCheckpointInterval = 100000
|
||||
)
|
||||
|
||||
//------------------------------------------------------------------------
|
||||
|
||||
const (
|
||||
// prefixes are unique across all tm db's
|
||||
prefixValidators = int64(5)
|
||||
prefixConsensusParams = int64(6)
|
||||
prefixABCIResponses = int64(7)
|
||||
prefixState = int64(8)
|
||||
)
|
||||
|
||||
func encodeKey(prefix int64, height int64) []byte {
|
||||
res, err := orderedcode.Append(nil, prefix, height)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func validatorsKey(height int64) []byte {
|
||||
return encodeKey(prefixValidators, height)
|
||||
}
|
||||
|
||||
func consensusParamsKey(height int64) []byte {
|
||||
return encodeKey(prefixConsensusParams, height)
|
||||
}
|
||||
|
||||
func abciResponsesKey(height int64) []byte {
|
||||
return encodeKey(prefixABCIResponses, height)
|
||||
}
|
||||
|
||||
// stateKey should never change after being set in init()
|
||||
var stateKey []byte
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
stateKey, err = orderedcode.Append(nil, prefixState)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------
|
||||
|
||||
//go:generate ../scripts/mockery_generate.sh Store
|
||||
|
||||
// Store defines the state store interface
|
||||
//
|
||||
// It is used to retrieve current state and save and load ABCI responses,
|
||||
// validators and consensus parameters
|
||||
type Store interface {
|
||||
// Load loads the current state of the blockchain
|
||||
Load() (State, error)
|
||||
// LoadValidators loads the validator set at a given height
|
||||
LoadValidators(int64) (*types.ValidatorSet, error)
|
||||
// LoadABCIResponses loads the abciResponse for a given height
|
||||
LoadABCIResponses(int64) (*tmstate.ABCIResponses, error)
|
||||
// LoadConsensusParams loads the consensus params for a given height
|
||||
LoadConsensusParams(int64) (types.ConsensusParams, error)
|
||||
// Save overwrites the previous state with the updated one
|
||||
Save(State) error
|
||||
// SaveABCIResponses saves ABCIResponses for a given height
|
||||
SaveABCIResponses(int64, *tmstate.ABCIResponses) error
|
||||
// SaveValidatorSet saves the validator set at a given height
|
||||
SaveValidatorSets(int64, int64, *types.ValidatorSet) error
|
||||
// Bootstrap is used for bootstrapping state when not starting from a initial height.
|
||||
Bootstrap(State) error
|
||||
// PruneStates takes the height from which to prune up to (exclusive)
|
||||
PruneStates(int64) error
|
||||
}
|
||||
|
||||
// dbStore wraps a db (github.com/tendermint/tm-db)
|
||||
type dbStore struct {
|
||||
db dbm.DB
|
||||
}
|
||||
|
||||
var _ Store = (*dbStore)(nil)
|
||||
|
||||
// NewStore creates the dbStore of the state pkg.
|
||||
func NewStore(db dbm.DB) Store {
|
||||
return dbStore{db}
|
||||
}
|
||||
|
||||
// LoadState loads the State from the database.
|
||||
func (store dbStore) Load() (State, error) {
|
||||
return store.loadState(stateKey)
|
||||
}
|
||||
|
||||
func (store dbStore) loadState(key []byte) (state State, err error) {
|
||||
buf, err := store.db.Get(key)
|
||||
if err != nil {
|
||||
return state, err
|
||||
}
|
||||
if len(buf) == 0 {
|
||||
return state, nil
|
||||
}
|
||||
|
||||
sp := new(tmstate.State)
|
||||
|
||||
err = proto.Unmarshal(buf, sp)
|
||||
if err != nil {
|
||||
// DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED
|
||||
tmos.Exit(fmt.Sprintf(`LoadState: Data has been corrupted or its spec has changed:
|
||||
%v\n`, err))
|
||||
}
|
||||
|
||||
sm, err := StateFromProto(sp)
|
||||
if err != nil {
|
||||
return state, err
|
||||
}
|
||||
|
||||
return *sm, nil
|
||||
}
|
||||
|
||||
// Save persists the State, the ValidatorsInfo, and the ConsensusParamsInfo to the database.
|
||||
// This flushes the writes (e.g. calls SetSync).
|
||||
func (store dbStore) Save(state State) error {
|
||||
return store.save(state, stateKey)
|
||||
}
|
||||
|
||||
func (store dbStore) save(state State, key []byte) error {
|
||||
batch := store.db.NewBatch()
|
||||
defer batch.Close()
|
||||
|
||||
nextHeight := state.LastBlockHeight + 1
|
||||
// If first block, save validators for the block.
|
||||
if nextHeight == 1 {
|
||||
nextHeight = state.InitialHeight
|
||||
// This extra logic due to Tendermint validator set changes being delayed 1 block.
|
||||
// It may get overwritten due to InitChain validator updates.
|
||||
if err := store.saveValidatorsInfo(nextHeight, nextHeight, state.Validators, batch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Save next validators.
|
||||
err := store.saveValidatorsInfo(nextHeight+1, state.LastHeightValidatorsChanged, state.NextValidators, batch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save next consensus params.
|
||||
if err := store.saveConsensusParamsInfo(nextHeight,
|
||||
state.LastHeightConsensusParamsChanged, state.ConsensusParams, batch); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := batch.Set(key, state.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return batch.WriteSync()
|
||||
}
|
||||
|
||||
// BootstrapState saves a new state, used e.g. by state sync when starting from non-zero height.
|
||||
func (store dbStore) Bootstrap(state State) error {
|
||||
height := state.LastBlockHeight + 1
|
||||
if height == 1 {
|
||||
height = state.InitialHeight
|
||||
}
|
||||
|
||||
batch := store.db.NewBatch()
|
||||
defer batch.Close()
|
||||
|
||||
if height > 1 && !state.LastValidators.IsNilOrEmpty() {
|
||||
if err := store.saveValidatorsInfo(height-1, height-1, state.LastValidators, batch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := store.saveValidatorsInfo(height, height, state.Validators, batch); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := store.saveValidatorsInfo(height+1, height+1, state.NextValidators, batch); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := store.saveConsensusParamsInfo(height,
|
||||
state.LastHeightConsensusParamsChanged, state.ConsensusParams, batch); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := batch.Set(stateKey, state.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return batch.WriteSync()
|
||||
}
|
||||
|
||||
// PruneStates deletes states up to the height specified (exclusive). It is not
|
||||
// guaranteed to delete all states, since the last checkpointed state and states being pointed to by
|
||||
// e.g. `LastHeightChanged` must remain. The state at retain height must also exist.
|
||||
// Pruning is done in descending order.
|
||||
func (store dbStore) PruneStates(retainHeight int64) error {
|
||||
if retainHeight <= 0 {
|
||||
return fmt.Errorf("height %v must be greater than 0", retainHeight)
|
||||
}
|
||||
|
||||
// NOTE: We need to prune consensus params first because the validator
|
||||
// sets have always one extra height. If validator sets were pruned first
|
||||
// we could get a situation where we prune up to the last validator set
|
||||
// yet don't have the respective consensus params at that height and thus
|
||||
// return an error
|
||||
if err := store.pruneConsensusParams(retainHeight); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := store.pruneValidatorSets(retainHeight); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := store.pruneABCIResponses(retainHeight); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// pruneValidatorSets calls a reverse iterator from base height to retain height (exclusive), deleting
|
||||
// all validator sets in between. Due to the fact that most validator sets stored reference an earlier
|
||||
// validator set, it is likely that there will remain one validator set left after pruning.
|
||||
func (store dbStore) pruneValidatorSets(retainHeight int64) error {
|
||||
valInfo, err := loadValidatorsInfo(store.db, retainHeight)
|
||||
if err != nil {
|
||||
return fmt.Errorf("validators at height %v not found: %w", retainHeight, err)
|
||||
}
|
||||
|
||||
// We will prune up to the validator set at the given "height". As we don't save validator sets every
|
||||
// height but only when they change or at a check point, it is likely that the validator set at the height
|
||||
// we prune to is empty and thus dependent on the validator set saved at a previous height. We must find
|
||||
// that validator set and make sure it is not pruned.
|
||||
lastRecordedValSetHeight := lastStoredHeightFor(retainHeight, valInfo.LastHeightChanged)
|
||||
lastRecordedValSet, err := loadValidatorsInfo(store.db, lastRecordedValSetHeight)
|
||||
if err != nil || lastRecordedValSet.ValidatorSet == nil {
|
||||
return fmt.Errorf("couldn't find validators at height %d (height %d was originally requested): %w",
|
||||
lastStoredHeightFor(retainHeight, valInfo.LastHeightChanged),
|
||||
retainHeight,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
// if this is not equal to the retain height, prune from the retain height to the height above
|
||||
// the last saved validator set. This way we can skip over the dependent validator set.
|
||||
if lastRecordedValSetHeight < retainHeight {
|
||||
err := store.pruneRange(
|
||||
validatorsKey(lastRecordedValSetHeight+1),
|
||||
validatorsKey(retainHeight),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// prune all the validators sets up to last saved validator set
|
||||
return store.pruneRange(
|
||||
validatorsKey(1),
|
||||
validatorsKey(lastRecordedValSetHeight),
|
||||
)
|
||||
}
|
||||
|
||||
// pruneConsensusParams calls a reverse iterator from base height to retain height batch deleting
|
||||
// all consensus params in between. If the consensus params at the new base height is dependent
|
||||
// on a prior height then this will keep that lower height too.
|
||||
func (store dbStore) pruneConsensusParams(retainHeight int64) error {
|
||||
paramsInfo, err := store.loadConsensusParamsInfo(retainHeight)
|
||||
if err != nil {
|
||||
return fmt.Errorf("consensus params at height %v not found: %w", retainHeight, err)
|
||||
}
|
||||
|
||||
// As we don't save the consensus params at every height, only when there is a consensus params change,
|
||||
// we must not prune (or save) the last consensus params that the consensus params info at height
|
||||
// is dependent on.
|
||||
if paramsInfo.ConsensusParams.Equal(&tmproto.ConsensusParams{}) {
|
||||
// sanity check that the consensus params at the last height it was changed is there
|
||||
lastRecordedConsensusParams, err := store.loadConsensusParamsInfo(paramsInfo.LastHeightChanged)
|
||||
if err != nil || lastRecordedConsensusParams.ConsensusParams.Equal(&tmproto.ConsensusParams{}) {
|
||||
return fmt.Errorf(
|
||||
"couldn't find consensus params at height %d (height %d was originally requested): %w",
|
||||
paramsInfo.LastHeightChanged,
|
||||
retainHeight,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
// prune the params above the height with which it last changed and below the retain height.
|
||||
err = store.pruneRange(
|
||||
consensusParamsKey(paramsInfo.LastHeightChanged+1),
|
||||
consensusParamsKey(retainHeight),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// prune all the consensus params up to either the last height the params changed or if the params
|
||||
// last changed at the retain height, then up to the retain height.
|
||||
return store.pruneRange(
|
||||
consensusParamsKey(1),
|
||||
consensusParamsKey(paramsInfo.LastHeightChanged),
|
||||
)
|
||||
}
|
||||
|
||||
// pruneABCIResponses calls a reverse iterator from base height to retain height batch deleting
|
||||
// all abci responses in between
|
||||
func (store dbStore) pruneABCIResponses(height int64) error {
|
||||
return store.pruneRange(abciResponsesKey(1), abciResponsesKey(height))
|
||||
}
|
||||
|
||||
// pruneRange is a generic function for deleting a range of keys in reverse order.
|
||||
// we keep filling up batches of at most 1000 keys, perform a deletion and continue until
|
||||
// we have gone through all of keys in the range. This avoids doing any writes whilst
|
||||
// iterating.
|
||||
func (store dbStore) pruneRange(start []byte, end []byte) error {
|
||||
var err error
|
||||
batch := store.db.NewBatch()
|
||||
defer batch.Close()
|
||||
|
||||
end, err = store.reverseBatchDelete(batch, start, end)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// iterate until the last batch of the pruning range in which case we will perform a
|
||||
// write sync
|
||||
for !bytes.Equal(start, end) {
|
||||
if err := batch.Write(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := batch.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
batch = store.db.NewBatch()
|
||||
|
||||
// fill a new batch of keys for deletion over the remainding range
|
||||
end, err = store.reverseBatchDelete(batch, start, end)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return batch.WriteSync()
|
||||
}
|
||||
|
||||
// reverseBatchDelete runs a reverse iterator (from end to start) filling up a batch until either
|
||||
// (a) the iterator reaches the start or (b) the iterator has added a 1000 keys (this avoids the
|
||||
// batch from growing too large)
|
||||
func (store dbStore) reverseBatchDelete(batch dbm.Batch, start, end []byte) ([]byte, error) {
|
||||
iter, err := store.db.ReverseIterator(start, end)
|
||||
if err != nil {
|
||||
return end, fmt.Errorf("iterator error: %w", err)
|
||||
}
|
||||
defer iter.Close()
|
||||
|
||||
size := 0
|
||||
for ; iter.Valid(); iter.Next() {
|
||||
if err := batch.Delete(iter.Key()); err != nil {
|
||||
return end, fmt.Errorf("pruning error at key %X: %w", iter.Key(), err)
|
||||
}
|
||||
|
||||
// avoid batches growing too large by capping them
|
||||
size++
|
||||
if size == 1000 {
|
||||
return iter.Key(), iter.Error()
|
||||
}
|
||||
}
|
||||
return start, iter.Error()
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------
|
||||
|
||||
// ABCIResponsesResultsHash returns the root hash of a Merkle tree of
|
||||
// ResponseDeliverTx responses (see ABCIResults.Hash)
|
||||
//
|
||||
// See merkle.SimpleHashFromByteSlices
|
||||
func ABCIResponsesResultsHash(ar *tmstate.ABCIResponses) []byte {
|
||||
return types.NewResults(ar.DeliverTxs).Hash()
|
||||
}
|
||||
|
||||
// LoadABCIResponses loads the ABCIResponses for the given height from the
|
||||
// database. If not found, ErrNoABCIResponsesForHeight is returned.
|
||||
//
|
||||
// This is useful for recovering from crashes where we called app.Commit and
|
||||
// before we called s.Save(). It can also be used to produce Merkle proofs of
|
||||
// the result of txs.
|
||||
func (store dbStore) LoadABCIResponses(height int64) (*tmstate.ABCIResponses, error) {
|
||||
buf, err := store.db.Get(abciResponsesKey(height))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(buf) == 0 {
|
||||
|
||||
return nil, ErrNoABCIResponsesForHeight{height}
|
||||
}
|
||||
|
||||
abciResponses := new(tmstate.ABCIResponses)
|
||||
err = abciResponses.Unmarshal(buf)
|
||||
if err != nil {
|
||||
// DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED
|
||||
tmos.Exit(fmt.Sprintf(`LoadABCIResponses: Data has been corrupted or its spec has
|
||||
changed: %v\n`, err))
|
||||
}
|
||||
// TODO: ensure that buf is completely read.
|
||||
|
||||
return abciResponses, nil
|
||||
}
|
||||
|
||||
// SaveABCIResponses persists the ABCIResponses to the database.
|
||||
// This is useful in case we crash after app.Commit and before s.Save().
|
||||
// Responses are indexed by height so they can also be loaded later to produce
|
||||
// Merkle proofs.
|
||||
//
|
||||
// Exposed for testing.
|
||||
func (store dbStore) SaveABCIResponses(height int64, abciResponses *tmstate.ABCIResponses) error {
|
||||
return store.saveABCIResponses(height, abciResponses)
|
||||
}
|
||||
|
||||
func (store dbStore) saveABCIResponses(height int64, abciResponses *tmstate.ABCIResponses) error {
|
||||
var dtxs []*abci.ResponseDeliverTx
|
||||
// strip nil values,
|
||||
for _, tx := range abciResponses.DeliverTxs {
|
||||
if tx != nil {
|
||||
dtxs = append(dtxs, tx)
|
||||
}
|
||||
}
|
||||
|
||||
abciResponses.DeliverTxs = dtxs
|
||||
|
||||
bz, err := abciResponses.Marshal()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return store.db.SetSync(abciResponsesKey(height), bz)
|
||||
}
|
||||
|
||||
// SaveValidatorSets is used to save the validator set over multiple heights.
|
||||
// It is exposed so that a backfill operation during state sync can populate
|
||||
// the store with the necessary amount of validator sets to verify any evidence
|
||||
// it may encounter.
|
||||
func (store dbStore) SaveValidatorSets(lowerHeight, upperHeight int64, vals *types.ValidatorSet) error {
|
||||
batch := store.db.NewBatch()
|
||||
defer batch.Close()
|
||||
|
||||
// batch together all the validator sets from lowerHeight to upperHeight
|
||||
for height := lowerHeight; height <= upperHeight; height++ {
|
||||
if err := store.saveValidatorsInfo(height, lowerHeight, vals, batch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return batch.WriteSync()
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
// LoadValidators loads the ValidatorSet for a given height.
|
||||
// Returns ErrNoValSetForHeight if the validator set can't be found for this height.
|
||||
func (store dbStore) LoadValidators(height int64) (*types.ValidatorSet, error) {
|
||||
|
||||
valInfo, err := loadValidatorsInfo(store.db, height)
|
||||
if err != nil {
|
||||
return nil, ErrNoValSetForHeight{height}
|
||||
}
|
||||
if valInfo.ValidatorSet == nil {
|
||||
lastStoredHeight := lastStoredHeightFor(height, valInfo.LastHeightChanged)
|
||||
valInfo2, err := loadValidatorsInfo(store.db, lastStoredHeight)
|
||||
if err != nil || valInfo2.ValidatorSet == nil {
|
||||
return nil,
|
||||
fmt.Errorf("couldn't find validators at height %d (height %d was originally requested): %w",
|
||||
lastStoredHeight,
|
||||
height,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
vs, err := types.ValidatorSetFromProto(valInfo2.ValidatorSet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vs.IncrementProposerPriority(tmmath.SafeConvertInt32(height - lastStoredHeight)) // mutate
|
||||
vi2, err := vs.ToProto()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
valInfo2.ValidatorSet = vi2
|
||||
valInfo = valInfo2
|
||||
}
|
||||
|
||||
vip, err := types.ValidatorSetFromProto(valInfo.ValidatorSet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vip, nil
|
||||
}
|
||||
|
||||
func lastStoredHeightFor(height, lastHeightChanged int64) int64 {
|
||||
checkpointHeight := height - height%valSetCheckpointInterval
|
||||
return tmmath.MaxInt64(checkpointHeight, lastHeightChanged)
|
||||
}
|
||||
|
||||
// CONTRACT: Returned ValidatorsInfo can be mutated.
|
||||
func loadValidatorsInfo(db dbm.DB, height int64) (*tmstate.ValidatorsInfo, error) {
|
||||
buf, err := db.Get(validatorsKey(height))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(buf) == 0 {
|
||||
return nil, errors.New("value retrieved from db is empty")
|
||||
}
|
||||
|
||||
v := new(tmstate.ValidatorsInfo)
|
||||
err = v.Unmarshal(buf)
|
||||
if err != nil {
|
||||
// DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED
|
||||
tmos.Exit(fmt.Sprintf(`LoadValidators: Data has been corrupted or its spec has changed:
|
||||
%v\n`, err))
|
||||
}
|
||||
// TODO: ensure that buf is completely read.
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// saveValidatorsInfo persists the validator set.
|
||||
//
|
||||
// `height` is the effective height for which the validator is responsible for
|
||||
// signing. It should be called from s.Save(), right before the state itself is
|
||||
// persisted.
|
||||
func (store dbStore) saveValidatorsInfo(
|
||||
height, lastHeightChanged int64,
|
||||
valSet *types.ValidatorSet,
|
||||
batch dbm.Batch,
|
||||
) error {
|
||||
if lastHeightChanged > height {
|
||||
return errors.New("lastHeightChanged cannot be greater than ValidatorsInfo height")
|
||||
}
|
||||
valInfo := &tmstate.ValidatorsInfo{
|
||||
LastHeightChanged: lastHeightChanged,
|
||||
}
|
||||
// Only persist validator set if it was updated or checkpoint height (see
|
||||
// valSetCheckpointInterval) is reached.
|
||||
if height == lastHeightChanged || height%valSetCheckpointInterval == 0 {
|
||||
pv, err := valSet.ToProto()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
valInfo.ValidatorSet = pv
|
||||
}
|
||||
|
||||
bz, err := valInfo.Marshal()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return batch.Set(validatorsKey(height), bz)
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
// ConsensusParamsInfo represents the latest consensus params, or the last height it changed
|
||||
|
||||
// Allocate empty Consensus params at compile time to avoid multiple allocations during runtime
|
||||
var (
|
||||
empty = types.ConsensusParams{}
|
||||
emptypb = tmproto.ConsensusParams{}
|
||||
)
|
||||
|
||||
// LoadConsensusParams loads the ConsensusParams for a given height.
|
||||
func (store dbStore) LoadConsensusParams(height int64) (types.ConsensusParams, error) {
|
||||
paramsInfo, err := store.loadConsensusParamsInfo(height)
|
||||
if err != nil {
|
||||
return empty, fmt.Errorf("could not find consensus params for height #%d: %w", height, err)
|
||||
}
|
||||
|
||||
if paramsInfo.ConsensusParams.Equal(&emptypb) {
|
||||
paramsInfo2, err := store.loadConsensusParamsInfo(paramsInfo.LastHeightChanged)
|
||||
if err != nil {
|
||||
return empty, fmt.Errorf(
|
||||
"couldn't find consensus params at height %d (height %d was originally requested): %w",
|
||||
paramsInfo.LastHeightChanged,
|
||||
height,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
paramsInfo = paramsInfo2
|
||||
}
|
||||
|
||||
return types.ConsensusParamsFromProto(paramsInfo.ConsensusParams), nil
|
||||
}
|
||||
|
||||
func (store dbStore) loadConsensusParamsInfo(height int64) (*tmstate.ConsensusParamsInfo, error) {
|
||||
buf, err := store.db.Get(consensusParamsKey(height))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(buf) == 0 {
|
||||
return nil, errors.New("value retrieved from db is empty")
|
||||
}
|
||||
|
||||
paramsInfo := new(tmstate.ConsensusParamsInfo)
|
||||
if err = paramsInfo.Unmarshal(buf); err != nil {
|
||||
// DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED
|
||||
tmos.Exit(fmt.Sprintf(`LoadConsensusParams: Data has been corrupted or its spec has changed:
|
||||
%v\n`, err))
|
||||
}
|
||||
// TODO: ensure that buf is completely read.
|
||||
|
||||
return paramsInfo, nil
|
||||
}
|
||||
|
||||
// saveConsensusParamsInfo persists the consensus params for the next block to disk.
|
||||
// It should be called from s.Save(), right before the state itself is persisted.
|
||||
// If the consensus params did not change after processing the latest block,
|
||||
// only the last height for which they changed is persisted.
|
||||
func (store dbStore) saveConsensusParamsInfo(
|
||||
nextHeight, changeHeight int64,
|
||||
params types.ConsensusParams,
|
||||
batch dbm.Batch,
|
||||
) error {
|
||||
paramsInfo := &tmstate.ConsensusParamsInfo{
|
||||
LastHeightChanged: changeHeight,
|
||||
}
|
||||
|
||||
if changeHeight == nextHeight {
|
||||
paramsInfo.ConsensusParams = params.ToProto()
|
||||
}
|
||||
bz, err := paramsInfo.Marshal()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return batch.Set(consensusParamsKey(nextHeight), bz)
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
package state_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
cfg "github.com/tendermint/tendermint/config"
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
"github.com/tendermint/tendermint/crypto/ed25519"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
"github.com/tendermint/tendermint/internal/test/factory"
|
||||
tmrand "github.com/tendermint/tendermint/libs/rand"
|
||||
tmstate "github.com/tendermint/tendermint/proto/tendermint/state"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
const (
|
||||
// make sure this is the same as in state/store.go
|
||||
valSetCheckpointInterval = 100000
|
||||
)
|
||||
|
||||
func TestStoreBootstrap(t *testing.T) {
|
||||
stateDB := dbm.NewMemDB()
|
||||
stateStore := sm.NewStore(stateDB)
|
||||
val, _ := factory.RandValidator(true, 10)
|
||||
val2, _ := factory.RandValidator(true, 10)
|
||||
val3, _ := factory.RandValidator(true, 10)
|
||||
vals := types.NewValidatorSet([]*types.Validator{val, val2, val3})
|
||||
bootstrapState := makeRandomStateFromValidatorSet(vals, 100, 100)
|
||||
err := stateStore.Bootstrap(bootstrapState)
|
||||
require.NoError(t, err)
|
||||
|
||||
// bootstrap should also save the previous validator
|
||||
_, err = stateStore.LoadValidators(99)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = stateStore.LoadValidators(100)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = stateStore.LoadValidators(101)
|
||||
require.NoError(t, err)
|
||||
|
||||
state, err := stateStore.Load()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, bootstrapState, state)
|
||||
}
|
||||
|
||||
func TestStoreLoadValidators(t *testing.T) {
|
||||
stateDB := dbm.NewMemDB()
|
||||
stateStore := sm.NewStore(stateDB)
|
||||
val, _ := factory.RandValidator(true, 10)
|
||||
val2, _ := factory.RandValidator(true, 10)
|
||||
val3, _ := factory.RandValidator(true, 10)
|
||||
vals := types.NewValidatorSet([]*types.Validator{val, val2, val3})
|
||||
|
||||
// 1) LoadValidators loads validators using a height where they were last changed
|
||||
// Note that only the next validators at height h + 1 are saved
|
||||
err := stateStore.Save(makeRandomStateFromValidatorSet(vals, 1, 1))
|
||||
require.NoError(t, err)
|
||||
err = stateStore.Save(makeRandomStateFromValidatorSet(vals.CopyIncrementProposerPriority(1), 2, 1))
|
||||
require.NoError(t, err)
|
||||
loadedVals, err := stateStore.LoadValidators(3)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, vals.CopyIncrementProposerPriority(3), loadedVals)
|
||||
|
||||
// 2) LoadValidators loads validators using a checkpoint height
|
||||
|
||||
// add a validator set at the checkpoint
|
||||
err = stateStore.Save(makeRandomStateFromValidatorSet(vals, valSetCheckpointInterval, 1))
|
||||
require.NoError(t, err)
|
||||
|
||||
// check that a request will go back to the last checkpoint
|
||||
_, err = stateStore.LoadValidators(valSetCheckpointInterval + 1)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, fmt.Sprintf("couldn't find validators at height %d (height %d was originally requested): "+
|
||||
"value retrieved from db is empty",
|
||||
valSetCheckpointInterval, valSetCheckpointInterval+1), err.Error())
|
||||
|
||||
// now save a validator set at that checkpoint
|
||||
err = stateStore.Save(makeRandomStateFromValidatorSet(vals, valSetCheckpointInterval-1, 1))
|
||||
require.NoError(t, err)
|
||||
|
||||
loadedVals, err = stateStore.LoadValidators(valSetCheckpointInterval)
|
||||
require.NoError(t, err)
|
||||
// validator set gets updated with the one given hence we expect it to equal next validators (with an increment of one)
|
||||
// as opposed to being equal to an increment of 100000 - 1 (if we didn't save at the checkpoint)
|
||||
require.Equal(t, vals.CopyIncrementProposerPriority(2), loadedVals)
|
||||
require.NotEqual(t, vals.CopyIncrementProposerPriority(valSetCheckpointInterval), loadedVals)
|
||||
}
|
||||
|
||||
// This benchmarks the speed of loading validators from different heights if there is no validator set change.
|
||||
// NOTE: This isn't too indicative of validator retrieval speed as the db is always (regardless of height) only
|
||||
// performing two operations: 1) retrieve validator info at height x, which has a last validator set change of 1
|
||||
// and 2) retrieve the validator set at the aforementioned height 1.
|
||||
func BenchmarkLoadValidators(b *testing.B) {
|
||||
const valSetSize = 100
|
||||
|
||||
config := cfg.ResetTestRoot("state_")
|
||||
defer os.RemoveAll(config.RootDir)
|
||||
dbType := dbm.BackendType(config.DBBackend)
|
||||
stateDB, err := dbm.NewDB("state", dbType, config.DBDir())
|
||||
require.NoError(b, err)
|
||||
stateStore := sm.NewStore(stateDB)
|
||||
state, err := sm.MakeGenesisStateFromFile(config.GenesisFile())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
state.Validators = genValSet(valSetSize)
|
||||
state.NextValidators = state.Validators.CopyIncrementProposerPriority(1)
|
||||
err = stateStore.Save(state)
|
||||
require.NoError(b, err)
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 10; i < 10000000000; i *= 10 { // 10, 100, 1000, ...
|
||||
i := i
|
||||
err = stateStore.Save(makeRandomStateFromValidatorSet(state.NextValidators,
|
||||
int64(i)-1, state.LastHeightValidatorsChanged))
|
||||
if err != nil {
|
||||
b.Fatalf("error saving store: %v", err)
|
||||
}
|
||||
|
||||
b.Run(fmt.Sprintf("height=%d", i), func(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
_, err := stateStore.LoadValidators(int64(i))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreLoadConsensusParams(t *testing.T) {
|
||||
stateDB := dbm.NewMemDB()
|
||||
stateStore := sm.NewStore(stateDB)
|
||||
err := stateStore.Save(makeRandomStateFromConsensusParams(types.DefaultConsensusParams(), 1, 1))
|
||||
require.NoError(t, err)
|
||||
params, err := stateStore.LoadConsensusParams(1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, types.DefaultConsensusParams(), ¶ms)
|
||||
|
||||
// we give the state store different params but say that the height hasn't changed, hence
|
||||
// it should save a pointer to the params at height 1
|
||||
differentParams := types.DefaultConsensusParams()
|
||||
differentParams.Block.MaxBytes = 20000
|
||||
err = stateStore.Save(makeRandomStateFromConsensusParams(differentParams, 10, 1))
|
||||
require.NoError(t, err)
|
||||
res, err := stateStore.LoadConsensusParams(10)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, res, params)
|
||||
require.NotEqual(t, res, differentParams)
|
||||
}
|
||||
|
||||
func TestPruneStates(t *testing.T) {
|
||||
testcases := map[string]struct {
|
||||
startHeight int64
|
||||
endHeight int64
|
||||
pruneHeight int64
|
||||
expectErr bool
|
||||
remainingValSetHeight int64
|
||||
remainingParamsHeight int64
|
||||
}{
|
||||
"error when prune height is 0": {1, 100, 0, true, 0, 0},
|
||||
"error when prune height is negative": {1, 100, -10, true, 0, 0},
|
||||
"error when prune height does not exist": {1, 100, 101, true, 0, 0},
|
||||
"prune all": {1, 100, 100, false, 93, 95},
|
||||
"prune from non 1 height": {10, 50, 40, false, 33, 35},
|
||||
"prune some": {1, 10, 8, false, 3, 5},
|
||||
// we test this because we flush to disk every 1000 "states"
|
||||
"prune more than 1000 state": {1, 1010, 1010, false, 1003, 1005},
|
||||
"prune across checkpoint": {99900, 100002, 100002, false, 100000, 99995},
|
||||
}
|
||||
for name, tc := range testcases {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
db := dbm.NewMemDB()
|
||||
|
||||
stateStore := sm.NewStore(db)
|
||||
pk := ed25519.GenPrivKey().PubKey()
|
||||
|
||||
// Generate a bunch of state data. Validators change for heights ending with 3, and
|
||||
// parameters when ending with 5.
|
||||
validator := &types.Validator{Address: tmrand.Bytes(crypto.AddressSize), VotingPower: 100, PubKey: pk}
|
||||
validatorSet := &types.ValidatorSet{
|
||||
Validators: []*types.Validator{validator},
|
||||
Proposer: validator,
|
||||
}
|
||||
valsChanged := int64(0)
|
||||
paramsChanged := int64(0)
|
||||
|
||||
for h := tc.startHeight; h <= tc.endHeight; h++ {
|
||||
if valsChanged == 0 || h%10 == 2 {
|
||||
valsChanged = h + 1 // Have to add 1, since NextValidators is what's stored
|
||||
}
|
||||
if paramsChanged == 0 || h%10 == 5 {
|
||||
paramsChanged = h
|
||||
}
|
||||
|
||||
state := sm.State{
|
||||
InitialHeight: 1,
|
||||
LastBlockHeight: h - 1,
|
||||
Validators: validatorSet,
|
||||
NextValidators: validatorSet,
|
||||
ConsensusParams: types.ConsensusParams{
|
||||
Block: types.BlockParams{MaxBytes: 10e6},
|
||||
},
|
||||
LastHeightValidatorsChanged: valsChanged,
|
||||
LastHeightConsensusParamsChanged: paramsChanged,
|
||||
}
|
||||
|
||||
if state.LastBlockHeight >= 1 {
|
||||
state.LastValidators = state.Validators
|
||||
}
|
||||
|
||||
err := stateStore.Save(state)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = stateStore.SaveABCIResponses(h, &tmstate.ABCIResponses{
|
||||
DeliverTxs: []*abci.ResponseDeliverTx{
|
||||
{Data: []byte{1}},
|
||||
{Data: []byte{2}},
|
||||
{Data: []byte{3}},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Test assertions
|
||||
err := stateStore.PruneStates(tc.pruneHeight)
|
||||
if tc.expectErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
for h := tc.pruneHeight; h <= tc.endHeight; h++ {
|
||||
vals, err := stateStore.LoadValidators(h)
|
||||
require.NoError(t, err, h)
|
||||
require.NotNil(t, vals, h)
|
||||
|
||||
params, err := stateStore.LoadConsensusParams(h)
|
||||
require.NoError(t, err, h)
|
||||
require.NotNil(t, params, h)
|
||||
|
||||
abci, err := stateStore.LoadABCIResponses(h)
|
||||
require.NoError(t, err, h)
|
||||
require.NotNil(t, abci, h)
|
||||
}
|
||||
|
||||
emptyParams := types.ConsensusParams{}
|
||||
|
||||
for h := tc.startHeight; h < tc.pruneHeight; h++ {
|
||||
vals, err := stateStore.LoadValidators(h)
|
||||
if h == tc.remainingValSetHeight {
|
||||
require.NoError(t, err, h)
|
||||
require.NotNil(t, vals, h)
|
||||
} else {
|
||||
require.Error(t, err, h)
|
||||
require.Nil(t, vals, h)
|
||||
}
|
||||
|
||||
params, err := stateStore.LoadConsensusParams(h)
|
||||
if h == tc.remainingParamsHeight {
|
||||
require.NoError(t, err, h)
|
||||
require.NotEqual(t, emptyParams, params, h)
|
||||
} else {
|
||||
require.Error(t, err, h)
|
||||
require.Equal(t, emptyParams, params, h)
|
||||
}
|
||||
|
||||
abci, err := stateStore.LoadABCIResponses(h)
|
||||
require.Error(t, err, h)
|
||||
require.Nil(t, abci, h)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestABCIResponsesResultsHash(t *testing.T) {
|
||||
responses := &tmstate.ABCIResponses{
|
||||
BeginBlock: &abci.ResponseBeginBlock{},
|
||||
DeliverTxs: []*abci.ResponseDeliverTx{
|
||||
{Code: 32, Data: []byte("Hello"), Log: "Huh?"},
|
||||
},
|
||||
EndBlock: &abci.ResponseEndBlock{},
|
||||
}
|
||||
|
||||
root := sm.ABCIResponsesResultsHash(responses)
|
||||
|
||||
// root should be Merkle tree root of DeliverTxs responses
|
||||
results := types.NewResults(responses.DeliverTxs)
|
||||
assert.Equal(t, root, results.Hash())
|
||||
|
||||
// test we can prove first DeliverTx
|
||||
proof := results.ProveResult(0)
|
||||
bz, err := results[0].Marshal()
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, proof.Verify(root, bz))
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
"github.com/tendermint/tendermint/internal/test/factory"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
func MakeBlocks(n int, state *sm.State, privVal types.PrivValidator) []*types.Block {
|
||||
blocks := make([]*types.Block, 0)
|
||||
|
||||
var (
|
||||
prevBlock *types.Block
|
||||
prevBlockMeta *types.BlockMeta
|
||||
)
|
||||
|
||||
appHeight := byte(0x01)
|
||||
for i := 0; i < n; i++ {
|
||||
height := int64(i + 1)
|
||||
|
||||
block, parts := makeBlockAndPartSet(*state, prevBlock, prevBlockMeta, privVal, height)
|
||||
blocks = append(blocks, block)
|
||||
|
||||
prevBlock = block
|
||||
prevBlockMeta = types.NewBlockMeta(block, parts)
|
||||
|
||||
// update state
|
||||
state.AppHash = []byte{appHeight}
|
||||
appHeight++
|
||||
state.LastBlockHeight = height
|
||||
}
|
||||
|
||||
return blocks
|
||||
}
|
||||
|
||||
func MakeBlock(state sm.State, height int64, c *types.Commit) *types.Block {
|
||||
block, _ := state.MakeBlock(
|
||||
height,
|
||||
factory.MakeTenTxs(state.LastBlockHeight),
|
||||
c,
|
||||
nil,
|
||||
state.Validators.GetProposer().Address,
|
||||
)
|
||||
return block
|
||||
}
|
||||
|
||||
func makeBlockAndPartSet(state sm.State, lastBlock *types.Block, lastBlockMeta *types.BlockMeta,
|
||||
privVal types.PrivValidator, height int64) (*types.Block, *types.PartSet) {
|
||||
|
||||
lastCommit := types.NewCommit(height-1, 0, types.BlockID{}, nil)
|
||||
if height > 1 {
|
||||
vote, _ := factory.MakeVote(
|
||||
privVal,
|
||||
lastBlock.Header.ChainID,
|
||||
1, lastBlock.Header.Height, 0, 2,
|
||||
lastBlockMeta.BlockID,
|
||||
time.Now())
|
||||
lastCommit = types.NewCommit(vote.Height, vote.Round,
|
||||
lastBlockMeta.BlockID, []types.CommitSig{vote.CommitSig()})
|
||||
}
|
||||
|
||||
return state.MakeBlock(height, []types.Tx{}, lastCommit, nil, state.Validators.GetProposer().Address)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// weightedTime for computing a median.
|
||||
type weightedTime struct {
|
||||
Time time.Time
|
||||
Weight int64
|
||||
}
|
||||
|
||||
// newWeightedTime with time and weight.
|
||||
func newWeightedTime(time time.Time, weight int64) *weightedTime {
|
||||
return &weightedTime{
|
||||
Time: time,
|
||||
Weight: weight,
|
||||
}
|
||||
}
|
||||
|
||||
// weightedMedian computes weighted median time for a given array of WeightedTime and the total voting power.
|
||||
func weightedMedian(weightedTimes []*weightedTime, totalVotingPower int64) (res time.Time) {
|
||||
median := totalVotingPower / 2
|
||||
|
||||
sort.Slice(weightedTimes, func(i, j int) bool {
|
||||
if weightedTimes[i] == nil {
|
||||
return false
|
||||
}
|
||||
if weightedTimes[j] == nil {
|
||||
return true
|
||||
}
|
||||
return weightedTimes[i].Time.UnixNano() < weightedTimes[j].Time.UnixNano()
|
||||
})
|
||||
|
||||
for _, weightedTime := range weightedTimes {
|
||||
if weightedTime != nil {
|
||||
if median <= weightedTime.Weight {
|
||||
res = weightedTime.Time
|
||||
break
|
||||
}
|
||||
median -= weightedTime.Weight
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
tmtime "github.com/tendermint/tendermint/libs/time"
|
||||
)
|
||||
|
||||
func TestWeightedMedian(t *testing.T) {
|
||||
m := make([]*weightedTime, 3)
|
||||
|
||||
t1 := tmtime.Now()
|
||||
t2 := t1.Add(5 * time.Second)
|
||||
t3 := t1.Add(10 * time.Second)
|
||||
|
||||
m[2] = newWeightedTime(t1, 33) // faulty processes
|
||||
m[0] = newWeightedTime(t2, 40) // correct processes
|
||||
m[1] = newWeightedTime(t3, 27) // correct processes
|
||||
totalVotingPower := int64(100)
|
||||
|
||||
median := weightedMedian(m, totalVotingPower)
|
||||
assert.Equal(t, t2, median)
|
||||
// median always returns value between values of correct processes
|
||||
assert.Equal(t, true, (median.After(t1) || median.Equal(t1)) &&
|
||||
(median.Before(t3) || median.Equal(t3)))
|
||||
|
||||
m[1] = newWeightedTime(t1, 40) // correct processes
|
||||
m[2] = newWeightedTime(t2, 27) // correct processes
|
||||
m[0] = newWeightedTime(t3, 33) // faulty processes
|
||||
totalVotingPower = int64(100)
|
||||
|
||||
median = weightedMedian(m, totalVotingPower)
|
||||
assert.Equal(t, t2, median)
|
||||
// median always returns value between values of correct processes
|
||||
assert.Equal(t, true, (median.After(t1) || median.Equal(t1)) &&
|
||||
(median.Before(t2) || median.Equal(t2)))
|
||||
|
||||
m = make([]*weightedTime, 8)
|
||||
t4 := t1.Add(15 * time.Second)
|
||||
t5 := t1.Add(60 * time.Second)
|
||||
|
||||
m[3] = newWeightedTime(t1, 10) // correct processes
|
||||
m[1] = newWeightedTime(t2, 10) // correct processes
|
||||
m[5] = newWeightedTime(t2, 10) // correct processes
|
||||
m[4] = newWeightedTime(t3, 23) // faulty processes
|
||||
m[0] = newWeightedTime(t4, 20) // correct processes
|
||||
m[7] = newWeightedTime(t5, 10) // faulty processes
|
||||
totalVotingPower = int64(83)
|
||||
|
||||
median = weightedMedian(m, totalVotingPower)
|
||||
assert.Equal(t, t3, median)
|
||||
// median always returns value between values of correct processes
|
||||
assert.Equal(t, true, (median.After(t1) || median.Equal(t1)) &&
|
||||
(median.Before(t4) || median.Equal(t4)))
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
mempl "github.com/tendermint/tendermint/internal/mempool"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
// TxPreCheck returns a function to filter transactions before processing.
|
||||
// The function limits the size of a transaction to the block's maximum data size.
|
||||
func TxPreCheck(state State) mempl.PreCheckFunc {
|
||||
maxDataBytes := types.MaxDataBytesNoEvidence(
|
||||
state.ConsensusParams.Block.MaxBytes,
|
||||
state.Validators.Size(),
|
||||
)
|
||||
return mempl.PreCheckMaxBytes(maxDataBytes)
|
||||
}
|
||||
|
||||
// TxPostCheck returns a function to filter transactions after processing.
|
||||
// The function limits the gas wanted by a transaction to the block's maximum total gas.
|
||||
func TxPostCheck(state State) mempl.PostCheckFunc {
|
||||
return mempl.PostCheckMaxGas(state.ConsensusParams.Block.MaxGas)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package state_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
tmrand "github.com/tendermint/tendermint/libs/rand"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
func TestTxFilter(t *testing.T) {
|
||||
genDoc := randomGenesisDoc()
|
||||
genDoc.ConsensusParams.Block.MaxBytes = 3000
|
||||
genDoc.ConsensusParams.Evidence.MaxBytes = 1500
|
||||
|
||||
// Max size of Txs is much smaller than size of block,
|
||||
// since we need to account for commits and evidence.
|
||||
testCases := []struct {
|
||||
tx types.Tx
|
||||
isErr bool
|
||||
}{
|
||||
{types.Tx(tmrand.Bytes(2155)), false},
|
||||
{types.Tx(tmrand.Bytes(2156)), true},
|
||||
{types.Tx(tmrand.Bytes(3000)), true},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
state, err := sm.MakeGenesisState(genDoc)
|
||||
require.NoError(t, err)
|
||||
|
||||
f := sm.TxPreCheck(state)
|
||||
if tc.isErr {
|
||||
assert.NotNil(t, f(tc.tx), "#%v", i)
|
||||
} else {
|
||||
assert.Nil(t, f(tc.tx), "#%v", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
//-----------------------------------------------------
|
||||
// Validate block
|
||||
|
||||
func validateBlock(state State, block *types.Block) error {
|
||||
// Validate internal consistency.
|
||||
if err := block.ValidateBasic(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate basic info.
|
||||
if block.Version.App != state.Version.Consensus.App ||
|
||||
block.Version.Block != state.Version.Consensus.Block {
|
||||
return fmt.Errorf("wrong Block.Header.Version. Expected %v, got %v",
|
||||
state.Version.Consensus,
|
||||
block.Version,
|
||||
)
|
||||
}
|
||||
if block.ChainID != state.ChainID {
|
||||
return fmt.Errorf("wrong Block.Header.ChainID. Expected %v, got %v",
|
||||
state.ChainID,
|
||||
block.ChainID,
|
||||
)
|
||||
}
|
||||
if state.LastBlockHeight == 0 && block.Height != state.InitialHeight {
|
||||
return fmt.Errorf("wrong Block.Header.Height. Expected %v for initial block, got %v",
|
||||
block.Height, state.InitialHeight)
|
||||
}
|
||||
if state.LastBlockHeight > 0 && block.Height != state.LastBlockHeight+1 {
|
||||
return fmt.Errorf("wrong Block.Header.Height. Expected %v, got %v",
|
||||
state.LastBlockHeight+1,
|
||||
block.Height,
|
||||
)
|
||||
}
|
||||
// Validate prev block info.
|
||||
if !block.LastBlockID.Equals(state.LastBlockID) {
|
||||
return fmt.Errorf("wrong Block.Header.LastBlockID. Expected %v, got %v",
|
||||
state.LastBlockID,
|
||||
block.LastBlockID,
|
||||
)
|
||||
}
|
||||
|
||||
// Validate app info
|
||||
if !bytes.Equal(block.AppHash, state.AppHash) {
|
||||
return fmt.Errorf("wrong Block.Header.AppHash. Expected %X, got %v",
|
||||
state.AppHash,
|
||||
block.AppHash,
|
||||
)
|
||||
}
|
||||
hashCP := state.ConsensusParams.HashConsensusParams()
|
||||
if !bytes.Equal(block.ConsensusHash, hashCP) {
|
||||
return fmt.Errorf("wrong Block.Header.ConsensusHash. Expected %X, got %v",
|
||||
hashCP,
|
||||
block.ConsensusHash,
|
||||
)
|
||||
}
|
||||
if !bytes.Equal(block.LastResultsHash, state.LastResultsHash) {
|
||||
return fmt.Errorf("wrong Block.Header.LastResultsHash. Expected %X, got %v",
|
||||
state.LastResultsHash,
|
||||
block.LastResultsHash,
|
||||
)
|
||||
}
|
||||
if !bytes.Equal(block.ValidatorsHash, state.Validators.Hash()) {
|
||||
return fmt.Errorf("wrong Block.Header.ValidatorsHash. Expected %X, got %v",
|
||||
state.Validators.Hash(),
|
||||
block.ValidatorsHash,
|
||||
)
|
||||
}
|
||||
if !bytes.Equal(block.NextValidatorsHash, state.NextValidators.Hash()) {
|
||||
return fmt.Errorf("wrong Block.Header.NextValidatorsHash. Expected %X, got %v",
|
||||
state.NextValidators.Hash(),
|
||||
block.NextValidatorsHash,
|
||||
)
|
||||
}
|
||||
|
||||
// Validate block LastCommit.
|
||||
if block.Height == state.InitialHeight {
|
||||
if len(block.LastCommit.Signatures) != 0 {
|
||||
return errors.New("initial block can't have LastCommit signatures")
|
||||
}
|
||||
} else {
|
||||
// LastCommit.Signatures length is checked in VerifyCommit.
|
||||
if err := state.LastValidators.VerifyCommit(
|
||||
state.ChainID, state.LastBlockID, block.Height-1, block.LastCommit); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: We can't actually verify it's the right proposer because we don't
|
||||
// know what round the block was first proposed. So just check that it's
|
||||
// a legit address and a known validator.
|
||||
// The length is checked in ValidateBasic above.
|
||||
if !state.Validators.HasAddress(block.ProposerAddress) {
|
||||
return fmt.Errorf("block.Header.ProposerAddress %X is not a validator",
|
||||
block.ProposerAddress,
|
||||
)
|
||||
}
|
||||
|
||||
// Validate block Time
|
||||
switch {
|
||||
case block.Height > state.InitialHeight:
|
||||
if !block.Time.After(state.LastBlockTime) {
|
||||
return fmt.Errorf("block time %v not greater than last block time %v",
|
||||
block.Time,
|
||||
state.LastBlockTime,
|
||||
)
|
||||
}
|
||||
medianTime := MedianTime(block.LastCommit, state.LastValidators)
|
||||
if !block.Time.Equal(medianTime) {
|
||||
return fmt.Errorf("invalid block time. Expected %v, got %v",
|
||||
medianTime,
|
||||
block.Time,
|
||||
)
|
||||
}
|
||||
|
||||
case block.Height == state.InitialHeight:
|
||||
genesisTime := state.LastBlockTime
|
||||
if !block.Time.Equal(genesisTime) {
|
||||
return fmt.Errorf("block time %v is not equal to genesis time %v",
|
||||
block.Time,
|
||||
genesisTime,
|
||||
)
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("block height %v lower than initial height %v",
|
||||
block.Height, state.InitialHeight)
|
||||
}
|
||||
|
||||
// Check evidence doesn't exceed the limit amount of bytes.
|
||||
if max, got := state.ConsensusParams.Evidence.MaxBytes, block.Evidence.ByteSize(); got > max {
|
||||
return types.NewErrEvidenceOverflow(max, got)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
package state_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/crypto/ed25519"
|
||||
"github.com/tendermint/tendermint/crypto/tmhash"
|
||||
memmock "github.com/tendermint/tendermint/internal/mempool/mock"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
"github.com/tendermint/tendermint/internal/state/mocks"
|
||||
sf "github.com/tendermint/tendermint/internal/state/test/factory"
|
||||
"github.com/tendermint/tendermint/internal/test/factory"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
tmtime "github.com/tendermint/tendermint/libs/time"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
"github.com/tendermint/tendermint/store"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
dbm "github.com/tendermint/tm-db"
|
||||
)
|
||||
|
||||
const validationTestsStopHeight int64 = 10
|
||||
|
||||
func TestValidateBlockHeader(t *testing.T) {
|
||||
proxyApp := newTestApp()
|
||||
require.NoError(t, proxyApp.Start())
|
||||
defer proxyApp.Stop() //nolint:errcheck // ignore for tests
|
||||
|
||||
state, stateDB, privVals := makeState(3, 1)
|
||||
stateStore := sm.NewStore(stateDB)
|
||||
blockStore := store.NewBlockStore(dbm.NewMemDB())
|
||||
blockExec := sm.NewBlockExecutor(
|
||||
stateStore,
|
||||
log.TestingLogger(),
|
||||
proxyApp.Consensus(),
|
||||
memmock.Mempool{},
|
||||
sm.EmptyEvidencePool{},
|
||||
blockStore,
|
||||
)
|
||||
lastCommit := types.NewCommit(0, 0, types.BlockID{}, nil)
|
||||
|
||||
// some bad values
|
||||
wrongHash := tmhash.Sum([]byte("this hash is wrong"))
|
||||
wrongVersion1 := state.Version.Consensus
|
||||
wrongVersion1.Block += 2
|
||||
wrongVersion2 := state.Version.Consensus
|
||||
wrongVersion2.App += 2
|
||||
|
||||
// Manipulation of any header field causes failure.
|
||||
testCases := []struct {
|
||||
name string
|
||||
malleateBlock func(block *types.Block)
|
||||
}{
|
||||
{"Version wrong1", func(block *types.Block) { block.Version = wrongVersion1 }},
|
||||
{"Version wrong2", func(block *types.Block) { block.Version = wrongVersion2 }},
|
||||
{"ChainID wrong", func(block *types.Block) { block.ChainID = "not-the-real-one" }},
|
||||
{"Height wrong", func(block *types.Block) { block.Height += 10 }},
|
||||
{"Time wrong", func(block *types.Block) { block.Time = block.Time.Add(-time.Second * 1) }},
|
||||
{"Time wrong 2", func(block *types.Block) { block.Time = block.Time.Add(time.Second * 1) }},
|
||||
|
||||
{"LastBlockID wrong", func(block *types.Block) { block.LastBlockID.PartSetHeader.Total += 10 }},
|
||||
{"LastCommitHash wrong", func(block *types.Block) { block.LastCommitHash = wrongHash }},
|
||||
{"DataHash wrong", func(block *types.Block) { block.DataHash = wrongHash }},
|
||||
|
||||
{"ValidatorsHash wrong", func(block *types.Block) { block.ValidatorsHash = wrongHash }},
|
||||
{"NextValidatorsHash wrong", func(block *types.Block) { block.NextValidatorsHash = wrongHash }},
|
||||
{"ConsensusHash wrong", func(block *types.Block) { block.ConsensusHash = wrongHash }},
|
||||
{"AppHash wrong", func(block *types.Block) { block.AppHash = wrongHash }},
|
||||
{"LastResultsHash wrong", func(block *types.Block) { block.LastResultsHash = wrongHash }},
|
||||
|
||||
{"EvidenceHash wrong", func(block *types.Block) { block.EvidenceHash = wrongHash }},
|
||||
{"Proposer wrong", func(block *types.Block) { block.ProposerAddress = ed25519.GenPrivKey().PubKey().Address() }},
|
||||
{"Proposer invalid", func(block *types.Block) { block.ProposerAddress = []byte("wrong size") }},
|
||||
|
||||
{"first LastCommit contains signatures", func(block *types.Block) {
|
||||
block.LastCommit = types.NewCommit(0, 0, types.BlockID{}, []types.CommitSig{types.NewCommitSigAbsent()})
|
||||
block.LastCommitHash = block.LastCommit.Hash()
|
||||
}},
|
||||
}
|
||||
|
||||
// Build up state for multiple heights
|
||||
for height := int64(1); height < validationTestsStopHeight; height++ {
|
||||
/*
|
||||
Invalid blocks don't pass
|
||||
*/
|
||||
for _, tc := range testCases {
|
||||
block := sf.MakeBlock(state, height, lastCommit)
|
||||
tc.malleateBlock(block)
|
||||
err := blockExec.ValidateBlock(state, block)
|
||||
t.Logf("%s: %v", tc.name, err)
|
||||
require.Error(t, err, tc.name)
|
||||
}
|
||||
|
||||
/*
|
||||
A good block passes
|
||||
*/
|
||||
var err error
|
||||
state, _, lastCommit, err = makeAndCommitGoodBlock(
|
||||
state, height, lastCommit, state.Validators.GetProposer().Address, blockExec, privVals, nil)
|
||||
require.NoError(t, err, "height %d", height)
|
||||
}
|
||||
|
||||
nextHeight := validationTestsStopHeight
|
||||
block := sf.MakeBlock(state, nextHeight, lastCommit)
|
||||
state.InitialHeight = nextHeight + 1
|
||||
err := blockExec.ValidateBlock(state, block)
|
||||
require.Error(t, err, "expected an error when state is ahead of block")
|
||||
assert.Contains(t, err.Error(), "lower than initial height")
|
||||
}
|
||||
|
||||
func TestValidateBlockCommit(t *testing.T) {
|
||||
proxyApp := newTestApp()
|
||||
require.NoError(t, proxyApp.Start())
|
||||
defer proxyApp.Stop() //nolint:errcheck // ignore for tests
|
||||
|
||||
state, stateDB, privVals := makeState(1, 1)
|
||||
stateStore := sm.NewStore(stateDB)
|
||||
blockStore := store.NewBlockStore(dbm.NewMemDB())
|
||||
blockExec := sm.NewBlockExecutor(
|
||||
stateStore,
|
||||
log.TestingLogger(),
|
||||
proxyApp.Consensus(),
|
||||
memmock.Mempool{},
|
||||
sm.EmptyEvidencePool{},
|
||||
blockStore,
|
||||
)
|
||||
lastCommit := types.NewCommit(0, 0, types.BlockID{}, nil)
|
||||
wrongSigsCommit := types.NewCommit(1, 0, types.BlockID{}, nil)
|
||||
badPrivVal := types.NewMockPV()
|
||||
|
||||
for height := int64(1); height < validationTestsStopHeight; height++ {
|
||||
proposerAddr := state.Validators.GetProposer().Address
|
||||
if height > 1 {
|
||||
/*
|
||||
#2589: ensure state.LastValidators.VerifyCommit fails here
|
||||
*/
|
||||
// should be height-1 instead of height
|
||||
wrongHeightVote, err := factory.MakeVote(
|
||||
privVals[proposerAddr.String()],
|
||||
chainID,
|
||||
1,
|
||||
height,
|
||||
0,
|
||||
2,
|
||||
state.LastBlockID,
|
||||
time.Now(),
|
||||
)
|
||||
require.NoError(t, err, "height %d", height)
|
||||
wrongHeightCommit := types.NewCommit(
|
||||
wrongHeightVote.Height,
|
||||
wrongHeightVote.Round,
|
||||
state.LastBlockID,
|
||||
[]types.CommitSig{wrongHeightVote.CommitSig()},
|
||||
)
|
||||
block := sf.MakeBlock(state, height, wrongHeightCommit)
|
||||
err = blockExec.ValidateBlock(state, block)
|
||||
_, isErrInvalidCommitHeight := err.(types.ErrInvalidCommitHeight)
|
||||
require.True(t, isErrInvalidCommitHeight, "expected ErrInvalidCommitHeight at height %d but got: %v", height, err)
|
||||
|
||||
/*
|
||||
#2589: test len(block.LastCommit.Signatures) == state.LastValidators.Size()
|
||||
*/
|
||||
block = sf.MakeBlock(state, height, wrongSigsCommit)
|
||||
err = blockExec.ValidateBlock(state, block)
|
||||
_, isErrInvalidCommitSignatures := err.(types.ErrInvalidCommitSignatures)
|
||||
require.True(t, isErrInvalidCommitSignatures,
|
||||
"expected ErrInvalidCommitSignatures at height %d, but got: %v",
|
||||
height,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
A good block passes
|
||||
*/
|
||||
var err error
|
||||
var blockID types.BlockID
|
||||
state, blockID, lastCommit, err = makeAndCommitGoodBlock(
|
||||
state,
|
||||
height,
|
||||
lastCommit,
|
||||
proposerAddr,
|
||||
blockExec,
|
||||
privVals,
|
||||
nil,
|
||||
)
|
||||
require.NoError(t, err, "height %d", height)
|
||||
|
||||
/*
|
||||
wrongSigsCommit is fine except for the extra bad precommit
|
||||
*/
|
||||
goodVote, err := factory.MakeVote(
|
||||
privVals[proposerAddr.String()],
|
||||
chainID,
|
||||
1,
|
||||
height,
|
||||
0,
|
||||
2,
|
||||
blockID,
|
||||
time.Now(),
|
||||
)
|
||||
require.NoError(t, err, "height %d", height)
|
||||
|
||||
bpvPubKey, err := badPrivVal.GetPubKey(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
badVote := &types.Vote{
|
||||
ValidatorAddress: bpvPubKey.Address(),
|
||||
ValidatorIndex: 0,
|
||||
Height: height,
|
||||
Round: 0,
|
||||
Timestamp: tmtime.Now(),
|
||||
Type: tmproto.PrecommitType,
|
||||
BlockID: blockID,
|
||||
}
|
||||
|
||||
g := goodVote.ToProto()
|
||||
b := badVote.ToProto()
|
||||
|
||||
err = badPrivVal.SignVote(context.Background(), chainID, g)
|
||||
require.NoError(t, err, "height %d", height)
|
||||
err = badPrivVal.SignVote(context.Background(), chainID, b)
|
||||
require.NoError(t, err, "height %d", height)
|
||||
|
||||
goodVote.Signature, badVote.Signature = g.Signature, b.Signature
|
||||
|
||||
wrongSigsCommit = types.NewCommit(goodVote.Height, goodVote.Round,
|
||||
blockID, []types.CommitSig{goodVote.CommitSig(), badVote.CommitSig()})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBlockEvidence(t *testing.T) {
|
||||
proxyApp := newTestApp()
|
||||
require.NoError(t, proxyApp.Start())
|
||||
defer proxyApp.Stop() //nolint:errcheck // ignore for tests
|
||||
|
||||
state, stateDB, privVals := makeState(4, 1)
|
||||
stateStore := sm.NewStore(stateDB)
|
||||
blockStore := store.NewBlockStore(dbm.NewMemDB())
|
||||
defaultEvidenceTime := time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
evpool := &mocks.EvidencePool{}
|
||||
evpool.On("CheckEvidence", mock.AnythingOfType("types.EvidenceList")).Return(nil)
|
||||
evpool.On("Update", mock.AnythingOfType("state.State"), mock.AnythingOfType("types.EvidenceList")).Return()
|
||||
evpool.On("ABCIEvidence", mock.AnythingOfType("int64"), mock.AnythingOfType("[]types.Evidence")).Return(
|
||||
[]abci.Evidence{})
|
||||
|
||||
state.ConsensusParams.Evidence.MaxBytes = 1000
|
||||
blockExec := sm.NewBlockExecutor(
|
||||
stateStore,
|
||||
log.TestingLogger(),
|
||||
proxyApp.Consensus(),
|
||||
memmock.Mempool{},
|
||||
evpool,
|
||||
blockStore,
|
||||
)
|
||||
lastCommit := types.NewCommit(0, 0, types.BlockID{}, nil)
|
||||
|
||||
for height := int64(1); height < validationTestsStopHeight; height++ {
|
||||
proposerAddr := state.Validators.GetProposer().Address
|
||||
maxBytesEvidence := state.ConsensusParams.Evidence.MaxBytes
|
||||
if height > 1 {
|
||||
/*
|
||||
A block with too much evidence fails
|
||||
*/
|
||||
evidence := make([]types.Evidence, 0)
|
||||
var currentBytes int64 = 0
|
||||
// more bytes than the maximum allowed for evidence
|
||||
for currentBytes <= maxBytesEvidence {
|
||||
newEv := types.NewMockDuplicateVoteEvidenceWithValidator(height, time.Now(),
|
||||
privVals[proposerAddr.String()], chainID)
|
||||
evidence = append(evidence, newEv)
|
||||
currentBytes += int64(len(newEv.Bytes()))
|
||||
}
|
||||
block, _ := state.MakeBlock(height, factory.MakeTenTxs(height), lastCommit, evidence, proposerAddr)
|
||||
err := blockExec.ValidateBlock(state, block)
|
||||
if assert.Error(t, err) {
|
||||
_, ok := err.(*types.ErrEvidenceOverflow)
|
||||
require.True(t, ok, "expected error to be of type ErrEvidenceOverflow at height %d but got %v", height, err)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
A good block with several pieces of good evidence passes
|
||||
*/
|
||||
evidence := make([]types.Evidence, 0)
|
||||
var currentBytes int64 = 0
|
||||
// precisely the amount of allowed evidence
|
||||
for {
|
||||
newEv := types.NewMockDuplicateVoteEvidenceWithValidator(height, defaultEvidenceTime,
|
||||
privVals[proposerAddr.String()], chainID)
|
||||
currentBytes += int64(len(newEv.Bytes()))
|
||||
if currentBytes >= maxBytesEvidence {
|
||||
break
|
||||
}
|
||||
evidence = append(evidence, newEv)
|
||||
}
|
||||
|
||||
var err error
|
||||
state, _, lastCommit, err = makeAndCommitGoodBlock(
|
||||
state,
|
||||
height,
|
||||
lastCommit,
|
||||
proposerAddr,
|
||||
blockExec,
|
||||
privVals,
|
||||
evidence,
|
||||
)
|
||||
require.NoError(t, err, "height %d", height)
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
state "github.com/tendermint/tendermint/state"
|
||||
state "github.com/tendermint/tendermint/internal/state"
|
||||
|
||||
types "github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
@@ -15,12 +15,12 @@ import (
|
||||
tmsync "github.com/tendermint/tendermint/internal/libs/sync"
|
||||
"github.com/tendermint/tendermint/internal/p2p"
|
||||
"github.com/tendermint/tendermint/internal/proxy"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/libs/service"
|
||||
"github.com/tendermint/tendermint/light"
|
||||
"github.com/tendermint/tendermint/light/provider"
|
||||
ssproto "github.com/tendermint/tendermint/proto/tendermint/statesync"
|
||||
sm "github.com/tendermint/tendermint/state"
|
||||
"github.com/tendermint/tendermint/store"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
@@ -18,13 +18,13 @@ import (
|
||||
"github.com/tendermint/tendermint/internal/p2p"
|
||||
"github.com/tendermint/tendermint/internal/proxy"
|
||||
proxymocks "github.com/tendermint/tendermint/internal/proxy/mocks"
|
||||
smmocks "github.com/tendermint/tendermint/internal/state/mocks"
|
||||
"github.com/tendermint/tendermint/internal/statesync/mocks"
|
||||
"github.com/tendermint/tendermint/internal/test/factory"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/light/provider"
|
||||
ssproto "github.com/tendermint/tendermint/proto/tendermint/statesync"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
smmocks "github.com/tendermint/tendermint/state/mocks"
|
||||
"github.com/tendermint/tendermint/store"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
tmsync "github.com/tendermint/tendermint/internal/libs/sync"
|
||||
"github.com/tendermint/tendermint/internal/p2p"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/light"
|
||||
lightprovider "github.com/tendermint/tendermint/light/provider"
|
||||
@@ -20,7 +21,6 @@ import (
|
||||
lightdb "github.com/tendermint/tendermint/light/store/db"
|
||||
ssproto "github.com/tendermint/tendermint/proto/tendermint/statesync"
|
||||
rpchttp "github.com/tendermint/tendermint/rpc/client/http"
|
||||
sm "github.com/tendermint/tendermint/state"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
"github.com/tendermint/tendermint/version"
|
||||
)
|
||||
|
||||
@@ -12,10 +12,10 @@ import (
|
||||
tmsync "github.com/tendermint/tendermint/internal/libs/sync"
|
||||
"github.com/tendermint/tendermint/internal/p2p"
|
||||
"github.com/tendermint/tendermint/internal/proxy"
|
||||
sm "github.com/tendermint/tendermint/internal/state"
|
||||
"github.com/tendermint/tendermint/libs/log"
|
||||
"github.com/tendermint/tendermint/light"
|
||||
ssproto "github.com/tendermint/tendermint/proto/tendermint/statesync"
|
||||
sm "github.com/tendermint/tendermint/state"
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user