github.com/0xPolygon/supernets2-node@v0.0.0-20230711153321-2fe574524eaa/pool/pgpoolstorage/pgpoolstorage.go (about) 1 package pgpoolstorage 2 3 import ( 4 "context" 5 "database/sql" 6 "errors" 7 "time" 8 9 "github.com/0xPolygon/supernets2-node/db" 10 "github.com/0xPolygon/supernets2-node/hex" 11 "github.com/0xPolygon/supernets2-node/pool" 12 "github.com/0xPolygon/supernets2-node/state" 13 "github.com/ethereum/go-ethereum/common" 14 "github.com/ethereum/go-ethereum/core/types" 15 "github.com/jackc/pgx/v4" 16 "github.com/jackc/pgx/v4/pgxpool" 17 ) 18 19 // PostgresPoolStorage is an implementation of the Pool interface 20 // that uses a postgres database to store the data 21 type PostgresPoolStorage struct { 22 db *pgxpool.Pool 23 } 24 25 // NewPostgresPoolStorage creates and initializes an instance of PostgresPoolStorage 26 func NewPostgresPoolStorage(cfg db.Config) (*PostgresPoolStorage, error) { 27 poolDB, err := db.NewSQLDB(cfg) 28 if err != nil { 29 return nil, err 30 } 31 32 return &PostgresPoolStorage{ 33 db: poolDB, 34 }, nil 35 } 36 37 // AddTx adds a transaction to the pool table with the provided status 38 func (p *PostgresPoolStorage) AddTx(ctx context.Context, tx pool.Transaction) error { 39 hash := tx.Hash().Hex() 40 41 b, err := tx.MarshalBinary() 42 if err != nil { 43 return err 44 } 45 encoded := hex.EncodeToHex(b) 46 47 b, err = tx.MarshalJSON() 48 if err != nil { 49 return err 50 } 51 decoded := string(b) 52 53 gasPrice := tx.GasPrice().Uint64() 54 nonce := tx.Nonce() 55 56 sql := ` 57 INSERT INTO pool.transaction 58 ( 59 hash, 60 encoded, 61 decoded, 62 status, 63 gas_price, 64 nonce, 65 cumulative_gas_used, 66 used_keccak_hashes, 67 used_poseidon_hashes, 68 used_poseidon_paddings, 69 used_mem_aligns, 70 used_arithmetics, 71 used_binaries, 72 used_steps, 73 received_at, 74 from_address, 75 is_wip, 76 ip 77 ) 78 VALUES 79 ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) 80 ON CONFLICT (hash) DO UPDATE SET 81 encoded = $2, 82 decoded = $3, 83 status = $4, 84 gas_price = $5, 85 nonce = $6, 86 cumulative_gas_used = $7, 87 used_keccak_hashes = $8, 88 used_poseidon_hashes = $9, 89 used_poseidon_paddings = $10, 90 used_mem_aligns = $11, 91 used_arithmetics = $12, 92 used_binaries = $13, 93 used_steps = $14, 94 received_at = $15, 95 from_address = $16, 96 is_wip = $17, 97 ip = $18 98 ` 99 100 // Get FromAddress from the JSON data 101 data, err := state.GetSender(tx.Transaction) 102 if err != nil { 103 return err 104 } 105 fromAddress := data.String() 106 107 if _, err := p.db.Exec(ctx, sql, 108 hash, 109 encoded, 110 decoded, 111 tx.Status, 112 gasPrice, 113 nonce, 114 tx.CumulativeGasUsed, 115 tx.UsedKeccakHashes, 116 tx.UsedPoseidonHashes, 117 tx.UsedPoseidonPaddings, 118 tx.UsedMemAligns, 119 tx.UsedArithmetics, 120 tx.UsedBinaries, 121 tx.UsedSteps, 122 tx.ReceivedAt, 123 fromAddress, 124 tx.IsWIP, 125 tx.IP); err != nil { 126 return err 127 } 128 return nil 129 } 130 131 // GetTxsByStatus returns an array of transactions filtered by status 132 // limit parameter is used to limit amount txs from the db, 133 // if limit = 0, then there is no limit 134 func (p *PostgresPoolStorage) GetTxsByStatus(ctx context.Context, status pool.TxStatus, limit uint64) ([]pool.Transaction, error) { 135 var ( 136 rows pgx.Rows 137 err error 138 sql string 139 ) 140 if limit == 0 { 141 sql = "SELECT encoded, status, received_at, is_wip, ip, failed_reason FROM pool.transaction WHERE status = $1 ORDER BY gas_price DESC" 142 rows, err = p.db.Query(ctx, sql, status.String()) 143 } else { 144 sql = "SELECT encoded, status, received_at, is_wip, ip, failed_reason FROM pool.transaction WHERE status = $1 ORDER BY gas_price DESC LIMIT $2" 145 rows, err = p.db.Query(ctx, sql, status.String(), limit) 146 } 147 if err != nil { 148 return nil, err 149 } 150 defer rows.Close() 151 152 txs := make([]pool.Transaction, 0, len(rows.RawValues())) 153 for rows.Next() { 154 tx, err := scanTx(rows) 155 if err != nil { 156 return nil, err 157 } 158 txs = append(txs, *tx) 159 } 160 161 return txs, nil 162 } 163 164 // GetNonWIPTxsByStatus returns an array of transactions filtered by status 165 // limit parameter is used to limit amount txs from the db, 166 // if limit = 0, then there is no limit 167 func (p *PostgresPoolStorage) GetNonWIPTxsByStatus(ctx context.Context, status pool.TxStatus, limit uint64) ([]pool.Transaction, error) { 168 var ( 169 rows pgx.Rows 170 err error 171 sql string 172 ) 173 if limit == 0 { 174 sql = "SELECT encoded, status, received_at, is_wip, ip, failed_reason FROM pool.transaction WHERE is_wip IS FALSE and status = $1 ORDER BY gas_price DESC" 175 rows, err = p.db.Query(ctx, sql, status.String()) 176 } else { 177 sql = "SELECT encoded, status, received_at, is_wip, ip, failed_reason FROM pool.transaction WHERE is_wip IS FALSE and status = $1 ORDER BY gas_price DESC LIMIT $2" 178 rows, err = p.db.Query(ctx, sql, status.String(), limit) 179 } 180 if err != nil { 181 return nil, err 182 } 183 defer rows.Close() 184 185 txs := make([]pool.Transaction, 0, len(rows.RawValues())) 186 for rows.Next() { 187 tx, err := scanTx(rows) 188 if err != nil { 189 return nil, err 190 } 191 txs = append(txs, *tx) 192 } 193 194 return txs, nil 195 } 196 197 // GetPendingTxHashesSince returns the pending tx since the given time. 198 func (p *PostgresPoolStorage) GetPendingTxHashesSince(ctx context.Context, since time.Time) ([]common.Hash, error) { 199 sql := "SELECT hash FROM pool.transaction WHERE status = $1 AND received_at >= $2" 200 rows, err := p.db.Query(ctx, sql, pool.TxStatusPending, since) 201 if err != nil { 202 return nil, err 203 } 204 defer rows.Close() 205 206 hashes := make([]common.Hash, 0, len(rows.RawValues())) 207 for rows.Next() { 208 var hash string 209 if err := rows.Scan(&hash); err != nil { 210 return nil, err 211 } 212 hashes = append(hashes, common.HexToHash(hash)) 213 } 214 215 return hashes, nil 216 } 217 218 // GetTxs gets txs with the lowest nonce 219 func (p *PostgresPoolStorage) GetTxs(ctx context.Context, filterStatus pool.TxStatus, minGasPrice, limit uint64) ([]*pool.Transaction, error) { 220 query := ` 221 SELECT 222 encoded, 223 status, 224 cumulative_gas_used, 225 used_keccak_hashes, 226 used_poseidon_hashes, 227 used_poseidon_paddings, 228 used_mem_aligns, 229 used_arithmetics, 230 used_binaries, 231 used_steps, 232 received_at, 233 nonce, 234 is_wip, 235 ip 236 FROM 237 pool.transaction p1 238 WHERE 239 status = $1 AND 240 gas_price >= $2 241 ORDER BY 242 nonce ASC 243 LIMIT $3 244 ` 245 246 if filterStatus == pool.TxStatusFailed { 247 query = ` 248 SELECT * FROM ( 249 SELECT 250 encoded, 251 status, 252 cumulative_gas_used, 253 used_keccak_hashes, 254 used_poseidon_hashes, 255 used_poseidon_paddings, 256 used_mem_aligns, 257 used_arithmetics, 258 used_binaries, 259 used_steps, 260 received_at, 261 nonce, 262 is_wip, 263 ip 264 FROM 265 pool.transaction p1 266 WHERE 267 status = $1 AND 268 gas_price >= $2 269 ORDER BY 270 nonce ASC 271 LIMIT $3 272 ) as tmp 273 ORDER BY nonce ASC 274 ` 275 } 276 277 var ( 278 encoded, status, ip string 279 receivedAt time.Time 280 cumulativeGasUsed uint64 281 282 usedKeccakHashes, usedPoseidonHashes, usedPoseidonPaddings, 283 usedMemAligns, usedArithmetics, usedBinaries, usedSteps uint32 284 nonce uint64 285 isWIP bool 286 ) 287 288 args := []interface{}{filterStatus, minGasPrice, limit} 289 290 rows, err := p.db.Query(ctx, query, args...) 291 if errors.Is(err, pgx.ErrNoRows) { 292 return nil, pool.ErrNotFound 293 } else if err != nil { 294 return nil, err 295 } 296 297 txs := make([]*pool.Transaction, 0, len(rows.RawValues())) 298 for rows.Next() { 299 err := rows.Scan( 300 &encoded, 301 &status, 302 &cumulativeGasUsed, 303 &usedKeccakHashes, 304 &usedPoseidonHashes, 305 &usedPoseidonPaddings, 306 &usedMemAligns, 307 &usedArithmetics, 308 &usedBinaries, 309 &usedSteps, 310 &receivedAt, 311 &nonce, 312 &isWIP, 313 &ip, 314 ) 315 316 if err != nil { 317 return nil, err 318 } 319 tx := new(pool.Transaction) 320 b, err := hex.DecodeHex(encoded) 321 if err != nil { 322 return nil, err 323 } 324 if err := tx.UnmarshalBinary(b); err != nil { 325 return nil, err 326 } 327 328 tx.Status = pool.TxStatus(status) 329 tx.ReceivedAt = receivedAt 330 tx.ZKCounters = state.ZKCounters{ 331 CumulativeGasUsed: cumulativeGasUsed, 332 UsedKeccakHashes: usedKeccakHashes, 333 UsedPoseidonHashes: usedPoseidonHashes, 334 UsedPoseidonPaddings: usedPoseidonPaddings, 335 UsedMemAligns: usedMemAligns, 336 UsedArithmetics: usedArithmetics, 337 UsedBinaries: usedBinaries, 338 UsedSteps: usedSteps, 339 } 340 tx.IsWIP = isWIP 341 tx.IP = ip 342 343 txs = append(txs, tx) 344 } 345 346 return txs, nil 347 } 348 349 // CountTransactionsByStatus get number of transactions 350 // accordingly to the provided status 351 func (p *PostgresPoolStorage) CountTransactionsByStatus(ctx context.Context, status pool.TxStatus) (uint64, error) { 352 sql := "SELECT COUNT(*) FROM pool.transaction WHERE status = $1" 353 var counter uint64 354 err := p.db.QueryRow(ctx, sql, status.String()).Scan(&counter) 355 if err != nil { 356 return 0, err 357 } 358 return counter, nil 359 } 360 361 // UpdateTxStatus updates a transaction status accordingly to the 362 // provided status and hash 363 func (p *PostgresPoolStorage) UpdateTxStatus(ctx context.Context, updateInfo pool.TxStatusUpdateInfo) error { 364 sql := "UPDATE pool.transaction SET status = $1, is_wip = $2" 365 args := []interface{}{updateInfo.NewStatus, updateInfo.IsWIP} 366 367 if updateInfo.FailedReason != nil { 368 sql += ", failed_reason = $3" 369 args = append(args, *updateInfo.FailedReason) 370 sql += " WHERE hash = $4" 371 } else { 372 sql += " WHERE hash = $3" 373 } 374 375 args = append(args, updateInfo.Hash.Hex()) 376 377 if _, err := p.db.Exec(ctx, sql, args...); err != nil { 378 return err 379 } 380 381 return nil 382 } 383 384 // UpdateTxsStatus updates transactions status accordingly to the provided status and hashes 385 func (p *PostgresPoolStorage) UpdateTxsStatus(ctx context.Context, updateInfos []pool.TxStatusUpdateInfo) error { 386 for _, updateInfo := range updateInfos { 387 if err := p.UpdateTxStatus(ctx, updateInfo); err != nil { 388 return err 389 } 390 } 391 return nil 392 } 393 394 // DeleteTransactionsByHashes deletes txs by their hashes 395 func (p *PostgresPoolStorage) DeleteTransactionsByHashes(ctx context.Context, hashes []common.Hash) error { 396 hh := make([]string, 0, len(hashes)) 397 for _, h := range hashes { 398 hh = append(hh, h.Hex()) 399 } 400 401 query := "DELETE FROM pool.transaction WHERE hash = ANY ($1)" 402 if _, err := p.db.Exec(ctx, query, hh); err != nil { 403 return err 404 } 405 return nil 406 } 407 408 // SetGasPrice allows an external component to define the gas price 409 func (p *PostgresPoolStorage) SetGasPrice(ctx context.Context, gasPrice uint64) error { 410 sql := "INSERT INTO pool.gas_price (price, timestamp) VALUES ($1, $2)" 411 if _, err := p.db.Exec(ctx, sql, gasPrice, time.Now().UTC()); err != nil { 412 return err 413 } 414 return nil 415 } 416 417 // GetGasPrice returns the current gas price 418 func (p *PostgresPoolStorage) GetGasPrice(ctx context.Context) (uint64, error) { 419 sql := "SELECT price FROM pool.gas_price ORDER BY item_id DESC LIMIT 1" 420 rows, err := p.db.Query(ctx, sql) 421 if errors.Is(err, pgx.ErrNoRows) { 422 return 0, state.ErrNotFound 423 } else if err != nil { 424 return 0, err 425 } 426 427 defer rows.Close() 428 429 gasPrice := uint64(0) 430 431 for rows.Next() { 432 err := rows.Scan(&gasPrice) 433 if err != nil { 434 return 0, err 435 } 436 } 437 438 return gasPrice, nil 439 } 440 441 // MinGasPriceSince returns the min gas price after given timestamp 442 func (p *PostgresPoolStorage) MinGasPriceSince(ctx context.Context, timestamp time.Time) (uint64, error) { 443 sql := "SELECT COALESCE(MIN(price), 0) FROM pool.gas_price WHERE \"timestamp\" >= $1 LIMIT 1" 444 var gasPrice uint64 445 err := p.db.QueryRow(ctx, sql, timestamp).Scan(&gasPrice) 446 if gasPrice == 0 || errors.Is(err, pgx.ErrNoRows) { 447 return 0, state.ErrNotFound 448 } else if err != nil { 449 return 0, err 450 } 451 452 return gasPrice, nil 453 } 454 455 // IsTxPending determines if the tx associated to the given hash is pending or 456 // not. 457 func (p *PostgresPoolStorage) IsTxPending(ctx context.Context, hash common.Hash) (bool, error) { 458 var exists bool 459 req := "SELECT exists (SELECT 1 FROM pool.transaction WHERE hash = $1 AND status = $2)" 460 err := p.db.QueryRow(ctx, req, hash.Hex(), pool.TxStatusPending).Scan(&exists) 461 if err != nil && err != sql.ErrNoRows { 462 return false, err 463 } 464 465 return exists, nil 466 } 467 468 // GetTxsByFromAndNonce get all the transactions from the pool with the same from and nonce 469 func (p *PostgresPoolStorage) GetTxsByFromAndNonce(ctx context.Context, from common.Address, nonce uint64) ([]pool.Transaction, error) { 470 sql := `SELECT encoded, status, received_at, is_wip, ip, failed_reason 471 FROM pool.transaction 472 WHERE from_address = $1 473 AND nonce = $2` 474 rows, err := p.db.Query(ctx, sql, from.String(), nonce) 475 if errors.Is(err, pgx.ErrNoRows) { 476 return nil, nil 477 } else if err != nil { 478 return nil, err 479 } 480 defer rows.Close() 481 482 txs := make([]pool.Transaction, 0, len(rows.RawValues())) 483 for rows.Next() { 484 tx, err := scanTx(rows) 485 if err != nil { 486 return nil, err 487 } 488 txs = append(txs, *tx) 489 } 490 491 return txs, nil 492 } 493 494 // GetTxFromAddressFromByHash gets tx from address by hash 495 func (p *PostgresPoolStorage) GetTxFromAddressFromByHash(ctx context.Context, hash common.Hash) (common.Address, uint64, error) { 496 query := `SELECT from_address, nonce 497 FROM pool.transaction 498 WHERE hash = $1 499 ` 500 501 var ( 502 fromAddr string 503 nonce uint64 504 ) 505 err := p.db.QueryRow(ctx, query, hash.String()).Scan(&fromAddr, &nonce) 506 if err != nil { 507 return common.Address{}, 0, err 508 } 509 510 return common.HexToAddress(fromAddr), nonce, nil 511 } 512 513 // GetNonce gets the nonce to the provided address accordingly to the txs in the pool 514 func (p *PostgresPoolStorage) GetNonce(ctx context.Context, address common.Address) (uint64, error) { 515 sql := `SELECT MAX(nonce) 516 FROM pool.transaction 517 WHERE from_address = $1 518 AND status IN ($2, $3)` 519 rows, err := p.db.Query(ctx, sql, address.String(), pool.TxStatusPending, pool.TxStatusSelected) 520 if errors.Is(err, pgx.ErrNoRows) { 521 return 0, nil 522 } else if err != nil { 523 return 0, err 524 } 525 defer rows.Close() 526 527 var nonce *uint64 528 for rows.Next() { 529 err := rows.Scan(&nonce) 530 if err != nil { 531 return 0, err 532 } else if rows.Err() != nil { 533 return 0, rows.Err() 534 } 535 } 536 537 if nonce == nil { 538 n := uint64(0) 539 nonce = &n 540 } else { 541 n := *nonce + 1 542 nonce = &n 543 } 544 545 return *nonce, nil 546 } 547 548 // GetTxByHash gets a transaction in the pool by its hash 549 func (p *PostgresPoolStorage) GetTxByHash(ctx context.Context, hash common.Hash) (*pool.Transaction, error) { 550 var ( 551 encoded, status, ip string 552 receivedAt time.Time 553 isWIP bool 554 ) 555 556 sql := `SELECT encoded, status, received_at, is_wip, ip 557 FROM pool.transaction 558 WHERE hash = $1` 559 err := p.db.QueryRow(ctx, sql, hash.String()).Scan(&encoded, &status, &receivedAt, &isWIP, &ip) 560 if errors.Is(err, pgx.ErrNoRows) { 561 return nil, pool.ErrNotFound 562 } else if err != nil { 563 return nil, err 564 } 565 566 b, err := hex.DecodeHex(encoded) 567 if err != nil { 568 return nil, err 569 } 570 571 tx := new(types.Transaction) 572 if err := tx.UnmarshalBinary(b); err != nil { 573 return nil, err 574 } 575 576 poolTx := &pool.Transaction{ 577 ReceivedAt: receivedAt, 578 Status: pool.TxStatus(status), 579 Transaction: *tx, 580 IsWIP: isWIP, 581 IP: ip, 582 } 583 584 return poolTx, nil 585 } 586 587 func scanTx(rows pgx.Rows) (*pool.Transaction, error) { 588 var ( 589 encoded, status, ip string 590 receivedAt time.Time 591 isWIP bool 592 failedReason *string 593 ) 594 595 if err := rows.Scan(&encoded, &status, &receivedAt, &isWIP, &ip, &failedReason); err != nil { 596 return nil, err 597 } 598 599 tx := new(pool.Transaction) 600 601 b, err := hex.DecodeHex(encoded) 602 if err != nil { 603 return nil, err 604 } 605 606 if err := tx.UnmarshalBinary(b); err != nil { 607 return nil, err 608 } 609 610 tx.Status = pool.TxStatus(status) 611 tx.ReceivedAt = receivedAt 612 tx.IsWIP = isWIP 613 tx.IP = ip 614 tx.FailedReason = failedReason 615 616 return tx, nil 617 } 618 619 // DeleteTransactionByHash deletes tx by its hash 620 func (p *PostgresPoolStorage) DeleteTransactionByHash(ctx context.Context, hash common.Hash) error { 621 query := "DELETE FROM pool.transaction WHERE hash = $1" 622 if _, err := p.db.Exec(ctx, query, hash); err != nil { 623 return err 624 } 625 return nil 626 } 627 628 // GetTxZkCountersByHash gets a transaction zkcounters by its hash 629 func (p *PostgresPoolStorage) GetTxZkCountersByHash(ctx context.Context, hash common.Hash) (*state.ZKCounters, error) { 630 var zkCounters state.ZKCounters 631 632 sql := `SELECT cumulative_gas_used, used_keccak_hashes, used_poseidon_hashes, used_poseidon_paddings, used_mem_aligns, 633 used_arithmetics, used_binaries, used_steps FROM pool.transaction WHERE hash = $1` 634 err := p.db.QueryRow(ctx, sql, hash.String()).Scan(&zkCounters.CumulativeGasUsed, &zkCounters.UsedKeccakHashes, 635 &zkCounters.UsedPoseidonHashes, &zkCounters.UsedPoseidonPaddings, 636 &zkCounters.UsedMemAligns, &zkCounters.UsedArithmetics, &zkCounters.UsedBinaries, &zkCounters.UsedSteps) 637 if errors.Is(err, pgx.ErrNoRows) { 638 return nil, pool.ErrNotFound 639 } else if err != nil { 640 return nil, err 641 } 642 643 return &zkCounters, nil 644 } 645 646 // MarkWIPTxsAsPending updates WIP status to non WIP 647 func (p *PostgresPoolStorage) MarkWIPTxsAsPending(ctx context.Context) error { 648 const query = `UPDATE pool.transaction SET is_wip = false WHERE is_wip = true` 649 if _, err := p.db.Exec(ctx, query); err != nil { 650 return err 651 } 652 return nil 653 } 654 655 // UpdateTxWIPStatus updates a transaction wip status accordingly to the 656 // provided WIP status and hash 657 func (p *PostgresPoolStorage) UpdateTxWIPStatus(ctx context.Context, hash common.Hash, isWIP bool) error { 658 sql := "UPDATE pool.transaction SET is_wip = $1 WHERE hash = $2" 659 if _, err := p.db.Exec(ctx, sql, isWIP, hash.Hex()); err != nil { 660 return err 661 } 662 return nil 663 } 664 665 // GetAllAddressesBlocked get all addresses blocked 666 func (p *PostgresPoolStorage) GetAllAddressesBlocked(ctx context.Context) ([]common.Address, error) { 667 sql := `SELECT addr FROM pool.blocked` 668 669 rows, err := p.db.Query(ctx, sql) 670 if err != nil { 671 if errors.Is(err, pgx.ErrNoRows) { 672 return nil, nil 673 } else { 674 return nil, err 675 } 676 } 677 defer rows.Close() 678 679 var addrs []common.Address 680 for rows.Next() { 681 var addr string 682 err := rows.Scan(&addr) 683 if err != nil { 684 return nil, err 685 } 686 addrs = append(addrs, common.HexToAddress(addr)) 687 } 688 689 return addrs, nil 690 }