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:
Jasmina Malicevic
2022-01-20 14:53:20 +01:00
committed by GitHub
parent 4e5c2b5e8f
commit d68d25dcd5
15 changed files with 213 additions and 11 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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()),
}
}

View File

@@ -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)
}
}
}

View File

@@ -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
}

View File

@@ -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())
}

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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

View File

@@ -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 {

View File

@@ -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

View File

@@ -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 {