Skip to content

Commit

Permalink
Merge pull request #159 from SiaFoundation/nate/consensus-updates
Browse files Browse the repository at this point in the history
Add consensus updates endpoints
  • Loading branch information
n8maninger authored Jul 31, 2024
2 parents cde2630 + f6662da commit 32632a3
Show file tree
Hide file tree
Showing 4 changed files with 237 additions and 14 deletions.
21 changes: 21 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"time"

"go.sia.tech/core/consensus"
"go.sia.tech/core/types"
"go.sia.tech/walletd/wallet"
)
Expand Down Expand Up @@ -102,3 +103,23 @@ type RescanResponse struct {
StartTime time.Time `json:"startTime"`
Error *string `json:"error,omitempty"`
}

// An ApplyUpdate is a consensus update that was applied to the best chain.
type ApplyUpdate struct {
Update consensus.ApplyUpdate `json:"update"`
State consensus.State `json:"state"`
Block types.Block `json:"block"`
}

// A RevertUpdate is a consensus update that was reverted from the best chain.
type RevertUpdate struct {
Update consensus.RevertUpdate `json:"update"`
State consensus.State `json:"state"`
Block types.Block `json:"block"`
}

// ConsensusUpdatesResponse is the response type for /consensus/updates/:index.
type ConsensusUpdatesResponse struct {
Applied []ApplyUpdate `json:"applied"`
Reverted []RevertUpdate `json:"reverted"`
}
67 changes: 67 additions & 0 deletions api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"go.sia.tech/core/consensus"
"go.sia.tech/core/gateway"
"go.sia.tech/core/types"
"go.sia.tech/coreutils"
"go.sia.tech/coreutils/chain"
"go.sia.tech/coreutils/syncer"
"go.sia.tech/jape"
Expand Down Expand Up @@ -1214,3 +1215,69 @@ func TestP2P(t *testing.T) {
t.Fatal(err)
}
}

func TestConsensusUpdates(t *testing.T) {
log := zaptest.NewLogger(t)

n, genesisBlock := testNetwork()
giftPrivateKey := types.GeneratePrivateKey()
giftAddress := types.StandardUnlockHash(giftPrivateKey.PublicKey())
genesisBlock.Transactions[0].SiacoinOutputs[0] = types.SiacoinOutput{
Value: types.Siacoins(1),
Address: giftAddress,
}

// create wallets
dbstore, tipState, err := chain.NewDBStore(chain.NewMemDB(), n, genesisBlock)
if err != nil {
t.Fatal(err)
}
cm := chain.NewManager(dbstore, tipState)

ws, err := sqlite.OpenDatabase(filepath.Join(t.TempDir(), "wallets.db"), log.Named("sqlite3"))
if err != nil {
t.Fatal(err)
}
defer ws.Close()

wm, err := wallet.NewManager(cm, ws, wallet.WithLogger(log.Named("wallet")))
if err != nil {
t.Fatal(err)
}
defer wm.Close()

c, shutdown := runServer(cm, nil, wm)
defer shutdown()

for i := 0; i < 10; i++ {
b, ok := coreutils.MineBlock(cm, types.VoidAddress, time.Second)
if !ok {
t.Fatal("failed to mine block")
} else if err := cm.AddBlocks([]types.Block{b}); err != nil {
t.Fatal(err)
}
}

waitForBlock(t, cm, ws)

reverted, applied, err := c.ConsensusUpdates(types.ChainIndex{}, 10)
if err != nil {
t.Fatal(err)
} else if len(reverted) != 0 {
t.Fatal("expected no reverted blocks")
} else if len(applied) != 11 { // genesis + 10 mined blocks (chain manager off-by-one)
t.Fatalf("expected 11 applied blocks, got %v", len(applied))
}

for i, cau := range applied {
// using i for height since we're testing the update contents
expected, ok := cm.BestIndex(uint64(i))
if !ok {
t.Fatalf("failed to get expected index for block %v", i)
} else if cau.State.Index != expected {
t.Fatalf("expected index %v, got %v", expected, cau.State.Index)
} else if cau.State.Network.Name != n.Name { // TODO: better comparison. reflect.DeepEqual is failing in CI, but passing local.
t.Fatalf("expected network to be %q, got %q", n.Name, cau.State.Network.Name)
}
}
}
92 changes: 81 additions & 11 deletions api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,36 @@ package api

import (
"fmt"
"sync"
"time"

"go.sia.tech/core/consensus"
"go.sia.tech/core/types"
"go.sia.tech/coreutils/chain"
"go.sia.tech/jape"
"go.sia.tech/walletd/wallet"
)

// A Client provides methods for interacting with a walletd API server.
type Client struct {
c jape.Client
n *consensus.Network // for ConsensusTipState

mu sync.Mutex // protects n
n *consensus.Network
}

func (c *Client) getNetwork() (*consensus.Network, error) {
c.mu.Lock()
defer c.mu.Unlock()

if c.n == nil {
var err error
c.n, err = c.ConsensusNetwork()
if err != nil {
return nil, err
}
}
return c.n, nil
}

// State returns information about the current state of the walletd daemon.
Expand All @@ -35,6 +53,13 @@ func (c *Client) TxpoolTransactions() (txns []types.Transaction, v2txns []types.
return resp.Transactions, resp.V2Transactions, err
}

// TxpoolParents returns the parents of a transaction that are currently in the
// transaction pool.
func (c *Client) TxpoolParents(txn types.Transaction) (resp []types.Transaction, err error) {
err = c.c.POST("/txpool/parents", txn, &resp)
return
}

// TxpoolFee returns the recommended fee (per weight unit) to ensure a high
// probability of inclusion in the next block.
func (c *Client) TxpoolFee() (resp types.Currency, err error) {
Expand All @@ -49,22 +74,67 @@ func (c *Client) ConsensusNetwork() (resp *consensus.Network, err error) {
return
}

// ConsensusTip returns the current tip index.
func (c *Client) ConsensusTip() (resp types.ChainIndex, err error) {
err = c.c.GET("/consensus/tip", &resp)
// ConsensusIndex returns the consensus index at the specified height.
func (c *Client) ConsensusIndex(height uint64) (resp types.ChainIndex, err error) {
err = c.c.GET(fmt.Sprintf("/consensus/index/%d", height), &resp)
return
}

// ConsensusUpdates returns at most n consensus updates that have occurred since
// the specified index
func (c *Client) ConsensusUpdates(index types.ChainIndex, limit int) ([]chain.RevertUpdate, []chain.ApplyUpdate, error) {
// index.String() is a short-hand representation. We need the full text
indexBuf, err := index.MarshalText()
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal index: %w", err)
}

var resp ConsensusUpdatesResponse
if err := c.c.GET(fmt.Sprintf("/consensus/updates/%s?limit=%d", indexBuf, limit), &resp); err != nil {
return nil, nil, err
}

network, err := c.getNetwork()
if err != nil {
return nil, nil, fmt.Errorf("failed to get network metadata: %w", err)
}

reverted := make([]chain.RevertUpdate, 0, len(resp.Reverted))
for _, u := range resp.Reverted {
revert := chain.RevertUpdate{
RevertUpdate: u.Update,
State: u.State,
Block: u.Block,
}
revert.State.Network = network
reverted = append(reverted, revert)
}

applied := make([]chain.ApplyUpdate, 0, len(resp.Applied))
for _, u := range resp.Applied {
apply := chain.ApplyUpdate{
ApplyUpdate: u.Update,
State: u.State,
Block: u.Block,
}
apply.State.Network = network
applied = append(applied, apply)
}
return reverted, applied, nil
}

// ConsensusTipState returns the current tip state.
func (c *Client) ConsensusTipState() (resp consensus.State, err error) {
if c.n == nil {
c.n, err = c.ConsensusNetwork()
if err != nil {
return
}
if err = c.c.GET("/consensus/tipstate", &resp); err != nil {
return
}
err = c.c.GET("/consensus/tipstate", &resp)
resp.Network = c.n
resp.Network, err = c.getNetwork()
return
}

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

Expand Down
71 changes: 68 additions & 3 deletions api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"go.sia.tech/core/consensus"
"go.sia.tech/core/gateway"
"go.sia.tech/core/types"
"go.sia.tech/coreutils/chain"
"go.sia.tech/coreutils/syncer"
"go.sia.tech/walletd/build"
"go.sia.tech/walletd/wallet"
Expand All @@ -23,6 +24,8 @@ import (
type (
// A ChainManager manages blockchain and txpool state.
ChainManager interface {
UpdatesSince(types.ChainIndex, int) ([]chain.RevertUpdate, []chain.ApplyUpdate, error)

BestIndex(height uint64) (types.ChainIndex, bool)
TipState() consensus.State
AddBlocks([]types.Block) error
Expand Down Expand Up @@ -120,6 +123,56 @@ func (s *server) consensusTipStateHandler(jc jape.Context) {
jc.Encode(s.cm.TipState())
}

func (s *server) consensusIndexHeightHandler(jc jape.Context) {
var height uint64
if jc.DecodeParam("height", &height) != nil {
return
}
index, ok := s.cm.BestIndex(height)
if !ok {
jc.Error(errors.New("height not found"), http.StatusNotFound)
return
}
jc.Encode(index)
}

func (s *server) consensusUpdatesIndexHandler(jc jape.Context) {
var index types.ChainIndex
if jc.DecodeParam("index", &index) != nil {
return
}

limit := 10
if jc.DecodeForm("limit", &limit) != nil {
return
} else if limit <= 0 || limit > 100 {
jc.Error(errors.New("limit must be between 0 and 100"), http.StatusBadRequest)
return
}

reverted, applied, err := s.cm.UpdatesSince(index, limit)
if jc.Check("couldn't get updates", err) != nil {
return
}

var res ConsensusUpdatesResponse
for _, ru := range reverted {
res.Reverted = append(res.Reverted, RevertUpdate{
Update: ru.RevertUpdate,
State: ru.State,
Block: ru.Block,
})
}
for _, au := range applied {
res.Applied = append(res.Applied, ApplyUpdate{
Update: au.ApplyUpdate,
State: au.State,
Block: au.Block,
})
}
jc.Encode(res)
}

func (s *server) syncerPeersHandler(jc jape.Context) {
var peers []GatewayPeer
for _, p := range s.s.Peers() {
Expand Down Expand Up @@ -173,6 +226,15 @@ func (s *server) syncerBroadcastBlockHandler(jc jape.Context) {
}
}

func (s *server) txpoolParentsHandler(jc jape.Context) {
var txn types.Transaction
if jc.Decode(&txn) != nil {
return
}

jc.Encode(s.cm.UnconfirmedParents(txn))
}

func (s *server) txpoolTransactionsHandler(jc jape.Context) {
jc.Encode(TxpoolTransactionsResponse{
Transactions: s.cm.PoolTransactions(),
Expand Down Expand Up @@ -765,14 +827,17 @@ func NewServer(cm ChainManager, s Syncer, wm WalletManager) http.Handler {
return jape.Mux(map[string]jape.Handler{
"GET /state": srv.stateHandler,

"GET /consensus/network": srv.consensusNetworkHandler,
"GET /consensus/tip": srv.consensusTipHandler,
"GET /consensus/tipstate": srv.consensusTipStateHandler,
"GET /consensus/network": srv.consensusNetworkHandler,
"GET /consensus/tip": srv.consensusTipHandler,
"GET /consensus/tipstate": srv.consensusTipStateHandler,
"GET /consensus/updates/:index": srv.consensusUpdatesIndexHandler,
"GET /consensus/index/:height": srv.consensusIndexHeightHandler,

"GET /syncer/peers": srv.syncerPeersHandler,
"POST /syncer/connect": srv.syncerConnectHandler,
"POST /syncer/broadcast/block": srv.syncerBroadcastBlockHandler,

"POST /txpool/parents": srv.txpoolParentsHandler,
"GET /txpool/transactions": srv.txpoolTransactionsHandler,
"GET /txpool/fee": srv.txpoolFeeHandler,
"POST /txpool/broadcast": srv.txpoolBroadcastHandler,
Expand Down

0 comments on commit 32632a3

Please sign in to comment.