Browse Source

light: run detector for sequentially validating light client (#5538)

Closes #5445
pull/5606/head
Anton Kaliaev 4 years ago
committed by GitHub
parent
commit
627f7b5989
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 94 additions and 76 deletions
  1. +19
    -8
      light/client.go
  2. +1
    -2
      light/detector.go
  3. +73
    -63
      light/detector_test.go
  4. +1
    -3
      light/errors.go

+ 19
- 8
light/client.go View File

@ -123,7 +123,7 @@ type Client struct {
providerMutex tmsync.Mutex providerMutex tmsync.Mutex
// Primary provider of new headers. // Primary provider of new headers.
primary provider.Provider primary provider.Provider
// See Witnesses option
// Providers used to "witness" new headers.
witnesses []provider.Provider witnesses []provider.Provider
// Where trusted light blocks are stored. // Where trusted light blocks are stored.
@ -218,7 +218,7 @@ func NewClientFromTrustedStore(
} }
// Validate the number of witnesses. // Validate the number of witnesses.
if len(c.witnesses) < 1 && c.verificationMode == skipping {
if len(c.witnesses) < 1 {
return nil, errNoWitnesses{} return nil, errNoWitnesses{}
} }
@ -363,10 +363,8 @@ func (c *Client) initializeWithTrustOptions(ctx context.Context, options TrustOp
} }
// 3) Cross-verify with witnesses to ensure everybody has the same state. // 3) Cross-verify with witnesses to ensure everybody has the same state.
if len(c.witnesses) > 0 {
if err := c.compareFirstHeaderWithWitnesses(ctx, l.SignedHeader); err != nil {
return err
}
if err := c.compareFirstHeaderWithWitnesses(ctx, l.SignedHeader); err != nil {
return err
} }
// 4) Persist both of them and continue. // 4) Persist both of them and continue.
@ -443,7 +441,7 @@ func (c *Client) Update(ctx context.Context, now time.Time) (*types.LightBlock,
} }
// VerifyLightBlockAtHeight fetches the light block at the given height // VerifyLightBlockAtHeight fetches the light block at the given height
// and calls verifyLightBlock. It returns the block immediately if it exists in
// and verifies it. It returns the block immediately if it exists in
// the trustedStore (no verification is needed). // the trustedStore (no verification is needed).
// //
// height must be > 0. // height must be > 0.
@ -600,6 +598,7 @@ func (c *Client) verifySequential(
verifiedBlock = trustedBlock verifiedBlock = trustedBlock
interimBlock *types.LightBlock interimBlock *types.LightBlock
err error err error
trace = []*types.LightBlock{trustedBlock}
) )
for height := trustedBlock.Height + 1; height <= newLightBlock.Height; height++ { for height := trustedBlock.Height + 1; height <= newLightBlock.Height; height++ {
@ -669,9 +668,17 @@ func (c *Client) verifySequential(
// 3) Update verifiedBlock // 3) Update verifiedBlock
verifiedBlock = interimBlock verifiedBlock = interimBlock
// 4) Add verifiedBlock to trace
trace = append(trace, verifiedBlock)
} }
return nil
// Compare header with the witnesses to ensure it's not a fork.
// More witnesses we have, more chance to notice one.
//
// CORRECTNESS ASSUMPTION: there's at least 1 correct full node
// (primary or one of the witnesses).
return c.detectDivergence(ctx, trace, now)
} }
// see VerifyHeader // see VerifyHeader
@ -995,6 +1002,10 @@ func (c *Client) compareFirstHeaderWithWitnesses(ctx context.Context, h *types.S
compareCtx, cancel := context.WithCancel(ctx) compareCtx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
if len(c.witnesses) < 1 {
return errNoWitnesses{}
}
errc := make(chan error, len(c.witnesses)) errc := make(chan error, len(c.witnesses))
for i, witness := range c.witnesses { for i, witness := range c.witnesses {
go c.compareNewHeaderWithWitness(compareCtx, errc, h, witness, i) go c.compareNewHeaderWithWitness(compareCtx, errc, h, witness, i)


+ 1
- 2
light/detector.go View File

@ -15,8 +15,7 @@ import (
// More info here: // More info here:
// tendermint/docs/architecture/adr-047-handling-evidence-from-light-client.md // tendermint/docs/architecture/adr-047-handling-evidence-from-light-client.md
// detectDivergence is a second wall of defense for the light client and is used
// only in the case of skipping verification which employs the trust level mechanism.
// detectDivergence is a second wall of defense for the light client.
// //
// It takes the target verified header and compares it with the headers of a set of // It takes the target verified header and compares it with the headers of a set of
// witness providers that the light client is connected to. If a conflicting header // witness providers that the light client is connected to. If a conflicting header


+ 73
- 63
light/detector_test.go View File

@ -63,7 +63,7 @@ func TestLightClientAttackEvidence_Lunatic(t *testing.T) {
// Check verification returns an error. // Check verification returns an error.
_, err = c.VerifyLightBlockAtHeight(ctx, 10, bTime.Add(1*time.Hour)) _, err = c.VerifyLightBlockAtHeight(ctx, 10, bTime.Add(1*time.Hour))
if assert.Error(t, err) { if assert.Error(t, err) {
assert.Equal(t, err, light.ErrLightClientAttack)
assert.Equal(t, light.ErrLightClientAttack, err)
} }
// Check evidence was sent to both full nodes. // Check evidence was sent to both full nodes.
@ -90,76 +90,86 @@ func TestLightClientAttackEvidence_Lunatic(t *testing.T) {
} }
func TestLightClientAttackEvidence_Equivocation(t *testing.T) { func TestLightClientAttackEvidence_Equivocation(t *testing.T) {
// primary performs an equivocation attack
var (
latestHeight = int64(10)
valSize = 5
divergenceHeight = int64(6)
primaryHeaders = make(map[int64]*types.SignedHeader, latestHeight)
primaryValidators = make(map[int64]*types.ValidatorSet, latestHeight)
)
// validators don't change in this network (however we still use a map just for convenience)
witnessHeaders, witnessValidators, chainKeys := genMockNodeWithKeys(chainID, latestHeight+2, valSize, 2, bTime)
witness := mockp.New(chainID, witnessHeaders, witnessValidators)
verificationOptions := map[string]light.Option{
"sequential": light.SequentialVerification(),
"skipping": light.SkippingVerification(light.DefaultTrustLevel),
}
for height := int64(1); height <= latestHeight; height++ {
if height < divergenceHeight {
primaryHeaders[height] = witnessHeaders[height]
for s, verificationOption := range verificationOptions {
t.Log("==> verification", s)
// primary performs an equivocation attack
var (
latestHeight = int64(10)
valSize = 5
divergenceHeight = int64(6)
primaryHeaders = make(map[int64]*types.SignedHeader, latestHeight)
primaryValidators = make(map[int64]*types.ValidatorSet, latestHeight)
)
// validators don't change in this network (however we still use a map just for convenience)
witnessHeaders, witnessValidators, chainKeys := genMockNodeWithKeys(chainID, latestHeight+2, valSize, 2, bTime)
witness := mockp.New(chainID, witnessHeaders, witnessValidators)
for height := int64(1); height <= latestHeight; height++ {
if height < divergenceHeight {
primaryHeaders[height] = witnessHeaders[height]
primaryValidators[height] = witnessValidators[height]
continue
}
// we don't have a network partition so we will make 4/5 (greater than 2/3) malicious and vote again for
// a different block (which we do by adding txs)
primaryHeaders[height] = chainKeys[height].GenSignedHeader(chainID, height,
bTime.Add(time.Duration(height)*time.Minute), []types.Tx{[]byte("abcd")},
witnessValidators[height], witnessValidators[height+1], hash("app_hash"),
hash("cons_hash"), hash("results_hash"), 0, len(chainKeys[height])-1)
primaryValidators[height] = witnessValidators[height] primaryValidators[height] = witnessValidators[height]
continue
} }
// we don't have a network partition so we will make 4/5 (greater than 2/3) malicious and vote again for
// a different block (which we do by adding txs)
primaryHeaders[height] = chainKeys[height].GenSignedHeader(chainID, height,
bTime.Add(time.Duration(height)*time.Minute), []types.Tx{[]byte("abcd")},
witnessValidators[height], witnessValidators[height+1], hash("app_hash"),
hash("cons_hash"), hash("results_hash"), 0, len(chainKeys[height])-1)
primaryValidators[height] = witnessValidators[height]
}
primary := mockp.New(chainID, primaryHeaders, primaryValidators)
primary := mockp.New(chainID, primaryHeaders, primaryValidators)
c, err := light.NewClient(
ctx,
chainID,
light.TrustOptions{
Period: 4 * time.Hour,
Height: 1,
Hash: primaryHeaders[1].Hash(),
},
primary,
[]provider.Provider{witness},
dbs.New(dbm.NewMemDB(), chainID),
light.Logger(log.TestingLogger()),
light.MaxRetryAttempts(1),
)
require.NoError(t, err)
c, err := light.NewClient(
ctx,
chainID,
light.TrustOptions{
Period: 4 * time.Hour,
Height: 1,
Hash: primaryHeaders[1].Hash(),
},
primary,
[]provider.Provider{witness},
dbs.New(dbm.NewMemDB(), chainID),
light.Logger(log.TestingLogger()),
light.MaxRetryAttempts(1),
verificationOption,
)
require.NoError(t, err)
// Check verification returns an error.
_, err = c.VerifyLightBlockAtHeight(ctx, 10, bTime.Add(1*time.Hour))
if assert.Error(t, err) {
assert.Equal(t, err, light.ErrLightClientAttack)
}
// Check verification returns an error.
_, err = c.VerifyLightBlockAtHeight(ctx, 10, bTime.Add(1*time.Hour))
if assert.Error(t, err) {
assert.Equal(t, light.ErrLightClientAttack, err)
}
// Check evidence was sent to both full nodes.
// Common height should be set to the height of the divergent header in the instance
// of an equivocation attack and the validator sets are the same as what the witness has
evAgainstPrimary := &types.LightClientAttackEvidence{
ConflictingBlock: &types.LightBlock{
SignedHeader: primaryHeaders[divergenceHeight],
ValidatorSet: primaryValidators[divergenceHeight],
},
CommonHeight: divergenceHeight,
}
assert.True(t, witness.HasEvidence(evAgainstPrimary))
// Check evidence was sent to both full nodes.
// Common height should be set to the height of the divergent header in the instance
// of an equivocation attack and the validator sets are the same as what the witness has
evAgainstPrimary := &types.LightClientAttackEvidence{
ConflictingBlock: &types.LightBlock{
SignedHeader: primaryHeaders[divergenceHeight],
ValidatorSet: primaryValidators[divergenceHeight],
},
CommonHeight: divergenceHeight,
}
assert.True(t, witness.HasEvidence(evAgainstPrimary))
evAgainstWitness := &types.LightClientAttackEvidence{
ConflictingBlock: &types.LightBlock{
SignedHeader: witnessHeaders[divergenceHeight],
ValidatorSet: witnessValidators[divergenceHeight],
},
CommonHeight: divergenceHeight,
evAgainstWitness := &types.LightClientAttackEvidence{
ConflictingBlock: &types.LightBlock{
SignedHeader: witnessHeaders[divergenceHeight],
ValidatorSet: witnessValidators[divergenceHeight],
},
CommonHeight: divergenceHeight,
}
assert.True(t, primary.HasEvidence(evAgainstWitness))
} }
assert.True(t, primary.HasEvidence(evAgainstWitness))
} }
// 1. Different nodes therefore a divergent header is produced. // 1. Different nodes therefore a divergent header is produced.


+ 1
- 3
light/errors.go View File

@ -60,9 +60,7 @@ func (e ErrVerificationFailed) Unwrap() error {
} }
func (e ErrVerificationFailed) Error() string { func (e ErrVerificationFailed) Error() string {
return fmt.Sprintf(
"verify from #%d to #%d failed: %v",
e.From, e.To, e.Reason)
return fmt.Sprintf("verify from #%d to #%d failed: %v", e.From, e.To, e.Reason)
} }
// ErrLightClientAttack is returned when the light client has detected an attempt // ErrLightClientAttack is returned when the light client has detected an attempt


Loading…
Cancel
Save