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