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

Add consensus updates endpoints #159

Merged
merged 1 commit into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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