Files
tendermint/light/client_test.go
M. J. Fromberger cf7537ea5f cleanup: Reduce and normalize import path aliasing. (#6975)
The code in the Tendermint repository makes heavy use of import aliasing.
This is made necessary by our extensive reuse of common base package names, and
by repetition of similar names across different subdirectories.

Unfortunately we have not been very consistent about which packages we alias in
various circumstances, and the aliases we use vary. In the spirit of the advice
in the style guide and https://github.com/golang/go/wiki/CodeReviewComments#imports,
his change makes an effort to clean up and normalize import aliasing.

This change makes no API or behavioral changes. It is a pure cleanup intended
o help make the code more readable to developers (including myself) trying to
understand what is being imported where.

Only unexported names have been modified, and the changes were generated and
applied mechanically with gofmt -r and comby, respecting the lexical and
syntactic rules of Go.  Even so, I did not fix every inconsistency. Where the
changes would be too disruptive, I left it alone.

The principles I followed in this cleanup are:

- Remove aliases that restate the package name.
- Remove aliases where the base package name is unambiguous.
- Move overly-terse abbreviations from the import to the usage site.
- Fix lexical issues (remove underscores, remove capitalization).
- Fix import groupings to more closely match the style guide.
- Group blank (side-effecting) imports and ensure they are commented.
- Add aliases to multiple imports with the same base package name.
2021-09-23 07:52:07 -07:00

1093 lines
29 KiB
Go

package light_test
import (
"context"
"errors"
"fmt"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
dbm "github.com/tendermint/tm-db"
"github.com/tendermint/tendermint/internal/test/factory"
"github.com/tendermint/tendermint/libs/log"
"github.com/tendermint/tendermint/light"
"github.com/tendermint/tendermint/light/provider"
provider_mocks "github.com/tendermint/tendermint/light/provider/mocks"
dbs "github.com/tendermint/tendermint/light/store/db"
"github.com/tendermint/tendermint/types"
)
const (
chainID = "test"
)
var (
ctx = context.Background()
keys = genPrivKeys(4)
vals = keys.ToValidators(20, 10)
bTime, _ = time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
h1 = keys.GenSignedHeader(chainID, 1, bTime, nil, vals, vals,
hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys))
// 3/3 signed
h2 = keys.GenSignedHeaderLastBlockID(chainID, 2, bTime.Add(30*time.Minute), nil, vals, vals,
hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys), types.BlockID{Hash: h1.Hash()})
// 3/3 signed
h3 = keys.GenSignedHeaderLastBlockID(chainID, 3, bTime.Add(1*time.Hour), nil, vals, vals,
hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys), types.BlockID{Hash: h2.Hash()})
trustPeriod = 4 * time.Hour
trustOptions = light.TrustOptions{
Period: 4 * time.Hour,
Height: 1,
Hash: h1.Hash(),
}
valSet = map[int64]*types.ValidatorSet{
1: vals,
2: vals,
3: vals,
4: vals,
}
headerSet = map[int64]*types.SignedHeader{
1: h1,
// interim header (3/3 signed)
2: h2,
// last header (3/3 signed)
3: h3,
}
l1 = &types.LightBlock{SignedHeader: h1, ValidatorSet: vals}
l2 = &types.LightBlock{SignedHeader: h2, ValidatorSet: vals}
l3 = &types.LightBlock{SignedHeader: h3, ValidatorSet: vals}
)
func TestValidateTrustOptions(t *testing.T) {
testCases := []struct {
err bool
to light.TrustOptions
}{
{
false,
trustOptions,
},
{
true,
light.TrustOptions{
Period: -1 * time.Hour,
Height: 1,
Hash: h1.Hash(),
},
},
{
true,
light.TrustOptions{
Period: 1 * time.Hour,
Height: 0,
Hash: h1.Hash(),
},
},
{
true,
light.TrustOptions{
Period: 1 * time.Hour,
Height: 1,
Hash: []byte("incorrect hash"),
},
},
}
for _, tc := range testCases {
err := tc.to.ValidateBasic()
if tc.err {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
}
}
func TestClient_SequentialVerification(t *testing.T) {
newKeys := genPrivKeys(4)
newVals := newKeys.ToValidators(10, 1)
differentVals, _ := factory.RandValidatorSet(10, 100)
testCases := []struct {
name string
otherHeaders map[int64]*types.SignedHeader // all except ^
vals map[int64]*types.ValidatorSet
initErr bool
verifyErr bool
}{
{
"good",
headerSet,
valSet,
false,
false,
},
{
"bad: different first header",
map[int64]*types.SignedHeader{
// different header
1: keys.GenSignedHeader(chainID, 1, bTime.Add(1*time.Hour), nil, vals, vals,
hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys)),
},
map[int64]*types.ValidatorSet{
1: vals,
},
true,
false,
},
{
"bad: no first signed header",
map[int64]*types.SignedHeader{},
map[int64]*types.ValidatorSet{
1: differentVals,
},
true,
true,
},
{
"bad: different first validator set",
map[int64]*types.SignedHeader{
1: h1,
},
map[int64]*types.ValidatorSet{
1: differentVals,
},
true,
true,
},
{
"bad: 1/3 signed interim header",
map[int64]*types.SignedHeader{
// trusted header
1: h1,
// interim header (1/3 signed)
2: keys.GenSignedHeader(chainID, 2, bTime.Add(1*time.Hour), nil, vals, vals,
hash("app_hash"), hash("cons_hash"), hash("results_hash"), len(keys)-1, len(keys)),
// last header (3/3 signed)
3: keys.GenSignedHeader(chainID, 3, bTime.Add(2*time.Hour), nil, vals, vals,
hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys)),
},
valSet,
false,
true,
},
{
"bad: 1/3 signed last header",
map[int64]*types.SignedHeader{
// trusted header
1: h1,
// interim header (3/3 signed)
2: keys.GenSignedHeader(chainID, 2, bTime.Add(1*time.Hour), nil, vals, vals,
hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys)),
// last header (1/3 signed)
3: keys.GenSignedHeader(chainID, 3, bTime.Add(2*time.Hour), nil, vals, vals,
hash("app_hash"), hash("cons_hash"), hash("results_hash"), len(keys)-1, len(keys)),
},
valSet,
false,
true,
},
{
"bad: different validator set at height 3",
headerSet,
map[int64]*types.ValidatorSet{
1: vals,
2: vals,
3: newVals,
},
false,
true,
},
}
for _, tc := range testCases {
testCase := tc
t.Run(testCase.name, func(t *testing.T) {
mockNode := mockNodeFromHeadersAndVals(testCase.otherHeaders, testCase.vals)
mockNode.On("LightBlock", mock.Anything, mock.Anything).Return(nil, provider.ErrLightBlockNotFound)
c, err := light.NewClient(
ctx,
chainID,
trustOptions,
mockNode,
[]provider.Provider{mockNode},
dbs.New(dbm.NewMemDB()),
light.SequentialVerification(),
light.Logger(log.TestingLogger()),
)
if testCase.initErr {
require.Error(t, err)
return
}
require.NoError(t, err)
_, err = c.VerifyLightBlockAtHeight(ctx, 3, bTime.Add(3*time.Hour))
if testCase.verifyErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
mockNode.AssertExpectations(t)
})
}
}
func TestClient_SkippingVerification(t *testing.T) {
// required for 2nd test case
newKeys := genPrivKeys(4)
newVals := newKeys.ToValidators(10, 1)
// 1/3+ of vals, 2/3- of newVals
transitKeys := keys.Extend(3)
transitVals := transitKeys.ToValidators(10, 1)
testCases := []struct {
name string
otherHeaders map[int64]*types.SignedHeader // all except ^
vals map[int64]*types.ValidatorSet
initErr bool
verifyErr bool
}{
{
"good",
map[int64]*types.SignedHeader{
// trusted header
1: h1,
// last header (3/3 signed)
3: h3,
},
valSet,
false,
false,
},
{
"good, but val set changes by 2/3 (1/3 of vals is still present)",
map[int64]*types.SignedHeader{
// trusted header
1: h1,
3: transitKeys.GenSignedHeader(chainID, 3, bTime.Add(2*time.Hour), nil, transitVals, transitVals,
hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(transitKeys)),
},
map[int64]*types.ValidatorSet{
1: vals,
2: vals,
3: transitVals,
},
false,
false,
},
{
"good, but val set changes 100% at height 2",
map[int64]*types.SignedHeader{
// trusted header
1: h1,
// interim header (3/3 signed)
2: keys.GenSignedHeader(chainID, 2, bTime.Add(1*time.Hour), nil, vals, newVals,
hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys)),
// last header (0/4 of the original val set signed)
3: newKeys.GenSignedHeader(chainID, 3, bTime.Add(2*time.Hour), nil, newVals, newVals,
hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(newKeys)),
},
map[int64]*types.ValidatorSet{
1: vals,
2: vals,
3: newVals,
},
false,
false,
},
{
"bad: last header signed by newVals, interim header has no signers",
map[int64]*types.SignedHeader{
// trusted header
1: h1,
// last header (0/4 of the original val set signed)
2: keys.GenSignedHeader(chainID, 2, bTime.Add(1*time.Hour), nil, vals, newVals,
hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, 0),
// last header (0/4 of the original val set signed)
3: newKeys.GenSignedHeader(chainID, 3, bTime.Add(2*time.Hour), nil, newVals, newVals,
hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(newKeys)),
},
map[int64]*types.ValidatorSet{
1: vals,
2: vals,
3: newVals,
},
false,
true,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
mockNode := mockNodeFromHeadersAndVals(tc.otherHeaders, tc.vals)
mockNode.On("LightBlock", mock.Anything, mock.Anything).Return(nil, provider.ErrLightBlockNotFound)
c, err := light.NewClient(
ctx,
chainID,
trustOptions,
mockNode,
[]provider.Provider{mockNode},
dbs.New(dbm.NewMemDB()),
light.SkippingVerification(light.DefaultTrustLevel),
light.Logger(log.TestingLogger()),
)
if tc.initErr {
require.Error(t, err)
return
}
require.NoError(t, err)
_, err = c.VerifyLightBlockAtHeight(ctx, 3, bTime.Add(3*time.Hour))
if tc.verifyErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
// start from a large light block to make sure that the pivot height doesn't select a height outside
// the appropriate range
func TestClientLargeBisectionVerification(t *testing.T) {
numBlocks := int64(300)
mockHeaders, mockVals, _ := genLightBlocksWithKeys(chainID, numBlocks, 101, 2, bTime)
lastBlock := &types.LightBlock{SignedHeader: mockHeaders[numBlocks], ValidatorSet: mockVals[numBlocks]}
mockNode := &provider_mocks.Provider{}
mockNode.On("LightBlock", mock.Anything, numBlocks).
Return(lastBlock, nil)
mockNode.On("LightBlock", mock.Anything, int64(200)).
Return(&types.LightBlock{SignedHeader: mockHeaders[200], ValidatorSet: mockVals[200]}, nil)
mockNode.On("LightBlock", mock.Anything, int64(256)).
Return(&types.LightBlock{SignedHeader: mockHeaders[256], ValidatorSet: mockVals[256]}, nil)
mockNode.On("LightBlock", mock.Anything, int64(0)).Return(lastBlock, nil)
trustedLightBlock, err := mockNode.LightBlock(ctx, int64(200))
require.NoError(t, err)
c, err := light.NewClient(
ctx,
chainID,
light.TrustOptions{
Period: 4 * time.Hour,
Height: trustedLightBlock.Height,
Hash: trustedLightBlock.Hash(),
},
mockNode,
[]provider.Provider{mockNode},
dbs.New(dbm.NewMemDB()),
light.SkippingVerification(light.DefaultTrustLevel),
)
require.NoError(t, err)
h, err := c.Update(ctx, bTime.Add(300*time.Minute))
assert.NoError(t, err)
height, err := c.LastTrustedHeight()
require.NoError(t, err)
require.Equal(t, numBlocks, height)
h2, err := mockNode.LightBlock(ctx, numBlocks)
require.NoError(t, err)
assert.Equal(t, h, h2)
mockNode.AssertExpectations(t)
}
func TestClientBisectionBetweenTrustedHeaders(t *testing.T) {
mockFullNode := mockNodeFromHeadersAndVals(headerSet, valSet)
c, err := light.NewClient(
ctx,
chainID,
light.TrustOptions{
Period: 4 * time.Hour,
Height: 1,
Hash: h1.Hash(),
},
mockFullNode,
[]provider.Provider{mockFullNode},
dbs.New(dbm.NewMemDB()),
light.SkippingVerification(light.DefaultTrustLevel),
)
require.NoError(t, err)
_, err = c.VerifyLightBlockAtHeight(ctx, 3, bTime.Add(2*time.Hour))
require.NoError(t, err)
// confirm that the client already doesn't have the light block
_, err = c.TrustedLightBlock(2)
require.Error(t, err)
// verify using bisection the light block between the two trusted light blocks
_, err = c.VerifyLightBlockAtHeight(ctx, 2, bTime.Add(1*time.Hour))
assert.NoError(t, err)
mockFullNode.AssertExpectations(t)
}
func TestClient_Cleanup(t *testing.T) {
mockFullNode := &provider_mocks.Provider{}
mockFullNode.On("LightBlock", mock.Anything, int64(1)).Return(l1, nil)
c, err := light.NewClient(
ctx,
chainID,
trustOptions,
mockFullNode,
[]provider.Provider{mockFullNode},
dbs.New(dbm.NewMemDB()),
light.Logger(log.TestingLogger()),
)
require.NoError(t, err)
_, err = c.TrustedLightBlock(1)
require.NoError(t, err)
err = c.Cleanup()
require.NoError(t, err)
// Check no light blocks exist after Cleanup.
l, err := c.TrustedLightBlock(1)
assert.Error(t, err)
assert.Nil(t, l)
mockFullNode.AssertExpectations(t)
}
// trustedHeader.Height == options.Height
func TestClientRestoresTrustedHeaderAfterStartup(t *testing.T) {
// 1. options.Hash == trustedHeader.Hash
t.Run("hashes should match", func(t *testing.T) {
mockNode := &provider_mocks.Provider{}
trustedStore := dbs.New(dbm.NewMemDB())
err := trustedStore.SaveLightBlock(l1)
require.NoError(t, err)
c, err := light.NewClient(
ctx,
chainID,
trustOptions,
mockNode,
[]provider.Provider{mockNode},
trustedStore,
light.Logger(log.TestingLogger()),
)
require.NoError(t, err)
l, err := c.TrustedLightBlock(1)
assert.NoError(t, err)
assert.NotNil(t, l)
assert.Equal(t, l.Hash(), h1.Hash())
assert.Equal(t, l.ValidatorSet.Hash(), h1.ValidatorsHash.Bytes())
mockNode.AssertExpectations(t)
})
// 2. options.Hash != trustedHeader.Hash
t.Run("hashes should not match", func(t *testing.T) {
trustedStore := dbs.New(dbm.NewMemDB())
err := trustedStore.SaveLightBlock(l1)
require.NoError(t, err)
// header1 != h1
header1 := keys.GenSignedHeader(chainID, 1, bTime.Add(1*time.Hour), nil, vals, vals,
hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys))
mockNode := &provider_mocks.Provider{}
c, err := light.NewClient(
ctx,
chainID,
light.TrustOptions{
Period: 4 * time.Hour,
Height: 1,
Hash: header1.Hash(),
},
mockNode,
[]provider.Provider{mockNode},
trustedStore,
light.Logger(log.TestingLogger()),
)
require.NoError(t, err)
l, err := c.TrustedLightBlock(1)
assert.NoError(t, err)
if assert.NotNil(t, l) {
// client take the trusted store and ignores the trusted options
assert.Equal(t, l.Hash(), l1.Hash())
assert.NoError(t, l.ValidateBasic(chainID))
}
mockNode.AssertExpectations(t)
})
}
func TestClient_Update(t *testing.T) {
mockFullNode := &provider_mocks.Provider{}
mockFullNode.On("LightBlock", mock.Anything, int64(0)).Return(l3, nil)
mockFullNode.On("LightBlock", mock.Anything, int64(1)).Return(l1, nil)
mockFullNode.On("LightBlock", mock.Anything, int64(3)).Return(l3, nil)
c, err := light.NewClient(
ctx,
chainID,
trustOptions,
mockFullNode,
[]provider.Provider{mockFullNode},
dbs.New(dbm.NewMemDB()),
light.Logger(log.TestingLogger()),
)
require.NoError(t, err)
// should result in downloading & verifying header #3
l, err := c.Update(ctx, bTime.Add(2*time.Hour))
assert.NoError(t, err)
if assert.NotNil(t, l) {
assert.EqualValues(t, 3, l.Height)
assert.NoError(t, l.ValidateBasic(chainID))
}
mockFullNode.AssertExpectations(t)
}
func TestClient_Concurrency(t *testing.T) {
mockFullNode := &provider_mocks.Provider{}
mockFullNode.On("LightBlock", mock.Anything, int64(2)).Return(l2, nil)
mockFullNode.On("LightBlock", mock.Anything, int64(1)).Return(l1, nil)
c, err := light.NewClient(
ctx,
chainID,
trustOptions,
mockFullNode,
[]provider.Provider{mockFullNode},
dbs.New(dbm.NewMemDB()),
light.Logger(log.TestingLogger()),
)
require.NoError(t, err)
_, err = c.VerifyLightBlockAtHeight(ctx, 2, bTime.Add(2*time.Hour))
require.NoError(t, err)
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// NOTE: Cleanup, Stop, VerifyLightBlockAtHeight and Verify are not supposed
// to be concurrently safe.
assert.Equal(t, chainID, c.ChainID())
_, err := c.LastTrustedHeight()
assert.NoError(t, err)
_, err = c.FirstTrustedHeight()
assert.NoError(t, err)
l, err := c.TrustedLightBlock(1)
assert.NoError(t, err)
assert.NotNil(t, l)
}()
}
wg.Wait()
mockFullNode.AssertExpectations(t)
}
func TestClient_AddProviders(t *testing.T) {
mockFullNode := mockNodeFromHeadersAndVals(map[int64]*types.SignedHeader{
1: h1,
2: h2,
}, valSet)
c, err := light.NewClient(
ctx,
chainID,
trustOptions,
mockFullNode,
[]provider.Provider{mockFullNode},
dbs.New(dbm.NewMemDB()),
light.Logger(log.TestingLogger()),
)
require.NoError(t, err)
closeCh := make(chan struct{})
go func() {
// run verification concurrently to make sure it doesn't dead lock
_, err = c.VerifyLightBlockAtHeight(ctx, 2, bTime.Add(2*time.Hour))
require.NoError(t, err)
close(closeCh)
}()
// NOTE: the light client doesn't check uniqueness of providers
c.AddProvider(mockFullNode)
require.Len(t, c.Witnesses(), 2)
select {
case <-closeCh:
case <-time.After(5 * time.Second):
t.Fatal("concurent light block verification failed to finish in 5s")
}
mockFullNode.AssertExpectations(t)
}
func TestClientReplacesPrimaryWithWitnessIfPrimaryIsUnavailable(t *testing.T) {
mockFullNode := &provider_mocks.Provider{}
mockFullNode.On("LightBlock", mock.Anything, mock.Anything).Return(l1, nil)
mockDeadNode := &provider_mocks.Provider{}
mockDeadNode.On("LightBlock", mock.Anything, mock.Anything).Return(nil, provider.ErrNoResponse)
c, err := light.NewClient(
ctx,
chainID,
trustOptions,
mockDeadNode,
[]provider.Provider{mockDeadNode, mockFullNode},
dbs.New(dbm.NewMemDB()),
light.Logger(log.TestingLogger()),
)
require.NoError(t, err)
_, err = c.Update(ctx, bTime.Add(2*time.Hour))
require.NoError(t, err)
// the primary should no longer be the deadNode
assert.NotEqual(t, c.Primary(), mockDeadNode)
// we should still have the dead node as a witness because it
// hasn't repeatedly been unresponsive yet
assert.Equal(t, 2, len(c.Witnesses()))
mockDeadNode.AssertExpectations(t)
mockFullNode.AssertExpectations(t)
}
func TestClientReplacesPrimaryWithWitnessIfPrimaryDoesntHaveBlock(t *testing.T) {
mockFullNode := &provider_mocks.Provider{}
mockFullNode.On("LightBlock", mock.Anything, mock.Anything).Return(l1, nil)
mockDeadNode := &provider_mocks.Provider{}
mockDeadNode.On("LightBlock", mock.Anything, mock.Anything).Return(nil, provider.ErrLightBlockNotFound)
c, err := light.NewClient(
ctx,
chainID,
trustOptions,
mockDeadNode,
[]provider.Provider{mockDeadNode, mockFullNode},
dbs.New(dbm.NewMemDB()),
light.Logger(log.TestingLogger()),
)
require.NoError(t, err)
_, err = c.Update(ctx, bTime.Add(2*time.Hour))
require.NoError(t, err)
// we should still have the dead node as a witness because it
// hasn't repeatedly been unresponsive yet
assert.Equal(t, 2, len(c.Witnesses()))
mockDeadNode.AssertExpectations(t)
mockFullNode.AssertExpectations(t)
}
func TestClient_BackwardsVerification(t *testing.T) {
{
headers, vals, _ := genLightBlocksWithKeys(chainID, 9, 3, 0, bTime)
delete(headers, 1)
delete(headers, 2)
delete(vals, 1)
delete(vals, 2)
mockLargeFullNode := mockNodeFromHeadersAndVals(headers, vals)
trustHeader, _ := mockLargeFullNode.LightBlock(ctx, 6)
c, err := light.NewClient(
ctx,
chainID,
light.TrustOptions{
Period: 4 * time.Minute,
Height: trustHeader.Height,
Hash: trustHeader.Hash(),
},
mockLargeFullNode,
[]provider.Provider{mockLargeFullNode},
dbs.New(dbm.NewMemDB()),
light.Logger(log.TestingLogger()),
)
require.NoError(t, err)
// 1) verify before the trusted header using backwards => expect no error
h, err := c.VerifyLightBlockAtHeight(ctx, 5, bTime.Add(6*time.Minute))
require.NoError(t, err)
if assert.NotNil(t, h) {
assert.EqualValues(t, 5, h.Height)
}
// 2) untrusted header is expired but trusted header is not => expect no error
h, err = c.VerifyLightBlockAtHeight(ctx, 3, bTime.Add(8*time.Minute))
assert.NoError(t, err)
assert.NotNil(t, h)
// 3) already stored headers should return the header without error
h, err = c.VerifyLightBlockAtHeight(ctx, 5, bTime.Add(6*time.Minute))
assert.NoError(t, err)
assert.NotNil(t, h)
// 4a) First verify latest header
_, err = c.VerifyLightBlockAtHeight(ctx, 9, bTime.Add(9*time.Minute))
require.NoError(t, err)
// 4b) Verify backwards using bisection => expect no error
_, err = c.VerifyLightBlockAtHeight(ctx, 7, bTime.Add(9*time.Minute))
assert.NoError(t, err)
// shouldn't have verified this header in the process
_, err = c.TrustedLightBlock(8)
assert.Error(t, err)
// 5) Try bisection method, but closest header (at 7) has expired
// so expect error
_, err = c.VerifyLightBlockAtHeight(ctx, 8, bTime.Add(12*time.Minute))
assert.Error(t, err)
mockLargeFullNode.AssertExpectations(t)
}
{
// 8) provides incorrect hash
headers := map[int64]*types.SignedHeader{
2: keys.GenSignedHeader(chainID, 2, bTime.Add(30*time.Minute), nil, vals, vals,
hash("app_hash2"), hash("cons_hash23"), hash("results_hash30"), 0, len(keys)),
3: h3,
}
vals := valSet
mockNode := mockNodeFromHeadersAndVals(headers, vals)
c, err := light.NewClient(
ctx,
chainID,
light.TrustOptions{
Period: 1 * time.Hour,
Height: 3,
Hash: h3.Hash(),
},
mockNode,
[]provider.Provider{mockNode},
dbs.New(dbm.NewMemDB()),
light.Logger(log.TestingLogger()),
)
require.NoError(t, err)
_, err = c.VerifyLightBlockAtHeight(ctx, 2, bTime.Add(1*time.Hour).Add(1*time.Second))
assert.Error(t, err)
mockNode.AssertExpectations(t)
}
}
func TestClient_NewClientFromTrustedStore(t *testing.T) {
// 1) Initiate DB and fill with a "trusted" header
db := dbs.New(dbm.NewMemDB())
err := db.SaveLightBlock(l1)
require.NoError(t, err)
mockNode := &provider_mocks.Provider{}
c, err := light.NewClientFromTrustedStore(
chainID,
trustPeriod,
mockNode,
[]provider.Provider{mockNode},
db,
)
require.NoError(t, err)
// 2) Check light block exists
h, err := c.TrustedLightBlock(1)
assert.NoError(t, err)
assert.EqualValues(t, l1.Height, h.Height)
mockNode.AssertExpectations(t)
}
func TestClientRemovesWitnessIfItSendsUsIncorrectHeader(t *testing.T) {
// different headers hash then primary plus less than 1/3 signed (no fork)
headers1 := map[int64]*types.SignedHeader{
1: h1,
2: keys.GenSignedHeaderLastBlockID(chainID, 2, bTime.Add(30*time.Minute), nil, vals, vals,
hash("app_hash2"), hash("cons_hash"), hash("results_hash"),
len(keys), len(keys), types.BlockID{Hash: h1.Hash()}),
}
vals1 := map[int64]*types.ValidatorSet{
1: vals,
2: vals,
}
mockBadNode1 := mockNodeFromHeadersAndVals(headers1, vals1)
mockBadNode1.On("LightBlock", mock.Anything, mock.Anything).Return(nil, provider.ErrLightBlockNotFound)
// header is empty
headers2 := map[int64]*types.SignedHeader{
1: h1,
2: h2,
}
vals2 := map[int64]*types.ValidatorSet{
1: vals,
2: vals,
}
mockBadNode2 := mockNodeFromHeadersAndVals(headers2, vals2)
mockBadNode2.On("LightBlock", mock.Anything, mock.Anything).Return(nil, provider.ErrLightBlockNotFound)
mockFullNode := mockNodeFromHeadersAndVals(headerSet, valSet)
lb1, _ := mockBadNode1.LightBlock(ctx, 2)
require.NotEqual(t, lb1.Hash(), l1.Hash())
c, err := light.NewClient(
ctx,
chainID,
trustOptions,
mockFullNode,
[]provider.Provider{mockBadNode1, mockBadNode2},
dbs.New(dbm.NewMemDB()),
light.Logger(log.TestingLogger()),
)
// witness should have behaved properly -> no error
require.NoError(t, err)
assert.EqualValues(t, 2, len(c.Witnesses()))
// witness behaves incorrectly -> removed from list, no error
l, err := c.VerifyLightBlockAtHeight(ctx, 2, bTime.Add(2*time.Hour))
assert.NoError(t, err)
assert.EqualValues(t, 1, len(c.Witnesses()))
// light block should still be verified
assert.EqualValues(t, 2, l.Height)
// remaining witnesses don't have light block -> error
_, err = c.VerifyLightBlockAtHeight(ctx, 3, bTime.Add(2*time.Hour))
if assert.Error(t, err) {
assert.Equal(t, light.ErrFailedHeaderCrossReferencing, err)
}
// witness does not have a light block -> left in the list
assert.EqualValues(t, 1, len(c.Witnesses()))
mockBadNode1.AssertExpectations(t)
mockBadNode2.AssertExpectations(t)
}
func TestClient_TrustedValidatorSet(t *testing.T) {
differentVals, _ := factory.RandValidatorSet(10, 100)
mockBadValSetNode := mockNodeFromHeadersAndVals(
map[int64]*types.SignedHeader{
1: h1,
// 3/3 signed, but validator set at height 2 below is invalid -> witness
// should be removed.
2: keys.GenSignedHeaderLastBlockID(chainID, 2, bTime.Add(30*time.Minute), nil, vals, vals,
hash("app_hash2"), hash("cons_hash"), hash("results_hash"),
0, len(keys), types.BlockID{Hash: h1.Hash()}),
},
map[int64]*types.ValidatorSet{
1: vals,
2: differentVals,
})
mockFullNode := mockNodeFromHeadersAndVals(
map[int64]*types.SignedHeader{
1: h1,
2: h2,
},
map[int64]*types.ValidatorSet{
1: vals,
2: vals,
})
c, err := light.NewClient(
ctx,
chainID,
trustOptions,
mockFullNode,
[]provider.Provider{mockBadValSetNode, mockFullNode},
dbs.New(dbm.NewMemDB()),
light.Logger(log.TestingLogger()),
)
require.NoError(t, err)
assert.Equal(t, 2, len(c.Witnesses()))
_, err = c.VerifyLightBlockAtHeight(ctx, 2, bTime.Add(2*time.Hour).Add(1*time.Second))
assert.NoError(t, err)
assert.Equal(t, 1, len(c.Witnesses()))
mockBadValSetNode.AssertExpectations(t)
mockFullNode.AssertExpectations(t)
}
func TestClientPrunesHeadersAndValidatorSets(t *testing.T) {
mockFullNode := mockNodeFromHeadersAndVals(
map[int64]*types.SignedHeader{
1: h1,
3: h3,
0: h3,
},
map[int64]*types.ValidatorSet{
1: vals,
3: vals,
0: vals,
})
c, err := light.NewClient(
ctx,
chainID,
trustOptions,
mockFullNode,
[]provider.Provider{mockFullNode},
dbs.New(dbm.NewMemDB()),
light.Logger(log.TestingLogger()),
light.PruningSize(1),
)
require.NoError(t, err)
_, err = c.TrustedLightBlock(1)
require.NoError(t, err)
h, err := c.Update(ctx, bTime.Add(2*time.Hour))
require.NoError(t, err)
require.Equal(t, int64(3), h.Height)
_, err = c.TrustedLightBlock(1)
assert.Error(t, err)
mockFullNode.AssertExpectations(t)
}
func TestClientEnsureValidHeadersAndValSets(t *testing.T) {
emptyValSet := &types.ValidatorSet{
Validators: nil,
Proposer: nil,
}
testCases := []struct {
headers map[int64]*types.SignedHeader
vals map[int64]*types.ValidatorSet
errorToThrow error
errorHeight int64
err bool
}{
{
headers: map[int64]*types.SignedHeader{
1: h1,
3: h3,
},
vals: map[int64]*types.ValidatorSet{
1: vals,
3: vals,
},
err: false,
},
{
headers: map[int64]*types.SignedHeader{
1: h1,
},
vals: map[int64]*types.ValidatorSet{
1: vals,
},
errorToThrow: provider.ErrBadLightBlock{Reason: errors.New("nil header or vals")},
errorHeight: 3,
err: true,
},
{
headers: map[int64]*types.SignedHeader{
1: h1,
},
errorToThrow: provider.ErrBadLightBlock{Reason: errors.New("nil header or vals")},
errorHeight: 3,
vals: valSet,
err: true,
},
{
headers: map[int64]*types.SignedHeader{
1: h1,
3: h3,
},
vals: map[int64]*types.ValidatorSet{
1: vals,
3: emptyValSet,
},
err: true,
},
}
for i, tc := range testCases {
testCase := tc
t.Run(fmt.Sprintf("case: %d", i), func(t *testing.T) {
mockBadNode := mockNodeFromHeadersAndVals(testCase.headers, testCase.vals)
if testCase.errorToThrow != nil {
mockBadNode.On("LightBlock", mock.Anything, testCase.errorHeight).Return(nil, testCase.errorToThrow)
}
c, err := light.NewClient(
ctx,
chainID,
trustOptions,
mockBadNode,
[]provider.Provider{mockBadNode, mockBadNode},
dbs.New(dbm.NewMemDB()),
)
require.NoError(t, err)
_, err = c.VerifyLightBlockAtHeight(ctx, 3, bTime.Add(2*time.Hour))
if testCase.err {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
mockBadNode.AssertExpectations(t)
})
}
}
func TestClientHandlesContexts(t *testing.T) {
mockNode := &provider_mocks.Provider{}
mockNode.On("LightBlock",
mock.MatchedBy(func(ctx context.Context) bool { return ctx.Err() == nil }),
int64(1)).Return(l1, nil)
mockNode.On("LightBlock",
mock.MatchedBy(func(ctx context.Context) bool { return ctx.Err() == context.DeadlineExceeded }),
mock.Anything).Return(nil, context.DeadlineExceeded)
mockNode.On("LightBlock",
mock.MatchedBy(func(ctx context.Context) bool { return ctx.Err() == context.Canceled }),
mock.Anything).Return(nil, context.Canceled)
// instantiate the light client with a timeout
ctxTimeOut, cancel := context.WithTimeout(ctx, 1*time.Nanosecond)
defer cancel()
_, err := light.NewClient(
ctxTimeOut,
chainID,
trustOptions,
mockNode,
[]provider.Provider{mockNode, mockNode},
dbs.New(dbm.NewMemDB()),
)
require.Error(t, ctxTimeOut.Err())
require.Error(t, err)
require.True(t, errors.Is(err, context.DeadlineExceeded))
// instantiate the client for real
c, err := light.NewClient(
ctx,
chainID,
trustOptions,
mockNode,
[]provider.Provider{mockNode, mockNode},
dbs.New(dbm.NewMemDB()),
)
require.NoError(t, err)
// verify a block with a timeout
ctxTimeOutBlock, cancel := context.WithTimeout(ctx, 1*time.Nanosecond)
defer cancel()
_, err = c.VerifyLightBlockAtHeight(ctxTimeOutBlock, 100, bTime.Add(100*time.Minute))
require.Error(t, ctxTimeOutBlock.Err())
require.Error(t, err)
require.True(t, errors.Is(err, context.DeadlineExceeded))
// verify a block with a cancel
ctxCancel, cancel := context.WithCancel(ctx)
cancel()
_, err = c.VerifyLightBlockAtHeight(ctxCancel, 100, bTime.Add(100*time.Minute))
require.Error(t, ctxCancel.Err())
require.Error(t, err)
require.True(t, errors.Is(err, context.Canceled))
mockNode.AssertExpectations(t)
}