Skip to content

Commit

Permalink
Merge pull request #1 from SiaFoundation/add-block
Browse files Browse the repository at this point in the history
Add block endpoints
  • Loading branch information
n8maninger authored Jan 8, 2024
2 parents df60088 + 7d21c16 commit 4508fbd
Show file tree
Hide file tree
Showing 12 changed files with 329 additions and 16 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ on:
push:
branches:
- master
env:
CGO_ENABLED: 1

jobs:
test:
Expand All @@ -14,7 +16,7 @@ jobs:
strategy:
matrix:
os: [ ubuntu-latest , macos-latest, windows-latest ]
go-version: [ '1.19', '1.20' ]
go-version: [ '1.20', '1.21' ]
steps:
- name: Configure git
run: git config --global core.autocrlf false # required on Windows
Expand Down
20 changes: 20 additions & 0 deletions api/client.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package api

import (
"fmt"

"go.sia.tech/core/consensus"
"go.sia.tech/core/types"
"go.sia.tech/jape"
Expand Down Expand Up @@ -58,3 +60,21 @@ func (c *Client) SyncerBroadcastBlock(b types.Block) (err error) {
err = c.c.POST("/syncer/broadcast/block", b, nil)
return
}

// Tip returns the current tip of the explorer.
func (c *Client) Tip() (resp types.ChainIndex, err error) {
err = c.c.GET("/explorer/tip", &resp)
return
}

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

// BlockHeight returns the block with the specified height.
func (c *Client) BlockHeight(height uint64) (resp types.Block, err error) {
err = c.c.GET(fmt.Sprintf("/explorer/block/height/%d", height), &resp)
return
}
47 changes: 46 additions & 1 deletion api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,18 @@ type (
BroadcastV2TransactionSet(txns []types.V2Transaction)
BroadcastV2BlockOutline(bo gateway.V2BlockOutline)
}

// Explorer implements a Sia explorer.
Explorer interface {
Tip() (types.ChainIndex, error)
BlockByID(id types.BlockID) (types.Block, error)
BlockByHeight(height uint64) (types.Block, error)
}
)

type server struct {
cm ChainManager
e Explorer
s Syncer

mu sync.Mutex
Expand Down Expand Up @@ -124,10 +132,43 @@ func (s *server) txpoolBroadcastHandler(jc jape.Context) {
}
}

func (s *server) explorerTipHandler(jc jape.Context) {
tip, err := s.e.Tip()
if jc.Check("failed to get tip", err) != nil {
return
}
jc.Encode(tip)
}

func (s *server) explorerBlockHandler(jc jape.Context) {
var id types.BlockID
if jc.DecodeParam("id", &id) != nil {
return
}
block, err := s.e.BlockByID(id)
if jc.Check("failed to get block", err) != nil {
return
}
jc.Encode(block)
}

func (s *server) explorerBlockHeightHandler(jc jape.Context) {
var height uint64
if jc.DecodeParam("height", &height) != nil {
return
}
block, err := s.e.BlockByHeight(height)
if jc.Check("failed to get block", err) != nil {
return
}
jc.Encode(block)
}

// NewServer returns an HTTP handler that serves the explored API.
func NewServer(cm ChainManager, s Syncer) http.Handler {
func NewServer(e Explorer, cm ChainManager, s Syncer) http.Handler {
srv := server{
cm: cm,
e: e,
s: s,
}
return jape.Mux(map[string]jape.Handler{
Expand All @@ -138,5 +179,9 @@ func NewServer(cm ChainManager, s Syncer) http.Handler {
"GET /txpool/transactions": srv.txpoolTransactionsHandler,
"GET /txpool/fee": srv.txpoolFeeHandler,
"POST /txpool/broadcast": srv.txpoolBroadcastHandler,

"GET /explorer/tip": srv.explorerTipHandler,
"GET /explorer/block/id/:id": srv.explorerBlockHandler,
"GET /explorer/block/height/:height": srv.explorerBlockHeightHandler,
})
}
14 changes: 12 additions & 2 deletions cmd/explored/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,21 @@ func newNode(addr, dir string, chainNetwork string, useUPNP bool, logger *zap.Lo
}
cm := chain.NewManager(dbstore, tipState)

store, err := sqlite.OpenDatabase("./explore.db", logger)
store, err := sqlite.OpenDatabase(filepath.Join(dir, "./explore.db"), logger)
if err != nil {
panic(err)
return nil, err
}
e := explorer.NewExplorer(store)
tip, err := store.Tip()
if errors.Is(err, sqlite.ErrNoTip) {
tip = types.ChainIndex{
ID: genesisBlock.ID(),
Height: 0,
}
} else if err != nil {
return nil, err
}
cm.AddSubscriber(store, tip)

l, err := net.Listen("tcp", addr)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/explored/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
)

func startWeb(l net.Listener, node *node, password string) error {
renter := api.NewServer(node.cm, node.s)
renter := api.NewServer(node.e, node.cm, node.s)
api := jape.BasicAuth(password)(renter)
return http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/api") {
Expand Down
32 changes: 23 additions & 9 deletions explorer/explorer.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
package explorer

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

// A Store is a database that stores information about elements, contracts,
// and blocks.
type Store interface{}
type Store interface {
chain.Subscriber

Tip() (types.ChainIndex, error)
BlockByID(id types.BlockID) (types.Block, error)
BlockByHeight(height uint64) (types.Block, error)
}

// Explorer implements a Sia explorer.
type Explorer struct {
Expand All @@ -13,15 +22,20 @@ type Explorer struct {

// NewExplorer returns a Sia explorer.
func NewExplorer(s Store) *Explorer {
return &Explorer{s}
return &Explorer{s: s}
}

// Tip returns the tip of the best known valid chain.
func (e *Explorer) Tip() (types.ChainIndex, error) {
return e.s.Tip()
}

// ProcessChainApplyUpdate implements chain.Subscriber.
func (e *Explorer) ProcessChainApplyUpdate(cau *chain.ApplyUpdate, mayCommit bool) error {
return nil
// BlockByID returns the block with the specified ID.
func (e *Explorer) BlockByID(id types.BlockID) (types.Block, error) {
return e.s.BlockByID(id)
}

// ProcessChainRevertUpdate implements chain.Subscriber.
func (e *Explorer) ProcessChainRevertUpdate(cru *chain.RevertUpdate) error {
return nil
// BlockByHeight returns the block with the specified height.
func (e *Explorer) BlockByHeight(height uint64) (types.Block, error) {
return e.s.BlockByHeight(height)
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ require (
github.com/mattn/go-sqlite3 v1.14.19
go.etcd.io/bbolt v1.3.7
go.sia.tech/core v0.1.12-0.20231021194448-f1e65eb9f0d0
go.sia.tech/jape v0.9.0
go.sia.tech/jape v0.11.1
go.uber.org/zap v1.26.0
golang.org/x/term v0.6.0
lukechampine.com/frand v1.4.2
lukechampine.com/upnp v0.3.0
Expand All @@ -17,7 +18,6 @@ require (
github.com/julienschmidt/httprouter v1.3.0 // indirect
go.sia.tech/mux v1.2.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/tools v0.7.0 // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ go.sia.tech/core v0.1.12-0.20231021194448-f1e65eb9f0d0 h1:2nKOKa99g9h9m3hL5UortA
go.sia.tech/core v0.1.12-0.20231021194448-f1e65eb9f0d0/go.mod h1:3EoY+rR78w1/uGoXXVqcYdwSjSJKuEMI5bL7WROA27Q=
go.sia.tech/jape v0.9.0 h1:kWgMFqALYhLMJYOwWBgJda5ko/fi4iZzRxHRP7pp8NY=
go.sia.tech/jape v0.9.0/go.mod h1:4QqmBB+t3W7cNplXPj++ZqpoUb2PeiS66RLpXmEGap4=
go.sia.tech/jape v0.11.1 h1:M7IP+byXL7xOqzxcHUQuXW+q3sYMkYzmMlMw+q8ZZw0=
go.sia.tech/jape v0.11.1/go.mod h1:4QqmBB+t3W7cNplXPj++ZqpoUb2PeiS66RLpXmEGap4=
go.sia.tech/mux v1.2.0 h1:ofa1Us9mdymBbGMY2XH/lSpY8itFsKIo/Aq8zwe+GHU=
go.sia.tech/mux v1.2.0/go.mod h1:Yyo6wZelOYTyvrHmJZ6aQfRoer3o4xyKQ4NmQLJrBSo=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
Expand Down
18 changes: 18 additions & 0 deletions persist/sqlite/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,23 @@ CREATE TABLE global_settings (
db_version INTEGER NOT NULL -- used for migrations
);

CREATE TABLE blocks (
id BLOB NOT NULL PRIMARY KEY,
height INTEGER NOT NULL,
parent_id BLOB NOT NULL,
nonce BLOB NOT NULL,
timestamp INTEGER NOT NULL
);

CREATE TABLE miner_payouts (
block_id BLOB REFERENCES blocks(id) ON DELETE CASCADE NOT NULL,
block_order INTEGER NOT NULL,
address BLOB NOT NULL,
value BLOB NOT NULL,
UNIQUE(block_id, block_order)
);

CREATE INDEX miner_payouts_index ON miner_payouts(block_id);

-- initialize the global settings table
INSERT INTO global_settings (id, db_version) VALUES (0, 0); -- should not be changed
104 changes: 104 additions & 0 deletions persist/sqlite/query.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package sqlite

import (
"database/sql"
"errors"
"time"

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

var (
// ErrNoTip is returned when Tip() is unable to find any blocks in the
// database and thus there is no tip. It does not mean there was an
// error in the underlying database.
ErrNoTip = errors.New("no tip found")
)

func decode(obj types.DecoderFrom, data []byte) error {
d := types.NewBufDecoder(data)
obj.DecodeFrom(d)
return d.Err()
}

func decodeUint64(x *uint64, data []byte) error {
d := types.NewBufDecoder(data)
if x != nil {
*x = d.ReadUint64()
}
return d.Err()
}

// Tip implements explorer.Store.
func (s *Store) Tip() (result types.ChainIndex, err error) {
var data []byte
err = s.queryRow("SELECT id, height FROM blocks WHERE height = (SELECT MAX(height) from blocks)").Scan(&data, &result.Height)
if errors.Is(err, sql.ErrNoRows) {
err = ErrNoTip
return
} else if err != nil {
return
}
if err = decode(&result.ID, data); err != nil {
return
}
return
}

// BlockByID implements explorer.Store.
func (s *Store) BlockByID(id types.BlockID) (result types.Block, err error) {
{
var timestamp int64
var parentID, nonce []byte
if err = s.queryRow("SELECT parent_id, nonce, timestamp FROM blocks WHERE id = ?", encode(id)).Scan(&parentID, &nonce, &timestamp); err != nil {
return
}
result.Timestamp = time.Unix(timestamp, 0).UTC()
if err = decode(&result.ParentID, parentID); err != nil {
return
}
if err = decodeUint64(&result.Nonce, nonce); err != nil {
return
}
}

{
var rows *loggedRows
if rows, err = s.query("SELECT address, value FROM miner_payouts WHERE block_id = ? ORDER BY block_order", encode(id)); err != nil {
return
}
defer rows.Close()

var address, value []byte
for rows.Next() {
if err = rows.Scan(&address, &value); err != nil {
return
}
var minerPayout types.SiacoinOutput
if err = decode(&minerPayout.Address, address); err != nil {
return
}
if err = decode(&minerPayout.Value, value); err != nil {
return
}
result.MinerPayouts = append(result.MinerPayouts, minerPayout)
}
}

return
}

// BlockByHeight implements explorer.Store.
func (s *Store) BlockByHeight(height uint64) (result types.Block, err error) {
var data []byte
if err = s.queryRow("SELECT id FROM blocks WHERE height = ?", height).Scan(&data); err != nil {
return
}

var bid types.BlockID
if err = decode(&bid, data); err != nil {
return
}
result, err = s.BlockByID(bid)
return
}
5 changes: 5 additions & 0 deletions persist/sqlite/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import (
"fmt"
"math"
"strings"
"sync"
"time"

"github.com/mattn/go-sqlite3"
"go.sia.tech/core/chain"
"go.uber.org/zap"
"lukechampine.com/frand"
)
Expand All @@ -19,6 +21,9 @@ type (
Store struct {
db *sql.DB
log *zap.Logger

mu sync.Mutex
pendingUpdates []*chain.ApplyUpdate
}
)

Expand Down
Loading

0 comments on commit 4508fbd

Please sign in to comment.