Skip to content

Commit

Permalink
Merge pull request #2 from SiaFoundation/add-txn
Browse files Browse the repository at this point in the history
Store transactions
  • Loading branch information
n8maninger authored Jan 13, 2024
2 parents 4508fbd + 70d384c commit cdaad98
Show file tree
Hide file tree
Showing 12 changed files with 521 additions and 289 deletions.
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])
}

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

0 comments on commit cdaad98

Please sign in to comment.