Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v0.37.x:state/kvindexer: port 0.34 query fix #77

Merged
merged 28 commits into from
Feb 8, 2023
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b6d99be
state/kvindexer: associate event attributes with events (#9759)
jmalicevic Dec 14, 2022
6fe2b13
Backport kvindexer fix
jmalicevic Dec 14, 2022
b9f5a5b
Fixed match.events order in light client rpc
jmalicevic Dec 14, 2022
bd9ed68
cli: add --hard flag to rollback command to remove block as well (ba…
mergify[bot] Dec 21, 2022
c52ffdc
By event search is now default behaviour. Including fixes from PRs ad…
jmalicevic Jan 5, 2023
65dac51
Merge branch 'v0.37.x' into jasmina/kvindexer-eventq-0.37
jmalicevic Jan 5, 2023
44113d7
Changelog update
jmalicevic Jan 5, 2023
bc44f40
Reverted function removal and signature changes
jmalicevic Jan 5, 2023
2ef62aa
Fix linter errors
jmalicevic Jan 5, 2023
b677ace
Fix linter errors
jmalicevic Jan 5, 2023
f7a0740
Apply suggestions from code review
jmalicevic Jan 9, 2023
81c37b4
Renamed variable
jmalicevic Jan 9, 2023
5f84a1e
Merge branch 'release/v0.37.1' into jasmina/kvindexer-eventq-0.37
jmalicevic Jan 9, 2023
4cfc9cb
Update CHANGELOG_PENDING.md
jmalicevic Jan 9, 2023
ca08390
Update CODEOWNERS (#99)
thanethomson Jan 12, 2023
16ffabc
security/p2p: prevent peers who errored being added to the peer_set (…
jmalicevic Jan 12, 2023
81caedc
Merge branch 'release/v0.37.1' into jasmina/kvindexer-eventq-0.37
jmalicevic Jan 12, 2023
225e7d6
Merge branch 'v0.37.x' into jasmina/kvindexer-eventq-0.37
jmalicevic Jan 27, 2023
8db6226
fixed renaming
jmalicevic Jan 27, 2023
233fd53
Added changelog entry for kvindexer fix
jmalicevic Jan 27, 2023
6ccb03e
Added missing text
jmalicevic Jan 27, 2023
34c3cff
Merge branch 'v0.37.x' into jasmina/kvindexer-eventq-0.37
jmalicevic Jan 31, 2023
52ab759
Merge branch 'v0.37.x' into jasmina/kvindexer-eventq-0.37
jmalicevic Feb 6, 2023
2efdd22
Some extra TX indexing test cases
sergio-mena Feb 2, 2023
896b509
Documentation and typos
jmalicevic Feb 6, 2023
8589f14
Update .changelog/unreleased/bug-fixes/77-kvindexer-fix-evattribute-i…
jmalicevic Feb 8, 2023
3796592
Merge branch 'v0.37.x' into jasmina/kvindexer-eventq-0.37
jmalicevic Feb 8, 2023
1d25966
Merge branch 'v0.37.x' into jasmina/kvindexer-eventq-0.37
jmalicevic Feb 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- [state/kvindexer] \#77 Fixed the default behaviour of the kvindexer to index and query attributes by events in which they occur. In 0.34.25 this was mitigated by a separated RPC flag. (@jmalicevic)
jmalicevic marked this conversation as resolved.
Show resolved Hide resolved
81 changes: 80 additions & 1 deletion abci/example/kvstore/kvstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,20 @@ type Application struct {
state State
RetainBlocks int64 // blocks to retain after commit (via ResponseCommit.RetainHeight)
txToRemove map[string]struct{}
// If true, the app will generate block events in BeginBlock. Used to test the event indexer
// Should be false by default to avoid generating too much data.
genBlockEvents bool
}

func NewApplication() *Application {
state := loadState(dbm.NewMemDB())
return &Application{state: state}
}

func (app *Application) SetGenBlockEvents() {
app.genBlockEvents = true
}

func (app *Application) Info(req types.RequestInfo) (resInfo types.ResponseInfo) {
return types.ResponseInfo{
Data: fmt.Sprintf("{\"size\":%v}", app.state.Size),
Expand Down Expand Up @@ -116,6 +123,15 @@ func (app *Application) DeliverTx(req types.RequestDeliverTx) types.ResponseDeli
{Key: "noindex_key", Value: "index is working", Index: false},
},
},
{
Type: "app",
Attributes: []types.EventAttribute{
{Key: "creator", Value: "Cosmoshi", Index: true},
{Key: "key", Value: value, Index: true},
{Key: "index_key", Value: "index is working", Index: true},
{Key: "noindex_key", Value: "index is working", Index: false},
},
},
}

return types.ResponseDeliverTx{Code: code.CodeTypeOK, Events: events}
Expand Down Expand Up @@ -189,7 +205,70 @@ func (app *Application) Query(reqQuery types.RequestQuery) (resQuery types.Respo

func (app *Application) BeginBlock(req types.RequestBeginBlock) types.ResponseBeginBlock {
app.txToRemove = map[string]struct{}{}
return types.ResponseBeginBlock{}
response := types.ResponseBeginBlock{}

if !app.genBlockEvents {
return response
}

if app.state.Height%2 == 0 {
response = types.ResponseBeginBlock{
Events: []types.Event{
{
Type: "begin_event",
Attributes: []types.EventAttribute{
{
Key: "foo",
Value: "100",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use something derived from Height as values?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll clean this up a bit, you are right we could add the % condition in the event value. In principle, the only reason why I separated these two use cases is to make sure there are events that do not occur at every height.

Index: true,
},
{
Key: "bar",
Value: "200",
Index: true,
},
},
},
{
Type: "begin_event",
Attributes: []types.EventAttribute{
{
Key: "foo",
Value: "200",
Index: true,
},
{
Key: "bar",
Value: "300",
Index: true,
},
},
},
},
}
} else {
response = types.ResponseBeginBlock{
Events: []types.Event{
{
Type: "begin_event",
Attributes: []types.EventAttribute{
{
Key: "foo",
Value: "400",
Index: true,
},
{
Key: "bar",
Value: "300",
Index: true,
},
},
},
},
}
}

return response
}

func (app *Application) ProcessProposal(
Expand Down
4 changes: 4 additions & 0 deletions abci/example/kvstore/persistent_kvstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ func NewPersistentKVStoreApplication(dbDir string) *PersistentKVStoreApplication
}
}

func (app *PersistentKVStoreApplication) SetGenBlockEvents() {
app.app.genBlockEvents = true
}
jmalicevic marked this conversation as resolved.
Show resolved Hide resolved

func (app *PersistentKVStoreApplication) SetLogger(l log.Logger) {
app.logger = l
}
Expand Down
100 changes: 82 additions & 18 deletions docs/app-dev/indexing-transactions.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ order: 6

# Indexing Transactions

Tendermint allows you to index transactions and blocks and later query or
CometBFT allows you to index transactions and blocks and later query or
subscribe to their results. Transactions are indexed by `TxResult.Events` and
blocks are indexed by `Response(Begin|End)Block.Events`. However, transactions
are also indexed by a primary key which includes the transaction hash and maps
Expand All @@ -15,7 +15,7 @@ the block itself is never stored.
Each event contains a type and a list of attributes, which are key-value pairs
denoting something about what happened during the method's execution. For more
details on `Events`, see the
[ABCI](https://github.com/tendermint/tendermint/blob/v0.37.x/spec/abci/abci.md#events)
[ABCI](https://github.com/cometbft/cometbft/blob/v0.37.x/spec/abci/abci.md#events)
documentation.

An `Event` has a composite key associated with it. A `compositeKey` is
Expand All @@ -31,9 +31,12 @@ For example:

would be equal to the composite key of `jack.account.number`.

By default, Tendermint will index all transactions by their respective hashes
By default, CometBFT will index all transactions by their respective hashes
and height and blocks by their height.

CometBFT allows for different events within the same height to have
equal attributes.

## Configuration

Operators can configure indexing via the `[tx_index]` section. The `indexer`
Expand Down Expand Up @@ -62,23 +65,77 @@ be turned off regardless of other values provided.
#### KV

The `kv` indexer type is an embedded key-value store supported by the main
underlying Tendermint database. Using the `kv` indexer type allows you to query
for block and transaction events directly against Tendermint's RPC. However, the
underlying CometBFT database. Using the `kv` indexer type allows you to query
for block and transaction events directly against CometBFT's RPC. However, the
query syntax is limited and so this indexer type might be deprecated or removed
entirely in the future.

**Implementation and data layout**

The kv indexer stores each attribute of an event individually, by creating a composite key
with
- event type,
- attribute key,
- attribute value,
- event generator (e.g. `EndBlock` and `BeginBlock`)
- the height, and
- event counter.
For example the following events:

```
Type: "transfer",
Attributes: []abci.EventAttribute{
{Key: "sender", Value: "Bob", Index: true},
{Key: "recipient", Value: "Alice", Index: true},
{Key: "balance", Value: "100", Index: true},
{Key: "note", Value: "nothing", Index: true},
},

```

```
Type: "transfer",
Attributes: []abci.EventAttribute{
{Key: "sender", Value: "Tom", Index: true},
{Key: "recipient", Value: "Alice", Index: true},
{Key: "balance", Value: "200", Index: true},
{Key: "note", Value: "nothing", Index: true},
},
```

will be represented as follows in the store, assuming these events result from the EndBlock call for height 1:

```
Key value
---- event1 ------
transferSenderBobEndBlock11 1
transferRecipientAliceEndBlock11 1
transferBalance100EndBlock11 1
transferNodeNothingEndblock11 1
---- event2 ------
transferSenderTomEndBlock12 1
transferRecepientAliceEndBlock12 1
transferBalance200EndBlock12 1
transferNodeNothingEndblock12 1

```
The event number is a local variable kept by the indexer and incremented when a new event is processed.
It is an `int64` variable and has no other semantics besides being used to associate attributes belonging to the same events within a height.
This variable is not atomically incremented as event indexing is deterministic. **Should this ever change**, the event id generation
will be broken.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a problem with this PR.
Line 163 states that "TM does not expose functionality to define which events to index and which to ignore." but isn't the field "Index" in the event exactly for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm you are right, these two sentences at the beginning are really a bit contradicting each other:

"Applications are free to define which events to index. Tendermint does not
expose functionality to define which events to index and which to ignore."

The index field is indeed set by the application if the event is to be indexed. I don't know what the original author was referring to with "TM does not ..." .. I will edit this paragraph.

#### PostgreSQL

The `psql` indexer type allows an operator to enable block and transaction event
indexing by proxying it to an external PostgreSQL instance allowing for the events
to be stored in relational models. Since the events are stored in a RDBMS, operators
can leverage SQL to perform a series of rich and complex queries that are not
supported by the `kv` indexer type. Since operators can leverage SQL directly,
searching is not enabled for the `psql` indexer type via Tendermint's RPC -- any
searching is not enabled for the `psql` indexer type via CometBFT's RPC -- any
such query will fail.

Note, the SQL schema is stored in `state/indexer/sink/psql/schema.sql` and operators
must explicitly create the relations prior to starting Tendermint and enabling
must explicitly create the relations prior to starting CometBFT and enabling
the `psql` indexer type.

Example:
Expand All @@ -89,7 +146,7 @@ $ psql ... -f state/indexer/sink/psql/schema.sql

## Default Indexes

The Tendermint tx and block event indexer indexes a few select reserved events
The CometBFT tx and block event indexer indexes a few select reserved events
by default.

### Transactions
Expand All @@ -107,7 +164,7 @@ The following indexes are indexed by default:

## Adding Events

Applications are free to define which events to index. Tendermint does not
Applications are free to define which events to index. CometBFT does not
expose functionality to define which events to index and which to ignore. In
your application's `DeliverTx` method, add the `Events` field with pairs of
UTF-8 encoded strings (e.g. "transfer.sender": "Bob", "transfer.recipient":
Expand All @@ -122,10 +179,10 @@ func (app *KVStoreApplication) DeliverTx(req types.RequestDeliverTx) types.Resul
{
Type: "transfer",
Attributes: []abci.EventAttribute{
{Key: []byte("sender"), Value: []byte("Bob"), Index: true},
{Key: []byte("recipient"), Value: []byte("Alice"), Index: true},
{Key: []byte("balance"), Value: []byte("100"), Index: true},
{Key: []byte("note"), Value: []byte("nothing"), Index: true},
{Key: "sender ", Value: "Bob ", Index: true},
{Key: "recipient ", Value: "Alice ", Index: true},
{Key: "balance ", Value: "100 ", Index: true},
{Key: "note ", Value: "nothing ", Index: true},
},
},
}
Expand All @@ -146,7 +203,7 @@ You can query for a paginated set of transaction by their events by calling the
curl "localhost:26657/tx_search?query=\"message.sender='cosmos1...'\"&prove=true"
```

Check out [API docs](https://docs.tendermint.com/v0.37/rpc/#/Info/tx_search)
Check out [API docs](https://docs.cometbft.com/v0.37/rpc/#/Info/tx_search)
for more information on query syntax and other options.

## Subscribing to Transactions
Expand All @@ -165,10 +222,10 @@ a query to `/subscribe` RPC endpoint.
}
```

Check out [API docs](https://docs.tendermint.com/v0.37/rpc/#subscribe) for more information
Check out [API docs](https://docs.cometbft.com/v0.37/rpc/#subscribe) for more information
on query syntax and other options.

## Querying Blocks Events
## Querying Block Events

You can query for a paginated set of blocks by their events by calling the
`/block_search` RPC endpoint:
Expand All @@ -177,5 +234,12 @@ You can query for a paginated set of blocks by their events by calling the
curl "localhost:26657/block_search?query=\"block.height > 10 AND val_set.num_changed > 0\""
```

Check out [API docs](https://docs.tendermint.com/v0.37/rpc/#/Info/block_search)
for more information on query syntax and other options.
**Backwards compatibility**

Storing the event sequence was introduced in CometBFT 0.34.26. Before that, up until Tendermint Core 0.34.26,
the event sequence was not stored in the kvstore and events were stored only by height. That means that queries
returned blocks and transactions whose event attributes match within the height but can match across different
events on that height.
This behavior was fixed with CometBFT 0.34.26+. However, if the data was indexed with earlier versions of
Tendermint Core and not re-indexed, that data will be queried as if all the attributes within a height
occurred within the same event.
2 changes: 2 additions & 0 deletions rpc/client/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ func TestMain(m *testing.M) {
}

app := kvstore.NewPersistentKVStoreApplication(dir)
// If testing block event generation
lasarojc marked this conversation as resolved.
Show resolved Hide resolved
// app.SetGenBlockEvents() needs to be called here
node = rpctest.StartTendermint(app)

code := m.Run()
Expand Down
30 changes: 26 additions & 4 deletions rpc/client/rpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,27 @@ func TestTxSearchWithTimeout(t *testing.T) {
require.Greater(t, len(result.Txs), 0, "expected a lot of transactions")
}

// This test does nothing if we do not call app.SetGenBlockEvents() within main_test.go
// It will nevertheless pass as there are no events being generated.
func TestBlockSearch(t *testing.T) {
c := getHTTPClient()

// first we broadcast a few txs
for i := 0; i < 10; i++ {
_, _, tx := MakeTxKV()

_, err := c.BroadcastTxCommit(context.Background(), tx)
require.NoError(t, err)
}
require.NoError(t, client.WaitForHeight(c, 5, nil))
lasarojc marked this conversation as resolved.
Show resolved Hide resolved
// This cannot test match_events as it calls the client BlockSearch function directly
// It is the RPC request handler that processes the match_event
result, err := c.BlockSearch(context.Background(), "begin_event.foo = 100 AND begin_event.bar = 300", nil, nil, "asc")
require.NoError(t, err)
blockCount := len(result.Blocks)
require.Equal(t, blockCount, 0)

}
func TestTxSearch(t *testing.T) {
c := getHTTPClient()

Expand All @@ -536,8 +557,7 @@ func TestTxSearch(t *testing.T) {
find := result.Txs[len(result.Txs)-1]
anotherTxHash := types.Tx("a different tx").Hash()

for i, c := range GetClients() {
t.Logf("client %d", i)
for _, c := range GetClients() {

// now we query for the tx.
result, err := c.TxSearch(context.Background(), fmt.Sprintf("tx.hash='%v'", find.Hash), true, nil, nil, "asc")
Expand Down Expand Up @@ -616,16 +636,17 @@ func TestTxSearch(t *testing.T) {
pages = int(math.Ceil(float64(txCount) / float64(perPage)))
)

totalTx := 0
for page := 1; page <= pages; page++ {
page := page
result, err := c.TxSearch(context.Background(), "tx.height >= 1", false, &page, &perPage, "asc")
result, err := c.TxSearch(context.Background(), "tx.height >= 1", true, &page, &perPage, "asc")
require.NoError(t, err)
if page < pages {
require.Len(t, result.Txs, perPage)
} else {
require.LessOrEqual(t, len(result.Txs), perPage)
}
require.Equal(t, txCount, result.TotalCount)
totalTx = totalTx + len(result.Txs)
for _, tx := range result.Txs {
require.False(t, seen[tx.Height],
"Found duplicate height %v in page %v", tx.Height, page)
Expand All @@ -635,6 +656,7 @@ func TestTxSearch(t *testing.T) {
maxHeight = tx.Height
}
}
require.Equal(t, txCount, totalTx)
require.Len(t, seen, txCount)
}
}
Expand Down
Loading