github.com/Team-Kujira/tendermint@v0.34.24-indexer/state/indexer/sink/psql/psql.go (about) 1 // Package psql implements an event sink backed by a PostgreSQL database. 2 package psql 3 4 import ( 5 "context" 6 "database/sql" 7 "errors" 8 "fmt" 9 "strings" 10 "time" 11 12 "github.com/gogo/protobuf/proto" 13 14 abci "github.com/tendermint/tendermint/abci/types" 15 "github.com/tendermint/tendermint/libs/pubsub/query" 16 "github.com/tendermint/tendermint/types" 17 ) 18 19 const ( 20 tableBlocks = "blocks" 21 tableTxResults = "tx_results" 22 tableEvents = "events" 23 tableAttributes = "attributes" 24 driverName = "postgres" 25 ) 26 27 // EventSink is an indexer backend providing the tx/block index services. This 28 // implementation stores records in a PostgreSQL database using the schema 29 // defined in state/indexer/sink/psql/schema.sql. 30 type EventSink struct { 31 store *sql.DB 32 chainID string 33 } 34 35 // NewEventSink constructs an event sink associated with the PostgreSQL 36 // database specified by connStr. Events written to the sink are attributed to 37 // the specified chainID. 38 func NewEventSink(connStr, chainID string) (*EventSink, error) { 39 db, err := sql.Open(driverName, connStr) 40 if err != nil { 41 return nil, err 42 } 43 44 return &EventSink{ 45 store: db, 46 chainID: chainID, 47 }, nil 48 } 49 50 // DB returns the underlying Postgres connection used by the sink. 51 // This is exported to support testing. 52 func (es *EventSink) DB() *sql.DB { return es.store } 53 54 // runInTransaction executes query in a fresh database transaction. 55 // If query reports an error, the transaction is rolled back and the 56 // error from query is reported to the caller. 57 // Otherwise, the result of committing the transaction is returned. 58 func runInTransaction(db *sql.DB, query func(*sql.Tx) error) error { 59 dbtx, err := db.Begin() 60 if err != nil { 61 return err 62 } 63 if err := query(dbtx); err != nil { 64 _ = dbtx.Rollback() // report the initial error, not the rollback 65 return err 66 } 67 return dbtx.Commit() 68 } 69 70 // queryWithID executes the specified SQL query with the given arguments, 71 // expecting a single-row, single-column result containing an ID. If the query 72 // succeeds, the ID from the result is returned. 73 func queryWithID(tx *sql.Tx, query string, args ...interface{}) (uint32, error) { 74 var id uint32 75 if err := tx.QueryRow(query, args...).Scan(&id); err != nil { 76 return 0, err 77 } 78 return id, nil 79 } 80 81 // insertEvents inserts a slice of events and any indexed attributes of those 82 // events into the database associated with dbtx. 83 // 84 // If txID > 0, the event is attributed to the Tendermint transaction with that 85 // ID; otherwise it is recorded as a block event. 86 func insertEvents(dbtx *sql.Tx, blockID, txID uint32, evts []abci.Event) error { 87 // Populate the transaction ID field iff one is defined (> 0). 88 var txIDArg interface{} 89 if txID > 0 { 90 txIDArg = txID 91 } 92 93 // Add each event to the events table, and retrieve its row ID to use when 94 // adding any attributes the event provides. 95 for idx, evt := range evts { 96 // Skip events with an empty type. 97 if evt.Type == "" { 98 continue 99 } 100 101 if evt.Type == "rewards" { 102 continue 103 } 104 105 if evt.Type == "commission" { 106 continue 107 } 108 109 eid, err := queryWithID(dbtx, ` 110 INSERT INTO `+tableEvents+` (block_id, tx_id, type) VALUES ($1, $2, $3) 111 RETURNING rowid; 112 `, blockID, txIDArg, evt.Type) 113 if err != nil { 114 return err 115 } 116 117 // Add any attributes flagged for indexing. 118 for _, attr := range evt.Attributes { 119 if !attr.Index { 120 continue 121 } 122 compositeKey := evt.Type + "." + string(attr.Key) 123 if _, err := dbtx.Exec(` 124 INSERT INTO `+tableAttributes+` (tx_id, event_id, idx, key, composite_key, value) 125 VALUES ($1, $2, $3, $4, $5, $6); 126 `, txIDArg, eid, idx, attr.Key, compositeKey, attr.Value); err != nil { 127 return err 128 } 129 } 130 } 131 return nil 132 } 133 134 // makeIndexedEvent constructs an event from the specified composite key and 135 // value. If the key has the form "type.name", the event will have a single 136 // attribute with that name and the value; otherwise the event will have only 137 // a type and no attributes. 138 func makeIndexedEvent(compositeKey, value string) abci.Event { 139 i := strings.Index(compositeKey, ".") 140 if i < 0 { 141 return abci.Event{Type: compositeKey} 142 } 143 return abci.Event{Type: compositeKey[:i], Attributes: []abci.EventAttribute{ 144 {Key: []byte(compositeKey[i+1:]), Value: []byte(value), Index: true}, 145 }} 146 } 147 148 // IndexBlockEvents indexes the specified block header, part of the 149 // indexer.EventSink interface. 150 func (es *EventSink) IndexBlockEvents(h types.EventDataNewBlockHeader) error { 151 ts := time.Now().UTC() 152 153 return runInTransaction(es.store, func(dbtx *sql.Tx) error { 154 // Add the block to the blocks table and report back its row ID for use 155 // in indexing the events for the block. 156 blockID, err := queryWithID(dbtx, ` 157 INSERT INTO `+tableBlocks+` (height, chain_id, created_at) 158 VALUES ($1, $2, $3) 159 ON CONFLICT DO NOTHING 160 RETURNING rowid; 161 `, h.Header.Height, es.chainID, ts) 162 if err == sql.ErrNoRows { 163 return nil // we already saw this block; quietly succeed 164 } else if err != nil { 165 return fmt.Errorf("indexing block header: %w", err) 166 } 167 168 // Insert the special block meta-event for height. 169 if err := insertEvents(dbtx, blockID, 0, []abci.Event{ 170 makeIndexedEvent(types.BlockHeightKey, fmt.Sprint(h.Header.Height)), 171 }); err != nil { 172 return fmt.Errorf("block meta-events: %w", err) 173 } 174 // Insert all the block events. Order is important here, 175 if err := insertEvents(dbtx, blockID, 0, h.ResultBeginBlock.Events); err != nil { 176 return fmt.Errorf("begin-block events: %w", err) 177 } 178 if err := insertEvents(dbtx, blockID, 0, h.ResultEndBlock.Events); err != nil { 179 return fmt.Errorf("end-block events: %w", err) 180 } 181 return nil 182 }) 183 } 184 185 func (es *EventSink) IndexTxEvents(txrs []*abci.TxResult) error { 186 ts := time.Now().UTC() 187 188 for _, txr := range txrs { 189 // Encode the result message in protobuf wire format for indexing. 190 resultData, err := proto.Marshal(txr) 191 if err != nil { 192 return fmt.Errorf("marshaling tx_result: %w", err) 193 } 194 195 // Index the hash of the underlying transaction as a hex string. 196 txHash := fmt.Sprintf("%X", types.Tx(txr.Tx).Hash()) 197 198 if err := runInTransaction(es.store, func(dbtx *sql.Tx) error { 199 // Find the block associated with this transaction. The block header 200 // must have been indexed prior to the transactions belonging to it. 201 blockID, err := queryWithID(dbtx, ` 202 SELECT rowid FROM `+tableBlocks+` WHERE height = $1 AND chain_id = $2; 203 `, txr.Height, es.chainID) 204 if err != nil { 205 return fmt.Errorf("finding block ID: %w", err) 206 } 207 208 // Insert a record for this tx_result and capture its ID for indexing events. 209 txID, err := queryWithID(dbtx, ` 210 INSERT INTO `+tableTxResults+` (block_id, index, created_at, tx_hash, tx_result) 211 VALUES ($1, $2, $3, $4, $5) 212 ON CONFLICT DO NOTHING 213 RETURNING rowid; 214 `, blockID, txr.Index, ts, txHash, resultData) 215 if err == sql.ErrNoRows { 216 return nil // we already saw this transaction; quietly succeed 217 } else if err != nil { 218 return fmt.Errorf("indexing tx_result: %w", err) 219 } 220 221 // Insert the special transaction meta-events for hash and height. 222 if err := insertEvents(dbtx, blockID, txID, []abci.Event{ 223 makeIndexedEvent(types.TxHashKey, txHash), 224 makeIndexedEvent(types.TxHeightKey, fmt.Sprint(txr.Height)), 225 }); err != nil { 226 return fmt.Errorf("indexing transaction meta-events: %w", err) 227 } 228 // Index any events packaged with the transaction. 229 if err := insertEvents(dbtx, blockID, txID, txr.Result.Events); err != nil { 230 return fmt.Errorf("indexing transaction events: %w", err) 231 } 232 return nil 233 234 }); err != nil { 235 return err 236 } 237 } 238 return nil 239 } 240 241 // SearchBlockEvents is not implemented by this sink, and reports an error for all queries. 242 func (es *EventSink) SearchBlockEvents(ctx context.Context, q *query.Query) ([]int64, error) { 243 return nil, errors.New("block search is not supported via the postgres event sink") 244 } 245 246 // SearchTxEvents is not implemented by this sink, and reports an error for all queries. 247 func (es *EventSink) SearchTxEvents(ctx context.Context, q *query.Query) ([]*abci.TxResult, error) { 248 return nil, errors.New("tx search is not supported via the postgres event sink") 249 } 250 251 // GetTxByHash is not implemented by this sink, and reports an error for all queries. 252 func (es *EventSink) GetTxByHash(hash []byte) (*abci.TxResult, error) { 253 return nil, errors.New("getTxByHash is not supported via the postgres event sink") 254 } 255 256 // HasBlock is not implemented by this sink, and reports an error for all queries. 257 func (es *EventSink) HasBlock(h int64) (bool, error) { 258 return false, errors.New("hasBlock is not supported via the postgres event sink") 259 } 260 261 // Stop closes the underlying PostgreSQL database. 262 func (es *EventSink) Stop() error { return es.store.Close() }