mirror of
https://github.com/tendermint/tendermint.git
synced 2026-01-07 05:46:32 +00:00
This pull request merges in the changes for implementing Proposer-based timestamps into `master`. The power was primarily being done in the `wb/proposer-based-timestamps` branch, with changes being merged into that branch during development. This pull request represents an amalgamation of the changes made into that development branch. All of the changes that were placed into that branch have been cleanly rebased on top of the latest `master`. The changes compile and the tests pass insofar as our tests in general pass. ### Note To Reviewers These changes have been extensively reviewed during development. There is not much new here. In the interest of making effective use of time, I would recommend against trying to perform a complete audit of the changes presented and instead examine for mistakes that may have occurred during the process of rebasing the changes. I gave the complete change set a first pass for any issues, but additional eyes would be very appreciated. In sum, this change set does the following: closes #6942 merges in #6849
1117 lines
30 KiB
Go
1117 lines
30 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 bTime time.Time
|
|
|
|
func init() {
|
|
var err error
|
|
bTime, err = time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func TestClient(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
var (
|
|
keys = genPrivKeys(4)
|
|
vals = keys.ToValidators(20, 10)
|
|
trustPeriod = 4 * time.Hour
|
|
|
|
valSet = map[int64]*types.ValidatorSet{
|
|
1: vals,
|
|
2: vals,
|
|
3: vals,
|
|
4: vals,
|
|
}
|
|
|
|
h1 = keys.GenSignedHeader(t, chainID, 1, bTime, nil, vals, vals,
|
|
hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys))
|
|
// 3/3 signed
|
|
h2 = keys.GenSignedHeaderLastBlockID(t, 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(t, 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()})
|
|
trustOptions = light.TrustOptions{
|
|
Period: 4 * time.Hour,
|
|
Height: 1,
|
|
Hash: h1.Hash(),
|
|
}
|
|
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}
|
|
)
|
|
t.Run("ValidateTrustOptions", func(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 idx, tc := range testCases {
|
|
t.Run(fmt.Sprint(idx), func(t *testing.T) {
|
|
err := tc.to.ValidateBasic()
|
|
if tc.err {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
t.Run("SequentialVerification", func(t *testing.T) {
|
|
newKeys := genPrivKeys(4)
|
|
newVals := newKeys.ToValidators(10, 1)
|
|
differentVals, _ := factory.ValidatorSet(ctx, t, 10, 100)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
otherHeaders map[int64]*types.SignedHeader // all except ^
|
|
vals map[int64]*types.ValidatorSet
|
|
initErr bool
|
|
verifyErr bool
|
|
}{
|
|
{
|
|
name: "good",
|
|
otherHeaders: headerSet,
|
|
vals: valSet,
|
|
initErr: false,
|
|
verifyErr: false,
|
|
},
|
|
{
|
|
"bad: different first header",
|
|
map[int64]*types.SignedHeader{
|
|
// different header
|
|
1: keys.GenSignedHeader(t, 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(t, 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(t, 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(t, 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(t, 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) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
logger := log.NewTestingLogger(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(logger),
|
|
)
|
|
|
|
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)
|
|
})
|
|
}
|
|
|
|
})
|
|
t.Run("SkippingVerification", func(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(t, 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(t, 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(t, 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(t, 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(t, 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,
|
|
},
|
|
}
|
|
|
|
bctx, bcancel := context.WithCancel(context.Background())
|
|
defer bcancel()
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(bctx)
|
|
defer cancel()
|
|
logger := log.NewTestingLogger(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(logger),
|
|
)
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
|
|
})
|
|
t.Run("LargeBisectionVerification", func(t *testing.T) {
|
|
// start from a large light block to make sure that the pivot height doesn't select a height outside
|
|
// the appropriate range
|
|
|
|
numBlocks := int64(300)
|
|
mockHeaders, mockVals, _ := genLightBlocksWithKeys(t, 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)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
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)
|
|
})
|
|
t.Run("BisectionBetweenTrustedHeaders", func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
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)
|
|
})
|
|
t.Run("Cleanup", func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
logger := log.NewTestingLogger(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(logger),
|
|
)
|
|
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)
|
|
})
|
|
t.Run("RestoresTrustedHeaderAfterStartup", func(t *testing.T) {
|
|
// trustedHeader.Height == options.Height
|
|
|
|
bctx, bcancel := context.WithCancel(context.Background())
|
|
defer bcancel()
|
|
|
|
// 1. options.Hash == trustedHeader.Hash
|
|
t.Run("hashes should match", func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(bctx)
|
|
defer cancel()
|
|
|
|
logger := log.NewTestingLogger(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(logger),
|
|
)
|
|
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) {
|
|
ctx, cancel := context.WithCancel(bctx)
|
|
defer cancel()
|
|
|
|
trustedStore := dbs.New(dbm.NewMemDB())
|
|
err := trustedStore.SaveLightBlock(l1)
|
|
require.NoError(t, err)
|
|
|
|
logger := log.NewTestingLogger(t)
|
|
|
|
// header1 != h1
|
|
header1 := keys.GenSignedHeader(t, 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(logger),
|
|
)
|
|
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)
|
|
})
|
|
})
|
|
t.Run("Update", func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
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)
|
|
|
|
logger := log.NewTestingLogger(t)
|
|
|
|
c, err := light.NewClient(
|
|
ctx,
|
|
chainID,
|
|
trustOptions,
|
|
mockFullNode,
|
|
[]provider.Provider{mockFullNode},
|
|
dbs.New(dbm.NewMemDB()),
|
|
light.Logger(logger),
|
|
)
|
|
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)
|
|
})
|
|
|
|
t.Run("Concurrency", func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
logger := log.NewTestingLogger(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(logger),
|
|
)
|
|
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)
|
|
})
|
|
t.Run("AddProviders", func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
mockFullNode := mockNodeFromHeadersAndVals(map[int64]*types.SignedHeader{
|
|
1: h1,
|
|
2: h2,
|
|
}, valSet)
|
|
logger := log.NewTestingLogger(t)
|
|
|
|
c, err := light.NewClient(
|
|
ctx,
|
|
chainID,
|
|
trustOptions,
|
|
mockFullNode,
|
|
[]provider.Provider{mockFullNode},
|
|
dbs.New(dbm.NewMemDB()),
|
|
light.Logger(logger),
|
|
)
|
|
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)
|
|
})
|
|
t.Run("ReplacesPrimaryWithWitnessIfPrimaryIsUnavailable", func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
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)
|
|
|
|
logger := log.NewTestingLogger(t)
|
|
|
|
c, err := light.NewClient(
|
|
ctx,
|
|
chainID,
|
|
trustOptions,
|
|
mockDeadNode,
|
|
[]provider.Provider{mockDeadNode, mockFullNode},
|
|
dbs.New(dbm.NewMemDB()),
|
|
light.Logger(logger),
|
|
)
|
|
|
|
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)
|
|
})
|
|
t.Run("ReplacesPrimaryWithWitnessIfPrimaryDoesntHaveBlock", func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
mockFullNode := &provider_mocks.Provider{}
|
|
mockFullNode.On("LightBlock", mock.Anything, mock.Anything).Return(l1, nil)
|
|
|
|
logger := log.NewTestingLogger(t)
|
|
|
|
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(logger),
|
|
)
|
|
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)
|
|
})
|
|
t.Run("BackwardsVerification", func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
logger := log.NewTestingLogger(t)
|
|
|
|
{
|
|
headers, vals, _ := genLightBlocksWithKeys(t, 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(logger),
|
|
)
|
|
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(t, 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(logger),
|
|
)
|
|
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)
|
|
}
|
|
})
|
|
t.Run("NewClientFromTrustedStore", func(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)
|
|
})
|
|
t.Run("RemovesWitnessIfItSendsUsIncorrectHeader", func(t *testing.T) {
|
|
logger := log.NewTestingLogger(t)
|
|
|
|
// different headers hash then primary plus less than 1/3 signed (no fork)
|
|
headers1 := map[int64]*types.SignedHeader{
|
|
1: h1,
|
|
2: keys.GenSignedHeaderLastBlockID(t, 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)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
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(logger),
|
|
)
|
|
// 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)
|
|
})
|
|
t.Run("TrustedValidatorSet", func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
logger := log.NewTestingLogger(t)
|
|
|
|
differentVals, _ := factory.ValidatorSet(ctx, t, 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(t, 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(logger),
|
|
)
|
|
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)
|
|
})
|
|
t.Run("PrunesHeadersAndValidatorSets", func(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,
|
|
})
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
logger := log.NewTestingLogger(t)
|
|
|
|
c, err := light.NewClient(
|
|
ctx,
|
|
chainID,
|
|
trustOptions,
|
|
mockFullNode,
|
|
[]provider.Provider{mockFullNode},
|
|
dbs.New(dbm.NewMemDB()),
|
|
light.Logger(logger),
|
|
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)
|
|
})
|
|
t.Run("EnsureValidHeadersAndValSets", func(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) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
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)
|
|
})
|
|
}
|
|
})
|
|
}
|