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

Store transactions #2

Merged
merged 22 commits into from
Jan 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7acd3b2
store transactions and their arbitrary data
chris124567 Jan 9, 2024
ab66e8d
add transaction method to store
chris124567 Jan 9, 2024
bc1b19d
add basic transaction API endpoint
chris124567 Jan 9, 2024
25d6a0a
add bulk transactions endpoint
chris124567 Jan 9, 2024
c438474
add context to transactionByID errors
chris124567 Jan 10, 2024
25fe2ab
make max number of ids a variable in explorerTransactionsHandler
chris124567 Jan 10, 2024
eba29ee
/explorer/transactions/:id -> /explorer/transactions/id/:id
chris124567 Jan 10, 2024
77f4fa3
don't use additional scoping in transactionByID
chris124567 Jan 10, 2024
d1f9d8e
add error context to addTransactions
chris124567 Jan 10, 2024
6125969
rename to dbTxn
chris124567 Jan 10, 2024
1841725
don't use named return variables in querying functions
chris124567 Jan 10, 2024
dade90e
use prepared statements for inserts
chris124567 Jan 12, 2024
cc9a676
use one query for querying transactions in a block
chris124567 Jan 12, 2024
625c78a
sqlite: remove implicit transaction query helper methods
n8maninger Jan 12, 2024
13c11ed
sqlite: add db encoding and decoding helpers
n8maninger Jan 12, 2024
d28dce4
sqlite: batch transaction queries
n8maninger Jan 12, 2024
e0751be
api, explorer: update interfaces, use batch transaction method
n8maninger Jan 12, 2024
225dc75
include column names in index names
chris124567 Jan 12, 2024
832e9aa
remove Transaction from interface
chris124567 Jan 12, 2024
fcefea8
encode transaction IDs before passing to DB
chris124567 Jan 12, 2024
77bfbfb
fix response type of explorerTransactionsIDHandler
chris124567 Jan 12, 2024
70d384c
check transaction was found
chris124567 Jan 12, 2024
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
12 changes: 12 additions & 0 deletions api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,15 @@ func (c *Client) BlockHeight(height uint64) (resp types.Block, err error) {
err = c.c.GET(fmt.Sprintf("/explorer/block/height/%d", height), &resp)
return
}

// Transaction returns the transaction with the specified ID.
func (c *Client) Transaction(id types.TransactionID) (resp types.Transaction, err error) {
err = c.c.GET(fmt.Sprintf("/explorer/transactions/id/%s", id), &resp)
return
}

// Transactions returns the transactions with the specified IDs.
func (c *Client) Transactions(ids []types.TransactionID) (resp []types.Transaction, err error) {
err = c.c.POST("/explorer/transactions", ids, &resp)
return
}
46 changes: 43 additions & 3 deletions api/server.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package api

import (
"errors"
"fmt"
"net/http"
"sync"

"go.sia.tech/jape"

Expand Down Expand Up @@ -42,15 +43,14 @@ type (
Tip() (types.ChainIndex, error)
BlockByID(id types.BlockID) (types.Block, error)
BlockByHeight(height uint64) (types.Block, error)
Transactions(ids []types.TransactionID) ([]types.Transaction, error)
}
)

type server struct {
cm ChainManager
e Explorer
s Syncer

mu sync.Mutex
}

func (s *server) syncerPeersHandler(jc jape.Context) {
Expand Down Expand Up @@ -164,6 +164,44 @@ func (s *server) explorerBlockHeightHandler(jc jape.Context) {
jc.Encode(block)
}

func (s *server) explorerTransactionsIDHandler(jc jape.Context) {
errNotFound := errors.New("no transaction found")

var id types.TransactionID
if jc.DecodeParam("id", &id) != nil {
return
}
txns, err := s.e.Transactions([]types.TransactionID{id})
if jc.Check("failed to get transaction", err) != nil {
return
} else if len(txns) == 0 {
jc.Error(errNotFound, http.StatusNotFound)
return
}
jc.Encode(txns[0])
chris124567 marked this conversation as resolved.
Show resolved Hide resolved
}

func (s *server) explorerTransactionsHandler(jc jape.Context) {
const (
maxIDs = 5000
)
errTooManyIDs := fmt.Errorf("too many IDs provided (provide less than %d)", maxIDs)

var ids []types.TransactionID
if jc.Decode(&ids) != nil {
return
} else if len(ids) > maxIDs {
jc.Error(errTooManyIDs, http.StatusBadRequest)
return
}

txns, err := s.e.Transactions(ids)
if jc.Check("failed to get transactions", err) != nil {
return
}
jc.Encode(txns)
}

// NewServer returns an HTTP handler that serves the explored API.
func NewServer(e Explorer, cm ChainManager, s Syncer) http.Handler {
srv := server{
Expand All @@ -183,5 +221,7 @@ func NewServer(e Explorer, cm ChainManager, s Syncer) http.Handler {
"GET /explorer/tip": srv.explorerTipHandler,
"GET /explorer/block/id/:id": srv.explorerBlockHandler,
"GET /explorer/block/height/:height": srv.explorerBlockHeightHandler,
"GET /explorer/transactions/id/:id": srv.explorerTransactionsIDHandler,
"POST /explorer/transactions": srv.explorerTransactionsHandler,
})
}
6 changes: 6 additions & 0 deletions explorer/explorer.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type Store interface {
Tip() (types.ChainIndex, error)
BlockByID(id types.BlockID) (types.Block, error)
BlockByHeight(height uint64) (types.Block, error)
Transactions(ids []types.TransactionID) ([]types.Transaction, error)
}

// Explorer implements a Sia explorer.
Expand All @@ -39,3 +40,8 @@ func (e *Explorer) BlockByID(id types.BlockID) (types.Block, error) {
func (e *Explorer) BlockByHeight(height uint64) (types.Block, error) {
return e.s.BlockByHeight(height)
}

// Transactions returns the transactions with the specified IDs.
func (e *Explorer) Transactions(ids []types.TransactionID) ([]types.Transaction, error) {
return e.s.Transactions(ids)
}
80 changes: 80 additions & 0 deletions persist/sqlite/blocks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package sqlite

import (
"fmt"

"go.sia.tech/core/types"
)

// BlockByID implements explorer.Store.
func (s *Store) BlockByID(id types.BlockID) (result types.Block, err error) {
err = s.transaction(func(tx txn) error {
err = tx.QueryRow(`SELECT parent_id, nonce, timestamp FROM blocks WHERE id=?`, dbEncode(id)).Scan(dbDecode(&result.ParentID), dbDecode(&result.Nonce), dbDecode(&result.Timestamp))
if err != nil {
return err
}

result.MinerPayouts, err = blockMinerPayouts(tx, id)
if err != nil {
return fmt.Errorf("failed to get miner payouts: %v", err)
}

// get block transaction IDs
transactionIDs, err := blockTransactionIDs(tx, id)
if err != nil {
return fmt.Errorf("failed to get block transaction IDs: %v", err)
}

// get arbitrary data for each transaction
txnArbitraryData, err := transactionArbitraryData(tx, transactionIDs)
if err != nil {
return fmt.Errorf("failed to get arbitrary data: %v", err)
}

for _, id := range transactionIDs {
txn := types.Transaction{
ArbitraryData: txnArbitraryData[id],
}
result.Transactions = append(result.Transactions, txn)
}
return nil
})
return
}

// BlockByHeight implements explorer.Store.
func (s *Store) BlockByHeight(height uint64) (result types.Block, err error) {
err = s.transaction(func(tx txn) error {
var blockID types.BlockID
err = tx.QueryRow(`SELECT id, parent_id, nonce, timestamp FROM blocks WHERE height=?`, height).Scan(dbDecode(&blockID), dbDecode(&result.ParentID), dbDecode(&result.Nonce), dbDecode(&result.Timestamp))
if err != nil {
return err
}

result.MinerPayouts, err = blockMinerPayouts(tx, blockID)
if err != nil {
return fmt.Errorf("failed to get miner payouts: %v", err)
}

// get block transaction IDs
transactionIDs, err := blockTransactionIDs(tx, blockID)
if err != nil {
return fmt.Errorf("failed to get block transaction IDs: %v", err)
}

// get arbitrary data for each transaction
txnArbitraryData, err := transactionArbitraryData(tx, transactionIDs)
if err != nil {
return fmt.Errorf("failed to get arbitrary data: %v", err)
}

for _, id := range transactionIDs {
txn := types.Transaction{
ArbitraryData: txnArbitraryData[id],
}
result.Transactions = append(result.Transactions, txn)
}
return nil
})
return
}
144 changes: 144 additions & 0 deletions persist/sqlite/consensus.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package sqlite

import (
"database/sql"
"errors"
"fmt"

"go.sia.tech/core/chain"
"go.sia.tech/core/types"
)

func (s *Store) addBlock(dbTxn txn, b types.Block, height uint64) error {
// nonce is encoded because database/sql doesn't support uint64 with high bit set
_, err := dbTxn.Exec("INSERT INTO blocks(id, height, parent_id, nonce, timestamp) VALUES (?, ?, ?, ?, ?);", dbEncode(b.ID()), height, dbEncode(b.ParentID), dbEncode(b.Nonce), dbEncode(b.Timestamp))
return err
}

func (s *Store) addMinerPayouts(dbTxn txn, bid types.BlockID, scos []types.SiacoinOutput) error {
stmt, err := dbTxn.Prepare(`INSERT INTO miner_payouts(block_id, block_order, address, value) VALUES (?, ?, ?, ?);`)
if err != nil {
return fmt.Errorf("addMinerPayouts: failed to prepare statement: %v", err)
}
defer stmt.Close()

for i, sco := range scos {
if _, err := stmt.Exec(dbEncode(bid), i, dbEncode(sco.Address), dbEncode(sco.Value)); err != nil {
return fmt.Errorf("addMinerPayouts: failed to execute statement: %v", err)
}
}
return nil
}

func (s *Store) addArbitraryData(dbTxn txn, id int64, txn types.Transaction) error {
stmt, err := dbTxn.Prepare(`INSERT INTO arbitrary_data(transaction_id, transaction_order, data) VALUES (?, ?, ?)`)
if err != nil {
return fmt.Errorf("addArbitraryData: failed to prepare statement: %v", err)
}
defer stmt.Close()

for i, arbitraryData := range txn.ArbitraryData {
if _, err := stmt.Exec(id, i, arbitraryData); err != nil {
return fmt.Errorf("addArbitraryData: failed to execute statement: %v", err)
}
}
return nil
}

func (s *Store) addTransactions(dbTxn txn, bid types.BlockID, txns []types.Transaction) error {
transactionsStmt, err := dbTxn.Prepare(`INSERT INTO transactions(transaction_id) VALUES (?);`)
if err != nil {
return fmt.Errorf("addTransactions: failed to prepare transactions statement: %v", err)
}
defer transactionsStmt.Close()

blockTransactionsStmt, err := dbTxn.Prepare(`INSERT INTO block_transactions(block_id, transaction_id, block_order) VALUES (?, ?, ?);`)
if err != nil {
return fmt.Errorf("addTransactions: failed to prepare block_transactions statement: %v", err)
}
defer blockTransactionsStmt.Close()

for i, txn := range txns {
result, err := transactionsStmt.Exec(dbEncode(txn.ID()))
if err != nil {
return fmt.Errorf("addTransactions: failed to insert into transactions: %v", err)
}
txnID, err := result.LastInsertId()
if err != nil {
return fmt.Errorf("addTransactions: failed to get insert result ID: %v", err)
}

if _, err := blockTransactionsStmt.Exec(dbEncode(bid), txnID, i); err != nil {
return fmt.Errorf("addTransactions: failed to insert into block_transactions: %v", err)
} else if err := s.addArbitraryData(dbTxn, txnID, txn); err != nil {
return fmt.Errorf("addTransactions: failed to add arbitrary data: %v", err)
}
}
return nil
}

func (s *Store) deleteBlock(dbTxn txn, bid types.BlockID) error {
_, err := dbTxn.Exec("DELETE FROM blocks WHERE id = ?", dbEncode(bid))
return err
}

func (s *Store) applyUpdates() error {
return s.transaction(func(dbTxn txn) error {
for _, update := range s.pendingUpdates {
if err := s.addBlock(dbTxn, update.Block, update.State.Index.Height); err != nil {
return fmt.Errorf("applyUpdates: failed to add block: %v", err)
} else if err := s.addMinerPayouts(dbTxn, update.Block.ID(), update.Block.MinerPayouts); err != nil {
return fmt.Errorf("applyUpdates: failed to add miner payouts: %v", err)
} else if err := s.addTransactions(dbTxn, update.Block.ID(), update.Block.Transactions); err != nil {
return fmt.Errorf("applyUpdates: failed to add transactions: %v", err)
}
}
s.pendingUpdates = s.pendingUpdates[:0]
return nil
})
}

func (s *Store) revertUpdate(cru *chain.RevertUpdate) error {
return s.transaction(func(dbTxn txn) error {
if err := s.deleteBlock(dbTxn, cru.Block.ID()); err != nil {
return fmt.Errorf("revertUpdate: failed to delete block: %v", err)
}
return nil
})
}

// ProcessChainApplyUpdate implements chain.Subscriber.
func (s *Store) ProcessChainApplyUpdate(cau *chain.ApplyUpdate, mayCommit bool) error {
s.mu.Lock()
defer s.mu.Unlock()

s.pendingUpdates = append(s.pendingUpdates, cau)
if mayCommit {
return s.applyUpdates()
}
return nil
}

// ProcessChainRevertUpdate implements chain.Subscriber.
func (s *Store) ProcessChainRevertUpdate(cru *chain.RevertUpdate) error {
s.mu.Lock()
defer s.mu.Unlock()

if len(s.pendingUpdates) > 0 && s.pendingUpdates[len(s.pendingUpdates)-1].Block.ID() == cru.Block.ID() {
s.pendingUpdates = s.pendingUpdates[:len(s.pendingUpdates)-1]
return nil
}
return s.revertUpdate(cru)
}

// Tip implements explorer.Store.
func (s *Store) Tip() (result types.ChainIndex, err error) {
const query = `SELECT id, height FROM blocks ORDER BY height DESC LIMIT 1`
err = s.transaction(func(dbTx txn) error {
return dbTx.QueryRow(query).Scan(dbDecode(&result.ID), &result.Height)
})
if errors.Is(err, sql.ErrNoRows) {
return types.ChainIndex{}, ErrNoTip
}
return
}
Loading