Browse Source

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()
pull/7647/head
Jasmina Malicevic 3 years ago
committed by GitHub
parent
commit
d68d25dcd5
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 213 additions and 11 deletions
  1. +1
    -0
      CHANGELOG_PENDING.md
  2. +3
    -0
      internal/statesync/dispatcher.go
  3. +27
    -0
      light/client.go
  4. +5
    -0
      light/client_benchmark_test.go
  5. +90
    -0
      light/light_test.go
  6. +2
    -1
      light/provider/http/http.go
  7. +3
    -4
      light/provider/http/http_test.go
  8. +14
    -0
      light/provider/mocks/provider.go
  9. +4
    -0
      light/provider/provider.go
  10. +12
    -1
      light/rpc/client.go
  11. +16
    -0
      light/rpc/mocks/light_client.go
  12. +4
    -3
      rpc/coretypes/responses.go
  13. +6
    -1
      test/e2e/runner/rpc.go
  14. +10
    -1
      test/e2e/runner/start.go
  15. +16
    -0
      types/light.go

+ 1
- 0
CHANGELOG_PENDING.md View File

@ -55,6 +55,7 @@ Special thanks to external contributors on this release:
- [pubsub] \#7319 Performance improvements for the event query API (@creachadair) - [pubsub] \#7319 Performance improvements for the event query API (@creachadair)
- [node] \#7521 Define concrete type for seed node implementation (@spacech1mp) - [node] \#7521 Define concrete type for seed node implementation (@spacech1mp)
- [rpc] \#7612 paginate mempool /unconfirmed_txs rpc endpoint (@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 ### BUG FIXES


+ 3
- 0
internal/statesync/dispatcher.go View File

@ -224,6 +224,9 @@ func (p *BlockProvider) ReportEvidence(ctx context.Context, ev types.Evidence) e
// String implements stringer interface // String implements stringer interface
func (p *BlockProvider) String() string { return string(p.peer) } 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 // peerList is a rolling list of peers. This is used to distribute the load of


+ 27
- 0
light/client.go View File

@ -13,6 +13,7 @@ import (
tmmath "github.com/tendermint/tendermint/libs/math" tmmath "github.com/tendermint/tendermint/libs/math"
"github.com/tendermint/tendermint/light/provider" "github.com/tendermint/tendermint/light/provider"
"github.com/tendermint/tendermint/light/store" "github.com/tendermint/tendermint/light/store"
"github.com/tendermint/tendermint/types" "github.com/tendermint/tendermint/types"
) )
@ -1146,3 +1147,29 @@ func (c *Client) providerShouldBeRemoved(err error) bool {
errors.As(err, &provider.ErrBadLightBlock{}) || errors.As(err, &provider.ErrBadLightBlock{}) ||
errors.Is(err, provider.ErrConnectionClosed) 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
- 0
light/client_benchmark_test.go View File

@ -61,6 +61,10 @@ func (impl *providerBenchmarkImpl) ReportEvidence(_ context.Context, _ types.Evi
return errors.New("not implemented") 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) { func BenchmarkSequence(b *testing.B) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
@ -168,4 +172,5 @@ func BenchmarkBackwards(b *testing.B) {
b.Fatal(err) b.Fatal(err)
} }
} }
} }

+ 90
- 0
light/light_test.go 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
light/provider/http/http.go 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()) return fmt.Sprintf("http{%s}", p.client.Remote())
} }


+ 3
- 4
light/provider/http/http_test.go View File

@ -3,7 +3,6 @@ package http_test
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"testing" "testing"
"time" "time"
@ -22,15 +21,15 @@ import (
func TestNewProvider(t *testing.T) { func TestNewProvider(t *testing.T) {
c, err := lighthttp.New("chain-test", "192.168.0.1:26657") c, err := lighthttp.New("chain-test", "192.168.0.1:26657")
require.NoError(t, err) 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") c, err = lighthttp.New("chain-test", "http://153.200.0.1:26657")
require.NoError(t, err) 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") c, err = lighthttp.New("chain-test", "153.200.0.1")
require.NoError(t, err) 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) { func TestProvider(t *testing.T) {


+ 14
- 0
light/provider/mocks/provider.go View File

@ -15,6 +15,20 @@ type Provider struct {
mock.Mock 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 // LightBlock provides a mock function with given fields: ctx, height
func (_m *Provider) LightBlock(ctx context.Context, height int64) (*types.LightBlock, error) { func (_m *Provider) LightBlock(ctx context.Context, height int64) (*types.LightBlock, error) {
ret := _m.Called(ctx, height) ret := _m.Called(ctx, height)


+ 4
- 0
light/provider/provider.go View File

@ -25,4 +25,8 @@ type Provider interface {
// ReportEvidence reports an evidence of misbehavior. // ReportEvidence reports an evidence of misbehavior.
ReportEvidence(context.Context, types.Evidence) error 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
light/rpc/client.go View File

@ -32,6 +32,7 @@ type LightClient interface {
Update(ctx context.Context, now time.Time) (*types.LightBlock, error) Update(ctx context.Context, now time.Time) (*types.LightBlock, error)
VerifyLightBlockAtHeight(ctx context.Context, height int64, now time.Time) (*types.LightBlock, error) VerifyLightBlockAtHeight(ctx context.Context, height int64, now time.Time) (*types.LightBlock, error)
TrustedLightBlock(height int64) (*types.LightBlock, error) TrustedLightBlock(height int64) (*types.LightBlock, error)
Status(ctx context.Context) *types.LightClientInfo
} }
var _ rpcclient.Client = (*Client)(nil) 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) { 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) { func (c *Client) ABCIInfo(ctx context.Context) (*coretypes.ResultABCIInfo, error) {


+ 16
- 0
light/rpc/mocks/light_client.go View File

@ -31,6 +31,22 @@ func (_m *LightClient) ChainID() string {
return r0 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 // TrustedLightBlock provides a mock function with given fields: height
func (_m *LightClient) TrustedLightBlock(height int64) (*types.LightBlock, error) { func (_m *LightClient) TrustedLightBlock(height int64) (*types.LightBlock, error) {
ret := _m.Called(height) ret := _m.Called(height)


+ 4
- 3
rpc/coretypes/responses.go View File

@ -124,9 +124,10 @@ type ValidatorInfo struct {
// Node Status // Node Status
type ResultStatus struct { 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 // Is TxIndexing enabled


+ 6
- 1
test/e2e/runner/rpc.go 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. // 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) { 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 { if node.Mode == e2e.ModeSeed {
return nil, nil 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) return nil, fmt.Errorf("timed out waiting for %v to reach height %v", node.Name, height)
case errors.Is(err, context.Canceled): case errors.Is(err, context.Canceled):
return nil, err 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 return status, nil
case counter%500 == 0: case counter%500 == 0:
switch { switch {


+ 10
- 1
test/e2e/runner/start.go View File

@ -118,8 +118,17 @@ func Start(ctx context.Context, testnet *e2e.Testnet) error {
wcancel() wcancel()
node.HasStarted = true 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", 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 return nil


+ 16
- 0
types/light.go View File

@ -4,10 +4,26 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"time"
tbytes "github.com/tendermint/tendermint/libs/bytes"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types" 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. // LightBlock is a SignedHeader and a ValidatorSet.
// It is the basis of the light client // It is the basis of the light client
type LightBlock struct { type LightBlock struct {


Loading…
Cancel
Save