package light_test import ( "fmt" "testing" "time" "github.com/stretchr/testify/assert" tmmath "github.com/tendermint/tendermint/libs/math" "github.com/tendermint/tendermint/light" "github.com/tendermint/tendermint/types" ) const ( maxClockDrift = 10 * time.Second ) func TestVerifyAdjacentHeaders(t *testing.T) { const ( chainID = "TestVerifyAdjacentHeaders" lastHeight = 1 nextHeight = 2 ) var ( keys = genPrivKeys(4) // 20, 30, 40, 50 - the first 3 don't have 2/3, the last 3 do! vals = keys.ToValidators(20, 10) bTime, _ = time.Parse(time.RFC3339, "2006-01-02T15:04:05Z") header = keys.GenSignedHeader(t, chainID, lastHeight, bTime, nil, vals, vals, hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys)) ) testCases := []struct { newHeader *types.SignedHeader newVals *types.ValidatorSet trustingPeriod time.Duration now time.Time expErr error expErrText string }{ // same header -> no error 0: { header, vals, 3 * time.Hour, bTime.Add(2 * time.Hour), nil, "headers must be adjacent in height", }, // different chainID -> error 1: { keys.GenSignedHeader(t, "different-chainID", nextHeight, bTime.Add(1*time.Hour), nil, vals, vals, hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys)), vals, 3 * time.Hour, bTime.Add(2 * time.Hour), nil, "header belongs to another chain", }, // new header's time is before old header's time -> error 2: { keys.GenSignedHeader(t, chainID, nextHeight, bTime.Add(-1*time.Hour), nil, vals, vals, hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys)), vals, 4 * time.Hour, bTime.Add(2 * time.Hour), nil, "to be after old header time", }, // new header's time is from the future -> error 3: { keys.GenSignedHeader(t, chainID, nextHeight, bTime.Add(3*time.Hour), nil, vals, vals, hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys)), vals, 3 * time.Hour, bTime.Add(2 * time.Hour), nil, "new header has a time from the future", }, // new header's time is from the future, but it's acceptable (< maxClockDrift) -> no error 4: { keys.GenSignedHeader(t, chainID, nextHeight, bTime.Add(2*time.Hour).Add(maxClockDrift).Add(-1*time.Millisecond), nil, vals, vals, hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys)), vals, 3 * time.Hour, bTime.Add(2 * time.Hour), nil, "", }, // 3/3 signed -> no error 5: { keys.GenSignedHeader(t, chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals, hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys)), vals, 3 * time.Hour, bTime.Add(2 * time.Hour), nil, "", }, // 2/3 signed -> no error 6: { keys.GenSignedHeader(t, chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals, hash("app_hash"), hash("cons_hash"), hash("results_hash"), 1, len(keys)), vals, 3 * time.Hour, bTime.Add(2 * time.Hour), nil, "", }, // 1/3 signed -> error 7: { keys.GenSignedHeader(t, chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals, hash("app_hash"), hash("cons_hash"), hash("results_hash"), len(keys)-1, len(keys)), vals, 3 * time.Hour, bTime.Add(2 * time.Hour), light.ErrInvalidHeader{Reason: types.ErrNotEnoughVotingPowerSigned{Got: 50, Needed: 93}}, "", }, // vals does not match with what we have -> error 8: { keys.GenSignedHeader(t, chainID, nextHeight, bTime.Add(1*time.Hour), nil, keys.ToValidators(10, 1), vals, hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys)), keys.ToValidators(10, 1), 3 * time.Hour, bTime.Add(2 * time.Hour), nil, "to match those from new header", }, // vals are inconsistent with newHeader -> error 9: { keys.GenSignedHeader(t, chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals, hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys)), keys.ToValidators(10, 1), 3 * time.Hour, bTime.Add(2 * time.Hour), nil, "to match those that were supplied", }, // old header has expired -> error 10: { keys.GenSignedHeader(t, chainID, nextHeight, bTime.Add(1*time.Hour), nil, vals, vals, hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys)), keys.ToValidators(10, 1), 1 * time.Hour, bTime.Add(2 * time.Hour), nil, "old header has expired", }, } for i, tc := range testCases { tc := tc t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) { err := light.VerifyAdjacent(header, tc.newHeader, tc.newVals, tc.trustingPeriod, tc.now, maxClockDrift) switch { case tc.expErr != nil && assert.Error(t, err): assert.Equal(t, tc.expErr, err) case tc.expErrText != "": assert.Contains(t, err.Error(), tc.expErrText) default: assert.NoError(t, err) } }) } } func TestVerifyNonAdjacentHeaders(t *testing.T) { const ( chainID = "TestVerifyNonAdjacentHeaders" lastHeight = 1 ) var ( keys = genPrivKeys(4) // 20, 30, 40, 50 - the first 3 don't have 2/3, the last 3 do! vals = keys.ToValidators(20, 10) bTime, _ = time.Parse(time.RFC3339, "2006-01-02T15:04:05Z") header = keys.GenSignedHeader(t, chainID, lastHeight, bTime, nil, vals, vals, hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys)) // 30, 40, 50 twoThirds = keys[1:] twoThirdsVals = twoThirds.ToValidators(30, 10) // 50 oneThird = keys[len(keys)-1:] oneThirdVals = oneThird.ToValidators(50, 10) // 20 lessThanOneThird = keys[0:1] lessThanOneThirdVals = lessThanOneThird.ToValidators(20, 10) ) testCases := []struct { newHeader *types.SignedHeader newVals *types.ValidatorSet trustingPeriod time.Duration now time.Time expErr error expErrText string }{ // 3/3 new vals signed, 3/3 old vals present -> no error 0: { keys.GenSignedHeader(t, chainID, 3, bTime.Add(1*time.Hour), nil, vals, vals, hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys)), vals, 3 * time.Hour, bTime.Add(2 * time.Hour), nil, "", }, // 2/3 new vals signed, 3/3 old vals present -> no error 1: { keys.GenSignedHeader(t, chainID, 4, bTime.Add(1*time.Hour), nil, vals, vals, hash("app_hash"), hash("cons_hash"), hash("results_hash"), 1, len(keys)), vals, 3 * time.Hour, bTime.Add(2 * time.Hour), nil, "", }, // 1/3 new vals signed, 3/3 old vals present -> error 2: { keys.GenSignedHeader(t, chainID, 5, bTime.Add(1*time.Hour), nil, vals, vals, hash("app_hash"), hash("cons_hash"), hash("results_hash"), len(keys)-1, len(keys)), vals, 3 * time.Hour, bTime.Add(2 * time.Hour), light.ErrInvalidHeader{types.ErrNotEnoughVotingPowerSigned{Got: 50, Needed: 93}}, "", }, // 3/3 new vals signed, 2/3 old vals present -> no error 3: { twoThirds.GenSignedHeader(t, chainID, 5, bTime.Add(1*time.Hour), nil, twoThirdsVals, twoThirdsVals, hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(twoThirds)), twoThirdsVals, 3 * time.Hour, bTime.Add(2 * time.Hour), nil, "", }, // 3/3 new vals signed, 1/3 old vals present -> no error 4: { oneThird.GenSignedHeader(t, chainID, 5, bTime.Add(1*time.Hour), nil, oneThirdVals, oneThirdVals, hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(oneThird)), oneThirdVals, 3 * time.Hour, bTime.Add(2 * time.Hour), nil, "", }, // 3/3 new vals signed, less than 1/3 old vals present -> error 5: { lessThanOneThird.GenSignedHeader(t, chainID, 5, bTime.Add(1*time.Hour), nil, lessThanOneThirdVals, lessThanOneThirdVals, hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(lessThanOneThird)), lessThanOneThirdVals, 3 * time.Hour, bTime.Add(2 * time.Hour), light.ErrNewValSetCantBeTrusted{types.ErrNotEnoughVotingPowerSigned{Got: 20, Needed: 46}}, "", }, } for i, tc := range testCases { tc := tc t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) { err := light.VerifyNonAdjacent(header, vals, tc.newHeader, tc.newVals, tc.trustingPeriod, tc.now, maxClockDrift, light.DefaultTrustLevel) switch { case tc.expErr != nil && assert.Error(t, err): assert.Equal(t, tc.expErr, err) case tc.expErrText != "": assert.Contains(t, err.Error(), tc.expErrText) default: assert.NoError(t, err) } }) } } func TestVerifyReturnsErrorIfTrustLevelIsInvalid(t *testing.T) { const ( chainID = "TestVerifyReturnsErrorIfTrustLevelIsInvalid" lastHeight = 1 ) var ( keys = genPrivKeys(4) // 20, 30, 40, 50 - the first 3 don't have 2/3, the last 3 do! vals = keys.ToValidators(20, 10) bTime, _ = time.Parse(time.RFC3339, "2006-01-02T15:04:05Z") header = keys.GenSignedHeader(t, chainID, lastHeight, bTime, nil, vals, vals, hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(keys)) ) err := light.Verify(header, vals, header, vals, 2*time.Hour, time.Now(), maxClockDrift, tmmath.Fraction{Numerator: 2, Denominator: 1}) assert.Error(t, err) } func TestValidateTrustLevel(t *testing.T) { testCases := []struct { lvl tmmath.Fraction valid bool }{ // valid 0: {tmmath.Fraction{Numerator: 1, Denominator: 3}, true}, 1: {tmmath.Fraction{Numerator: 2, Denominator: 3}, true}, 2: {tmmath.Fraction{Numerator: 4, Denominator: 5}, true}, 3: {tmmath.Fraction{Numerator: 99, Denominator: 100}, true}, // invalid 4: {tmmath.Fraction{Numerator: 3, Denominator: 3}, false}, 5: {tmmath.Fraction{Numerator: 6, Denominator: 5}, false}, 6: {tmmath.Fraction{Numerator: 3, Denominator: 10}, false}, 7: {tmmath.Fraction{Numerator: 0, Denominator: 1}, false}, 8: {tmmath.Fraction{Numerator: 0, Denominator: 0}, false}, 9: {tmmath.Fraction{Numerator: 1, Denominator: 0}, false}, } for idx, tc := range testCases { err := light.ValidateTrustLevel(tc.lvl) if !tc.valid { assert.Error(t, err, idx) } else { assert.NoError(t, err, idx) } } }