You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

332 lines
9.3 KiB

abci: Synchronize FinalizeBlock with the updated specification (#7983) This change set implements the most recent version of `FinalizeBlock`. # What does this change actually contain? * This change set is rather large but fear not! The majority of the files touched and changes are renaming `ResponseDeliverTx` to `ExecTxResult`. This should be a pretty inoffensive change since they're effectively the same type but with a different name. * The `execBlockOnProxyApp` was totally removed since it served as just a wrapper around the logic that is now mostly encapsulated within `FinalizeBlock` * The `updateState` helper function has been made a public method on `State`. It was being exposed as a shim through the testing infrastructure, so this seemed innocuous. * Tests already existed to ensure that the application received the `ByzantineValidators` and the `ValidatorUpdates`, but one was fixed up to ensure that `LastCommitInfo` was being sent across. * Tests were removed from the `psql` indexer that seemed to search for an event in the indexer that was not being created. # Questions for reviewers * We store this [ABCIResponses](https://github.com/tendermint/tendermint/blob/5721a13ab1f4479f9807f449f0bf5c536b9a05f2/proto/tendermint/state/types.pb.go#L37) type in the data base as the block results. This type has changed since v0.35 to contain the `FinalizeBlock` response. I'm wondering if we need to do any shimming to keep the old data retrieveable? * Similarly, this change is exposed via the RPC through [ResultBlockResults](https://github.com/tendermint/tendermint/blob/5721a13ab1f4479f9807f449f0bf5c536b9a05f2/rpc/coretypes/responses.go#L69) changing. Should we somehow shim or notify for this change? closes: #7658
3 years ago
abci: Synchronize FinalizeBlock with the updated specification (#7983) This change set implements the most recent version of `FinalizeBlock`. # What does this change actually contain? * This change set is rather large but fear not! The majority of the files touched and changes are renaming `ResponseDeliverTx` to `ExecTxResult`. This should be a pretty inoffensive change since they're effectively the same type but with a different name. * The `execBlockOnProxyApp` was totally removed since it served as just a wrapper around the logic that is now mostly encapsulated within `FinalizeBlock` * The `updateState` helper function has been made a public method on `State`. It was being exposed as a shim through the testing infrastructure, so this seemed innocuous. * Tests already existed to ensure that the application received the `ByzantineValidators` and the `ValidatorUpdates`, but one was fixed up to ensure that `LastCommitInfo` was being sent across. * Tests were removed from the `psql` indexer that seemed to search for an event in the indexer that was not being created. # Questions for reviewers * We store this [ABCIResponses](https://github.com/tendermint/tendermint/blob/5721a13ab1f4479f9807f449f0bf5c536b9a05f2/proto/tendermint/state/types.pb.go#L37) type in the data base as the block results. This type has changed since v0.35 to contain the `FinalizeBlock` response. I'm wondering if we need to do any shimming to keep the old data retrieveable? * Similarly, this change is exposed via the RPC through [ResultBlockResults](https://github.com/tendermint/tendermint/blob/5721a13ab1f4479f9807f449f0bf5c536b9a05f2/rpc/coretypes/responses.go#L69) changing. Should we somehow shim or notify for this change? closes: #7658
3 years ago
  1. package psql
  2. import (
  3. "context"
  4. "database/sql"
  5. "flag"
  6. "fmt"
  7. "log"
  8. "os"
  9. "os/signal"
  10. "testing"
  11. "time"
  12. "github.com/adlio/schema"
  13. "github.com/gogo/protobuf/proto"
  14. "github.com/ory/dockertest"
  15. "github.com/ory/dockertest/docker"
  16. "github.com/stretchr/testify/assert"
  17. "github.com/stretchr/testify/require"
  18. abci "github.com/tendermint/tendermint/abci/types"
  19. "github.com/tendermint/tendermint/internal/state/indexer"
  20. "github.com/tendermint/tendermint/types"
  21. // Register the Postgres database driver.
  22. _ "github.com/lib/pq"
  23. )
  24. // Verify that the type satisfies the EventSink interface.
  25. var _ indexer.EventSink = (*EventSink)(nil)
  26. var (
  27. doPauseAtExit = flag.Bool("pause-at-exit", false,
  28. "If true, pause the test until interrupted at shutdown, to allow debugging")
  29. // A hook that test cases can call to obtain the shared database instance
  30. // used for testing the sink. This is initialized in TestMain (see below).
  31. testDB func() *sql.DB
  32. )
  33. const (
  34. user = "postgres"
  35. password = "secret"
  36. port = "5432"
  37. dsn = "postgres://%s:%s@localhost:%s/%s?sslmode=disable"
  38. dbName = "postgres"
  39. chainID = "test-chainID"
  40. viewTxEvents = "tx_events"
  41. )
  42. func TestMain(m *testing.M) {
  43. flag.Parse()
  44. // Set up docker.
  45. pool, err := dockertest.NewPool(os.Getenv("DOCKER_URL"))
  46. if err != nil {
  47. log.Fatalf("Creating docker pool: %v", err)
  48. }
  49. // If docker is unavailable, log and exit without reporting failure.
  50. if _, err := pool.Client.Info(); err != nil {
  51. log.Printf("WARNING: Docker is not available: %v [skipping this test]", err)
  52. return
  53. }
  54. // Start a container running PostgreSQL.
  55. resource, err := pool.RunWithOptions(&dockertest.RunOptions{
  56. Repository: "postgres",
  57. Tag: "13",
  58. Env: []string{
  59. "POSTGRES_USER=" + user,
  60. "POSTGRES_PASSWORD=" + password,
  61. "POSTGRES_DB=" + dbName,
  62. "listen_addresses = '*'",
  63. },
  64. ExposedPorts: []string{port},
  65. }, func(config *docker.HostConfig) {
  66. // set AutoRemove to true so that stopped container goes away by itself
  67. config.AutoRemove = true
  68. config.RestartPolicy = docker.RestartPolicy{
  69. Name: "no",
  70. }
  71. })
  72. if err != nil {
  73. log.Fatalf("Starting docker pool: %v", err)
  74. }
  75. if *doPauseAtExit {
  76. log.Print("Pause at exit is enabled, containers will not expire")
  77. } else {
  78. const expireSeconds = 60
  79. _ = resource.Expire(expireSeconds)
  80. log.Printf("Container expiration set to %d seconds", expireSeconds)
  81. }
  82. // Connect to the database, clear any leftover data, and install the
  83. // indexing schema.
  84. conn := fmt.Sprintf(dsn, user, password, resource.GetPort(port+"/tcp"), dbName)
  85. var db *sql.DB
  86. if err := pool.Retry(func() error {
  87. sink, err := NewEventSink(conn, chainID)
  88. if err != nil {
  89. return err
  90. }
  91. db = sink.DB() // set global for test use
  92. return db.Ping()
  93. }); err != nil {
  94. log.Fatalf("Connecting to database: %v", err)
  95. }
  96. if err := resetDatabase(db); err != nil {
  97. log.Fatalf("Flushing database: %v", err)
  98. }
  99. sm, err := readSchema()
  100. if err != nil {
  101. log.Fatalf("Reading schema: %v", err)
  102. }
  103. migrator := schema.NewMigrator()
  104. if err := migrator.Apply(db, sm); err != nil {
  105. log.Fatalf("Applying schema: %v", err)
  106. }
  107. // Set up the hook for tests to get the shared database handle.
  108. testDB = func() *sql.DB { return db }
  109. // Run the selected test cases.
  110. code := m.Run()
  111. // Clean up and shut down the database container.
  112. if *doPauseAtExit {
  113. log.Print("Testing complete, pausing for inspection. Send SIGINT to resume teardown")
  114. waitForInterrupt()
  115. log.Print("(resuming)")
  116. }
  117. log.Print("Shutting down database")
  118. if err := pool.Purge(resource); err != nil {
  119. log.Printf("WARNING: Purging pool failed: %v", err)
  120. }
  121. if err := db.Close(); err != nil {
  122. log.Printf("WARNING: Closing database failed: %v", err)
  123. }
  124. os.Exit(code)
  125. }
  126. func TestType(t *testing.T) {
  127. psqlSink := &EventSink{store: testDB(), chainID: chainID}
  128. assert.Equal(t, indexer.PSQL, psqlSink.Type())
  129. }
  130. func TestIndexing(t *testing.T) {
  131. ctx, cancel := context.WithCancel(context.Background())
  132. defer cancel()
  133. t.Run("IndexBlockEvents", func(t *testing.T) {
  134. indexer := &EventSink{store: testDB(), chainID: chainID}
  135. require.NoError(t, indexer.IndexBlockEvents(newTestBlockHeader()))
  136. verifyBlock(t, 1)
  137. verifyBlock(t, 2)
  138. verifyNotImplemented(t, "hasBlock", func() (bool, error) { return indexer.HasBlock(1) })
  139. verifyNotImplemented(t, "hasBlock", func() (bool, error) { return indexer.HasBlock(2) })
  140. verifyNotImplemented(t, "block search", func() (bool, error) {
  141. v, err := indexer.SearchBlockEvents(ctx, nil)
  142. return v != nil, err
  143. })
  144. require.NoError(t, verifyTimeStamp(tableBlocks))
  145. // Attempting to reindex the same events should gracefully succeed.
  146. require.NoError(t, indexer.IndexBlockEvents(newTestBlockHeader()))
  147. })
  148. t.Run("IndexTxEvents", func(t *testing.T) {
  149. indexer := &EventSink{store: testDB(), chainID: chainID}
  150. txResult := txResultWithEvents([]abci.Event{
  151. makeIndexedEvent("account.number", "1"),
  152. makeIndexedEvent("account.owner", "Ivan"),
  153. makeIndexedEvent("account.owner", "Yulieta"),
  154. {Type: "", Attributes: []abci.EventAttribute{{Key: "not_allowed", Value: "Vlad", Index: true}}},
  155. })
  156. require.NoError(t, indexer.IndexTxEvents([]*abci.TxResult{txResult}))
  157. txr, err := loadTxResult(types.Tx(txResult.Tx).Hash())
  158. require.NoError(t, err)
  159. assert.Equal(t, txResult, txr)
  160. require.NoError(t, verifyTimeStamp(tableTxResults))
  161. require.NoError(t, verifyTimeStamp(viewTxEvents))
  162. verifyNotImplemented(t, "getTxByHash", func() (bool, error) {
  163. txr, err := indexer.GetTxByHash(types.Tx(txResult.Tx).Hash())
  164. return txr != nil, err
  165. })
  166. verifyNotImplemented(t, "tx search", func() (bool, error) {
  167. txr, err := indexer.SearchTxEvents(ctx, nil)
  168. return txr != nil, err
  169. })
  170. // try to insert the duplicate tx events.
  171. err = indexer.IndexTxEvents([]*abci.TxResult{txResult})
  172. require.NoError(t, err)
  173. })
  174. }
  175. func TestStop(t *testing.T) {
  176. indexer := &EventSink{store: testDB()}
  177. require.NoError(t, indexer.Stop())
  178. }
  179. // newTestBlockHeader constructs a fresh copy of a block header containing
  180. // known test values to exercise the indexer.
  181. func newTestBlockHeader() types.EventDataNewBlockHeader {
  182. return types.EventDataNewBlockHeader{
  183. Header: types.Header{Height: 1},
  184. ResultFinalizeBlock: abci.ResponseFinalizeBlock{
  185. Events: []abci.Event{
  186. makeIndexedEvent("finalize_event.proposer", "FCAA001"),
  187. makeIndexedEvent("thingy.whatzit", "O.O"),
  188. makeIndexedEvent("my_event.foo", "100"),
  189. makeIndexedEvent("thingy.whatzit", "-.O"),
  190. },
  191. },
  192. }
  193. }
  194. // readSchema loads the indexing database schema file
  195. func readSchema() ([]*schema.Migration, error) {
  196. const filename = "schema.sql"
  197. contents, err := os.ReadFile(filename)
  198. if err != nil {
  199. return nil, fmt.Errorf("failed to read sql file from '%s': %w", filename, err)
  200. }
  201. return []*schema.Migration{{
  202. ID: time.Now().Local().String() + " db schema",
  203. Script: string(contents),
  204. }}, nil
  205. }
  206. // resetDB drops all the data from the test database.
  207. func resetDatabase(db *sql.DB) error {
  208. _, err := db.Exec(`DROP TABLE IF EXISTS blocks,tx_results,events,attributes CASCADE;`)
  209. if err != nil {
  210. return fmt.Errorf("dropping tables: %w", err)
  211. }
  212. _, err = db.Exec(`DROP VIEW IF EXISTS event_attributes,block_events,tx_events CASCADE;`)
  213. if err != nil {
  214. return fmt.Errorf("dropping views: %w", err)
  215. }
  216. return nil
  217. }
  218. // txResultWithEvents constructs a fresh transaction result with fixed values
  219. // for testing, that includes the specified events.
  220. func txResultWithEvents(events []abci.Event) *abci.TxResult {
  221. return &abci.TxResult{
  222. Height: 1,
  223. Index: 0,
  224. Tx: types.Tx("HELLO WORLD"),
  225. Result: abci.ExecTxResult{
  226. Data: []byte{0},
  227. Code: abci.CodeTypeOK,
  228. Log: "",
  229. Events: events,
  230. },
  231. }
  232. }
  233. func loadTxResult(hash []byte) (*abci.TxResult, error) {
  234. hashString := fmt.Sprintf("%X", hash)
  235. var resultData []byte
  236. if err := testDB().QueryRow(`
  237. SELECT tx_result FROM `+tableTxResults+` WHERE tx_hash = $1;
  238. `, hashString).Scan(&resultData); err != nil {
  239. return nil, fmt.Errorf("lookup transaction for hash %q failed: %v", hashString, err)
  240. }
  241. txr := new(abci.TxResult)
  242. if err := proto.Unmarshal(resultData, txr); err != nil {
  243. return nil, fmt.Errorf("unmarshaling txr: %w", err)
  244. }
  245. return txr, nil
  246. }
  247. func verifyTimeStamp(tableName string) error {
  248. return testDB().QueryRow(fmt.Sprintf(`
  249. SELECT DISTINCT %[1]s.created_at
  250. FROM %[1]s
  251. WHERE %[1]s.created_at >= $1;
  252. `, tableName), time.Now().Add(-2*time.Second)).Err()
  253. }
  254. func verifyBlock(t *testing.T, height int64) {
  255. // Check that the blocks table contains an entry for this height.
  256. if err := testDB().QueryRow(`
  257. SELECT height FROM `+tableBlocks+` WHERE height = $1;
  258. `, height).Err(); err == sql.ErrNoRows {
  259. t.Errorf("No block found for height=%d", height)
  260. } else if err != nil {
  261. t.Fatalf("Database query failed: %v", err)
  262. }
  263. }
  264. // verifyNotImplemented calls f and verifies that it returns both a
  265. // false-valued flag and a non-nil error whose string matching the expected
  266. // "not supported" message with label prefixed.
  267. func verifyNotImplemented(t *testing.T, label string, f func() (bool, error)) {
  268. t.Helper()
  269. t.Logf("Verifying that %q reports it is not implemented", label)
  270. want := label + " is not supported via the postgres event sink"
  271. ok, err := f()
  272. assert.False(t, ok)
  273. require.Error(t, err)
  274. assert.Equal(t, want, err.Error())
  275. }
  276. // waitForInterrupt blocks until a SIGINT is received by the process.
  277. func waitForInterrupt() {
  278. ch := make(chan os.Signal, 1)
  279. signal.Notify(ch, os.Interrupt)
  280. <-ch
  281. }