mirror of
https://github.com/tendermint/tendermint.git
synced 2026-04-14 04:37:05 +00:00
light: return light client status on rpc /status (#7536)
*light: rpc /status returns status of light client ; code refactoring light: moved lightClientInfo into light.go, renamed String to ID test/e2e: Return light client trusted height instead of SyncInfo trusted height test/e2e/start.go: Not waiting for light client to catch up in tests. Removed querying of syncInfo in start if the node is a light node * light: Removed call to primary /status. Added trustedPeriod to light info * light/provider: added ID function to return IP of primary and witnesses * light/provider/http/http_test: renamed String() to ID()
This commit is contained in:
committed by
GitHub
parent
4e5c2b5e8f
commit
d68d25dcd5
@@ -55,6 +55,7 @@ Special thanks to external contributors on this release:
|
||||
- [pubsub] \#7319 Performance improvements for the event query API (@creachadair)
|
||||
- [node] \#7521 Define concrete type for seed node implementation (@spacech1mp)
|
||||
- [rpc] \#7612 paginate mempool /unconfirmed_txs rpc endpoint (@spacech1mp)
|
||||
- [light] [\#7536](https://github.com/tendermint/tendermint/pull/7536) rpc /status call returns info about the light client (@jmalicevic)
|
||||
|
||||
### BUG FIXES
|
||||
|
||||
|
||||
@@ -224,6 +224,9 @@ func (p *BlockProvider) ReportEvidence(ctx context.Context, ev types.Evidence) e
|
||||
// String implements stringer interface
|
||||
func (p *BlockProvider) String() string { return string(p.peer) }
|
||||
|
||||
// Returns the ID address of the provider (NodeID of peer)
|
||||
func (p *BlockProvider) ID() string { return string(p.peer) }
|
||||
|
||||
//----------------------------------------------------------------
|
||||
|
||||
// peerList is a rolling list of peers. This is used to distribute the load of
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
tmmath "github.com/tendermint/tendermint/libs/math"
|
||||
"github.com/tendermint/tendermint/light/provider"
|
||||
"github.com/tendermint/tendermint/light/store"
|
||||
|
||||
"github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
@@ -1146,3 +1147,29 @@ func (c *Client) providerShouldBeRemoved(err error) bool {
|
||||
errors.As(err, &provider.ErrBadLightBlock{}) ||
|
||||
errors.Is(err, provider.ErrConnectionClosed)
|
||||
}
|
||||
|
||||
func (c *Client) Status(ctx context.Context) *types.LightClientInfo {
|
||||
chunks := make([]string, len(c.witnesses))
|
||||
|
||||
// If primary is in witness list we do not want to count it twice in the number of peers
|
||||
primaryNotInWitnessList := 1
|
||||
for i, val := range c.witnesses {
|
||||
chunks[i] = val.ID()
|
||||
if chunks[i] == c.primary.ID() {
|
||||
primaryNotInWitnessList = 0
|
||||
}
|
||||
}
|
||||
|
||||
return &types.LightClientInfo{
|
||||
PrimaryID: c.primary.ID(),
|
||||
WitnessesID: chunks,
|
||||
NumPeers: len(chunks) + primaryNotInWitnessList,
|
||||
LastTrustedHeight: c.latestTrustedBlock.Height,
|
||||
LastTrustedHash: c.latestTrustedBlock.Hash(),
|
||||
LatestBlockTime: c.latestTrustedBlock.Time,
|
||||
TrustingPeriod: c.trustingPeriod.String(),
|
||||
// The caller of /status can deduce this from the two variables above
|
||||
// Having a boolean flag improves readbility
|
||||
TrustedBlockExpired: HeaderExpired(c.latestTrustedBlock.SignedHeader, c.trustingPeriod, time.Now()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,10 @@ func (impl *providerBenchmarkImpl) ReportEvidence(_ context.Context, _ types.Evi
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
// provierBenchmarkImpl does not have an ID iteself.
|
||||
// Thus we return a sample string
|
||||
func (impl *providerBenchmarkImpl) ID() string { return "ip-not-defined.com" }
|
||||
|
||||
func BenchmarkSequence(b *testing.B) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
@@ -168,4 +172,5 @@ func BenchmarkBackwards(b *testing.B) {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -167,3 +167,93 @@ func waitForBlock(ctx context.Context, p provider.Provider, height int64) (*type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientStatusRPC(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
conf, err := rpctest.CreateConfig(t.Name())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Start a test application
|
||||
app := kvstore.NewApplication()
|
||||
|
||||
_, closer, err := rpctest.StartTendermint(ctx, conf, app, rpctest.SuppressStdout)
|
||||
require.NoError(t, err)
|
||||
defer func() { require.NoError(t, closer(ctx)) }()
|
||||
|
||||
dbDir, err := os.MkdirTemp("", "light-client-test-status-example")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { os.RemoveAll(dbDir) })
|
||||
|
||||
chainID := conf.ChainID()
|
||||
|
||||
primary, err := httpp.New(chainID, conf.RPC.ListenAddress)
|
||||
require.NoError(t, err)
|
||||
|
||||
// give Tendermint time to generate some blocks
|
||||
block, err := waitForBlock(ctx, primary, 2)
|
||||
require.NoError(t, err)
|
||||
|
||||
db, err := dbm.NewGoLevelDB("light-client-db", dbDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
// In order to not create a full testnet to verify whether we get the correct IPs
|
||||
// if we have more than one witness, we add the primary multiple times
|
||||
// TODO This should be buggy behavior, we should not be allowed to add the same nodes as witnesses
|
||||
witnesses := []provider.Provider{primary, primary, primary}
|
||||
|
||||
c, err := light.NewClient(ctx,
|
||||
chainID,
|
||||
light.TrustOptions{
|
||||
Period: 504 * time.Hour, // 21 days
|
||||
Height: 2,
|
||||
Hash: block.Hash(),
|
||||
},
|
||||
primary,
|
||||
witnesses,
|
||||
dbs.New(db),
|
||||
light.Logger(log.TestingLogger()),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer func() { require.NoError(t, c.Cleanup()) }()
|
||||
|
||||
lightStatus := c.Status(ctx)
|
||||
|
||||
// Verify primary IP
|
||||
require.True(t, lightStatus.PrimaryID == primary.ID())
|
||||
|
||||
// Verify IPs of witnesses
|
||||
require.ElementsMatch(t, mapProviderArrayToIP(witnesses), lightStatus.WitnessesID)
|
||||
|
||||
// Verify that number of peers is equal to number of witnesses (+ 1 if the primary is not a witness)
|
||||
require.Equal(t, len(witnesses)+1*primaryNotInWitnessList(witnesses, primary), lightStatus.NumPeers)
|
||||
|
||||
// Verify that the last trusted hash returned matches the stored hash of the trusted
|
||||
// block at the last trusted height.
|
||||
blockAtTrustedHeight, err := c.TrustedLightBlock(lightStatus.LastTrustedHeight)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.EqualValues(t, lightStatus.LastTrustedHash, blockAtTrustedHeight.Hash())
|
||||
|
||||
}
|
||||
|
||||
// Extract the IP address of all the providers within an array
|
||||
func mapProviderArrayToIP(el []provider.Provider) []string {
|
||||
ips := make([]string, len(el))
|
||||
for i, v := range el {
|
||||
ips[i] = v.ID()
|
||||
}
|
||||
return ips
|
||||
}
|
||||
|
||||
// If the primary is not in the witness list, we will return 1
|
||||
// Otherwise, return 0
|
||||
func primaryNotInWitnessList(witnesses []provider.Provider, primary provider.Provider) int {
|
||||
for _, el := range witnesses {
|
||||
if el == primary {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -100,7 +100,8 @@ func NewWithClientAndOptions(chainID string, client rpcclient.RemoteClient, opti
|
||||
}
|
||||
}
|
||||
|
||||
func (p *http) String() string {
|
||||
// Identifies the provider with an IP in string format
|
||||
func (p *http) ID() string {
|
||||
return fmt.Sprintf("http{%s}", p.client.Remote())
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package http_test
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -22,15 +21,15 @@ import (
|
||||
func TestNewProvider(t *testing.T) {
|
||||
c, err := lighthttp.New("chain-test", "192.168.0.1:26657")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fmt.Sprintf("%s", c), "http{http://192.168.0.1:26657}")
|
||||
require.Equal(t, c.ID(), "http{http://192.168.0.1:26657}")
|
||||
|
||||
c, err = lighthttp.New("chain-test", "http://153.200.0.1:26657")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fmt.Sprintf("%s", c), "http{http://153.200.0.1:26657}")
|
||||
require.Equal(t, c.ID(), "http{http://153.200.0.1:26657}")
|
||||
|
||||
c, err = lighthttp.New("chain-test", "153.200.0.1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fmt.Sprintf("%s", c), "http{http://153.200.0.1}")
|
||||
require.Equal(t, c.ID(), "http{http://153.200.0.1}")
|
||||
}
|
||||
|
||||
func TestProvider(t *testing.T) {
|
||||
|
||||
@@ -15,6 +15,20 @@ type Provider struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// ID provides a mock function with given fields:
|
||||
func (_m *Provider) ID() string {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// LightBlock provides a mock function with given fields: ctx, height
|
||||
func (_m *Provider) LightBlock(ctx context.Context, height int64) (*types.LightBlock, error) {
|
||||
ret := _m.Called(ctx, height)
|
||||
|
||||
@@ -25,4 +25,8 @@ type Provider interface {
|
||||
|
||||
// ReportEvidence reports an evidence of misbehavior.
|
||||
ReportEvidence(context.Context, types.Evidence) error
|
||||
|
||||
// Returns the ID of a provider. For RPC providers it returns the IP address of the client
|
||||
// For p2p providers it returns a combination of NodeID and IP address
|
||||
ID() string
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ type LightClient interface {
|
||||
Update(ctx context.Context, now time.Time) (*types.LightBlock, error)
|
||||
VerifyLightBlockAtHeight(ctx context.Context, height int64, now time.Time) (*types.LightBlock, error)
|
||||
TrustedLightBlock(height int64) (*types.LightBlock, error)
|
||||
Status(ctx context.Context) *types.LightClientInfo
|
||||
}
|
||||
|
||||
var _ rpcclient.Client = (*Client)(nil)
|
||||
@@ -124,8 +125,18 @@ func (c *Client) OnStop() {
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the status of the light client. Previously this was querying the primary connected to the client
|
||||
// As a consequence of this change, running /status on the light client will return nil for SyncInfo, NodeInfo
|
||||
// and ValdiatorInfo.
|
||||
func (c *Client) Status(ctx context.Context) (*coretypes.ResultStatus, error) {
|
||||
return c.next.Status(ctx)
|
||||
lightClientInfo := c.lc.Status(ctx)
|
||||
|
||||
return &coretypes.ResultStatus{
|
||||
NodeInfo: types.NodeInfo{},
|
||||
SyncInfo: coretypes.SyncInfo{},
|
||||
ValidatorInfo: coretypes.ValidatorInfo{},
|
||||
LightClientInfo: *lightClientInfo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) ABCIInfo(ctx context.Context) (*coretypes.ResultABCIInfo, error) {
|
||||
|
||||
@@ -31,6 +31,22 @@ func (_m *LightClient) ChainID() string {
|
||||
return r0
|
||||
}
|
||||
|
||||
// Status provides a mock function with given fields: ctx
|
||||
func (_m *LightClient) Status(ctx context.Context) *types.LightClientInfo {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
var r0 *types.LightClientInfo
|
||||
if rf, ok := ret.Get(0).(func(context.Context) *types.LightClientInfo); ok {
|
||||
r0 = rf(ctx)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*types.LightClientInfo)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// TrustedLightBlock provides a mock function with given fields: height
|
||||
func (_m *LightClient) TrustedLightBlock(height int64) (*types.LightBlock, error) {
|
||||
ret := _m.Called(height)
|
||||
|
||||
@@ -124,9 +124,10 @@ type ValidatorInfo struct {
|
||||
|
||||
// Node Status
|
||||
type ResultStatus struct {
|
||||
NodeInfo types.NodeInfo `json:"node_info"`
|
||||
SyncInfo SyncInfo `json:"sync_info"`
|
||||
ValidatorInfo ValidatorInfo `json:"validator_info"`
|
||||
NodeInfo types.NodeInfo `json:"node_info"`
|
||||
SyncInfo SyncInfo `json:"sync_info"`
|
||||
ValidatorInfo ValidatorInfo `json:"validator_info"`
|
||||
LightClientInfo types.LightClientInfo `json:"light_client_info,omitempty"`
|
||||
}
|
||||
|
||||
// Is TxIndexing enabled
|
||||
|
||||
@@ -128,6 +128,8 @@ func waitForHeight(ctx context.Context, testnet *e2e.Testnet, height int64) (*ty
|
||||
|
||||
// waitForNode waits for a node to become available and catch up to the given block height.
|
||||
func waitForNode(ctx context.Context, node *e2e.Node, height int64) (*rpctypes.ResultStatus, error) {
|
||||
// If the node is the light client or seed note, we do not check for the last height.
|
||||
// The light client and seed note can be behind the full node and validator
|
||||
if node.Mode == e2e.ModeSeed {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -167,7 +169,10 @@ func waitForNode(ctx context.Context, node *e2e.Node, height int64) (*rpctypes.R
|
||||
return nil, fmt.Errorf("timed out waiting for %v to reach height %v", node.Name, height)
|
||||
case errors.Is(err, context.Canceled):
|
||||
return nil, err
|
||||
case err == nil && status.SyncInfo.LatestBlockHeight >= height:
|
||||
// If the node is the light client, it is not essential to wait for it to catch up, but we must return status info
|
||||
case err == nil && node.Mode == e2e.ModeLight:
|
||||
return status, nil
|
||||
case err == nil && node.Mode != e2e.ModeLight && status.SyncInfo.LatestBlockHeight >= height:
|
||||
return status, nil
|
||||
case counter%500 == 0:
|
||||
switch {
|
||||
|
||||
@@ -118,8 +118,17 @@ func Start(ctx context.Context, testnet *e2e.Testnet) error {
|
||||
wcancel()
|
||||
|
||||
node.HasStarted = true
|
||||
|
||||
var lastNodeHeight int64
|
||||
|
||||
// If the node is a light client, we fetch its current height
|
||||
if node.Mode == e2e.ModeLight {
|
||||
lastNodeHeight = status.LightClientInfo.LastTrustedHeight
|
||||
} else {
|
||||
lastNodeHeight = status.SyncInfo.LatestBlockHeight
|
||||
}
|
||||
logger.Info(fmt.Sprintf("Node %v up on http://127.0.0.1:%v at height %v",
|
||||
node.Name, node.ProxyPort, status.SyncInfo.LatestBlockHeight))
|
||||
node.Name, node.ProxyPort, lastNodeHeight))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -4,10 +4,26 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
tbytes "github.com/tendermint/tendermint/libs/bytes"
|
||||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
||||
)
|
||||
|
||||
// Info about the status of the light client
|
||||
type LightClientInfo struct {
|
||||
PrimaryID string `json:"primaryID"`
|
||||
WitnessesID []string `json:"witnessesID"`
|
||||
NumPeers int `json:"number_of_peers,string"`
|
||||
LastTrustedHeight int64 `json:"last_trusted_height,string"`
|
||||
LastTrustedHash tbytes.HexBytes `json:"last_trusted_hash"`
|
||||
LatestBlockTime time.Time `json:"latest_block_time"`
|
||||
TrustingPeriod string `json:"trusting_period"`
|
||||
// Boolean that reflects whether LatestBlockTime + trusting period is before
|
||||
// time.Now() (time when /status is called)
|
||||
TrustedBlockExpired bool `json:"trusted_block_expired"`
|
||||
}
|
||||
|
||||
// LightBlock is a SignedHeader and a ValidatorSet.
|
||||
// It is the basis of the light client
|
||||
type LightBlock struct {
|
||||
|
||||
Reference in New Issue
Block a user