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
+27
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()),
}
}
+5
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)
}
}
}
+90
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
}
+2 -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())
}
+3 -4
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) {
+14
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)
+4
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
}
+12 -1
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) {
+16
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)