github.com/DFWallet/tendermint-cosmos@v0.0.2/light/detector.go (about)

     1  package light
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"time"
     9  
    10  	"github.com/DFWallet/tendermint-cosmos/light/provider"
    11  	"github.com/DFWallet/tendermint-cosmos/types"
    12  )
    13  
    14  // The detector component of the light client detects and handles attacks on the light client.
    15  // More info here:
    16  // tendermint/docs/architecture/adr-047-handling-evidence-from-light-client.md
    17  
    18  // detectDivergence is a second wall of defense for the light client.
    19  //
    20  // It takes the target verified header and compares it with the headers of a set of
    21  // witness providers that the light client is connected to. If a conflicting header
    22  // is returned it verifies and examines the conflicting header against the verified
    23  // trace that was produced from the primary. If successful, it produces two sets of evidence
    24  // and sends them to the opposite provider before halting.
    25  //
    26  // If there are no conflictinge headers, the light client deems the verified target header
    27  // trusted and saves it to the trusted store.
    28  func (c *Client) detectDivergence(ctx context.Context, primaryTrace []*types.LightBlock, now time.Time) error {
    29  	if primaryTrace == nil || len(primaryTrace) < 2 {
    30  		return errors.New("nil or single block primary trace")
    31  	}
    32  	var (
    33  		headerMatched      bool
    34  		lastVerifiedHeader = primaryTrace[len(primaryTrace)-1].SignedHeader
    35  		witnessesToRemove  = make([]int, 0)
    36  	)
    37  	c.logger.Debug("Running detector against trace", "endBlockHeight", lastVerifiedHeader.Height,
    38  		"endBlockHash", lastVerifiedHeader.Hash, "length", len(primaryTrace))
    39  
    40  	c.providerMutex.Lock()
    41  	defer c.providerMutex.Unlock()
    42  
    43  	if len(c.witnesses) == 0 {
    44  		return ErrNoWitnesses
    45  	}
    46  
    47  	// launch one goroutine per witness to retrieve the light block of the target height
    48  	// and compare it with the header from the primary
    49  	errc := make(chan error, len(c.witnesses))
    50  	for i, witness := range c.witnesses {
    51  		go c.compareNewHeaderWithWitness(ctx, errc, lastVerifiedHeader, witness, i)
    52  	}
    53  
    54  	// handle errors from the header comparisons as they come in
    55  	for i := 0; i < cap(errc); i++ {
    56  		err := <-errc
    57  
    58  		switch e := err.(type) {
    59  		case nil: // at least one header matched
    60  			headerMatched = true
    61  		case errConflictingHeaders:
    62  			// We have conflicting headers. This could possibly imply an attack on the light client.
    63  			// First we need to verify the witness's header using the same skipping verification and then we
    64  			// need to find the point that the headers diverge and examine this for any evidence of an attack.
    65  			//
    66  			// We combine these actions together, verifying the witnesses headers and outputting the trace
    67  			// which captures the bifurcation point and if successful provides the information to create valid evidence.
    68  			err := c.handleConflictingHeaders(ctx, primaryTrace, e.Block, e.WitnessIndex, now)
    69  			if err != nil {
    70  				// return information of the attack
    71  				return err
    72  			}
    73  			// if attempt to generate conflicting headers failed then remove witness
    74  			witnessesToRemove = append(witnessesToRemove, e.WitnessIndex)
    75  
    76  		case errBadWitness:
    77  			// these are all melevolent errors and should result in removing the
    78  			// witness
    79  			c.logger.Info("witness returned an error during header comparison, removing...",
    80  				"witness", c.witnesses[e.WitnessIndex], "err", err)
    81  			witnessesToRemove = append(witnessesToRemove, e.WitnessIndex)
    82  		default:
    83  			// Benign errors which can be ignored unless there was a context
    84  			// canceled
    85  			if errors.Is(e, context.Canceled) || errors.Is(e, context.DeadlineExceeded) {
    86  				return e
    87  			}
    88  			c.logger.Info("error in light block request to witness", "err", err)
    89  		}
    90  	}
    91  
    92  	// remove witnesses that have misbehaved
    93  	if err := c.removeWitnesses(witnessesToRemove); err != nil {
    94  		return err
    95  	}
    96  
    97  	// 1. If we had at least one witness that returned the same header then we
    98  	// conclude that we can trust the header
    99  	if headerMatched {
   100  		return nil
   101  	}
   102  
   103  	// 2. Else all witnesses have either not responded, don't have the block or sent invalid blocks.
   104  	return ErrFailedHeaderCrossReferencing
   105  }
   106  
   107  // compareNewHeaderWithWitness takes the verified header from the primary and compares it with a
   108  // header from a specified witness. The function can return one of three errors:
   109  //
   110  // 1: errConflictingHeaders -> there may have been an attack on this light client
   111  // 2: errBadWitness -> the witness has either not responded, doesn't have the header or has given us an invalid one
   112  //    Note: In the case of an invalid header we remove the witness
   113  // 3: nil -> the hashes of the two headers match
   114  func (c *Client) compareNewHeaderWithWitness(ctx context.Context, errc chan error, h *types.SignedHeader,
   115  	witness provider.Provider, witnessIndex int) {
   116  
   117  	lightBlock, err := witness.LightBlock(ctx, h.Height)
   118  	switch err {
   119  	// no error means we move on to checking the hash of the two headers
   120  	case nil:
   121  		break
   122  
   123  	// the witness hasn't been helpful in comparing headers, we mark the response and continue
   124  	// comparing with the rest of the witnesses
   125  	case provider.ErrNoResponse, provider.ErrLightBlockNotFound, context.DeadlineExceeded, context.Canceled:
   126  		errc <- err
   127  		return
   128  
   129  	// the witness' head of the blockchain is lower than the height of the primary. This could be one of
   130  	// two things:
   131  	//    1) The witness is lagging behind
   132  	//    2) The primary may be performing a lunatic attack with a height and time in the future
   133  	case provider.ErrHeightTooHigh:
   134  		// The light client now asks for the latest header that the witness has
   135  		var isTargetHeight bool
   136  		isTargetHeight, lightBlock, err = c.getTargetBlockOrLatest(ctx, h.Height, witness)
   137  		if err != nil {
   138  			errc <- err
   139  			return
   140  		}
   141  
   142  		// if the witness caught up and has returned a block of the target height then we can
   143  		// break from this switch case and continue to verify the hashes
   144  		if isTargetHeight {
   145  			break
   146  		}
   147  
   148  		// witness' last header is below the primary's header. We check the times to see if the blocks
   149  		// have conflicting times
   150  		if !lightBlock.Time.Before(h.Time) {
   151  			errc <- errConflictingHeaders{Block: lightBlock, WitnessIndex: witnessIndex}
   152  			return
   153  		}
   154  
   155  		// the witness is behind. We wait for a period WAITING = 2 * DRIFT + LAG.
   156  		// This should give the witness ample time if it is a participating member
   157  		// of consensus to produce a block that has a time that is after the primary's
   158  		// block time. If not the witness is too far behind and the light client removes it
   159  		time.Sleep(2*c.maxClockDrift + c.maxBlockLag)
   160  		isTargetHeight, lightBlock, err = c.getTargetBlockOrLatest(ctx, h.Height, witness)
   161  		if err != nil {
   162  			if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
   163  				errc <- err
   164  			} else {
   165  				errc <- errBadWitness{Reason: err, WitnessIndex: witnessIndex}
   166  			}
   167  			return
   168  		}
   169  		if isTargetHeight {
   170  			break
   171  		}
   172  
   173  		// the witness still doesn't have a block at the height of the primary.
   174  		// Check if there is a conflicting time
   175  		if !lightBlock.Time.Before(h.Time) {
   176  			errc <- errConflictingHeaders{Block: lightBlock, WitnessIndex: witnessIndex}
   177  			return
   178  		}
   179  
   180  		// Following this request response procedure, the witness has been unable to produce a block
   181  		// that can somehow conflict with the primary's block. We thus conclude that the witness
   182  		// is too far behind and thus we return a no response error.
   183  		//
   184  		// NOTE: If the clock drift / lag has been miscalibrated it is feasible that the light client has
   185  		// drifted too far ahead for any witness to be able provide a comparable block and thus may allow
   186  		// for a malicious primary to attack it
   187  		errc <- provider.ErrNoResponse
   188  		return
   189  
   190  	default:
   191  		// all other errors (i.e. invalid block, closed connection or unreliable provider) we mark the
   192  		// witness as bad and remove it
   193  		errc <- errBadWitness{Reason: err, WitnessIndex: witnessIndex}
   194  		return
   195  	}
   196  
   197  	if !bytes.Equal(h.Hash(), lightBlock.Hash()) {
   198  		errc <- errConflictingHeaders{Block: lightBlock, WitnessIndex: witnessIndex}
   199  	}
   200  
   201  	c.logger.Debug("Matching header received by witness", "height", h.Height, "witness", witnessIndex)
   202  	errc <- nil
   203  }
   204  
   205  // sendEvidence sends evidence to a provider on a best effort basis.
   206  func (c *Client) sendEvidence(ctx context.Context, ev *types.LightClientAttackEvidence, receiver provider.Provider) {
   207  	err := receiver.ReportEvidence(ctx, ev)
   208  	if err != nil {
   209  		c.logger.Error("Failed to report evidence to provider", "ev", ev, "provider", receiver)
   210  	}
   211  }
   212  
   213  // handleConflictingHeaders handles the primary style of attack, which is where a primary and witness have
   214  // two headers of the same height but with different hashes
   215  func (c *Client) handleConflictingHeaders(
   216  	ctx context.Context,
   217  	primaryTrace []*types.LightBlock,
   218  	challendingBlock *types.LightBlock,
   219  	witnessIndex int,
   220  	now time.Time,
   221  ) error {
   222  	supportingWitness := c.witnesses[witnessIndex]
   223  	witnessTrace, primaryBlock, err := c.examineConflictingHeaderAgainstTrace(
   224  		ctx,
   225  		primaryTrace,
   226  		challendingBlock,
   227  		supportingWitness,
   228  		now,
   229  	)
   230  	if err != nil {
   231  		c.logger.Info("error validating witness's divergent header", "witness", supportingWitness, "err", err)
   232  		return nil
   233  	}
   234  
   235  	// We are suspecting that the primary is faulty, hence we hold the witness as the source of truth
   236  	// and generate evidence against the primary that we can send to the witness
   237  	commonBlock, trustedBlock := witnessTrace[0], witnessTrace[len(witnessTrace)-1]
   238  	evidenceAgainstPrimary := newLightClientAttackEvidence(primaryBlock, trustedBlock, commonBlock)
   239  	c.logger.Error("ATTEMPTED ATTACK DETECTED. Sending evidence againt primary by witness", "ev", evidenceAgainstPrimary,
   240  		"primary", c.primary, "witness", supportingWitness)
   241  	c.sendEvidence(ctx, evidenceAgainstPrimary, supportingWitness)
   242  
   243  	if primaryBlock.Commit.Round != witnessTrace[len(witnessTrace)-1].Commit.Round {
   244  		c.logger.Info("The light client has detected, and prevented, an attempted amnesia attack." +
   245  			" We think this attack is pretty unlikely, so if you see it, that's interesting to us." +
   246  			" Can you let us know by opening an issue through https://github.com/DFWallet/tendermint-cosmos/issues/new?")
   247  	}
   248  
   249  	// This may not be valid because the witness itself is at fault. So now we reverse it, examining the
   250  	// trace provided by the witness and holding the primary as the source of truth. Note: primary may not
   251  	// respond but this is okay as we will halt anyway.
   252  	primaryTrace, witnessBlock, err := c.examineConflictingHeaderAgainstTrace(
   253  		ctx,
   254  		witnessTrace,
   255  		primaryBlock,
   256  		c.primary,
   257  		now,
   258  	)
   259  	if err != nil {
   260  		c.logger.Info("Error validating primary's divergent header", "primary", c.primary, "err", err)
   261  		return ErrLightClientAttack
   262  	}
   263  
   264  	// We now use the primary trace to create evidence against the witness and send it to the primary
   265  	commonBlock, trustedBlock = primaryTrace[0], primaryTrace[len(primaryTrace)-1]
   266  	evidenceAgainstWitness := newLightClientAttackEvidence(witnessBlock, trustedBlock, commonBlock)
   267  	c.logger.Error("Sending evidence against witness by primary", "ev", evidenceAgainstWitness,
   268  		"primary", c.primary, "witness", supportingWitness)
   269  	c.sendEvidence(ctx, evidenceAgainstWitness, c.primary)
   270  	// We return the error and don't process anymore witnesses
   271  	return ErrLightClientAttack
   272  }
   273  
   274  // examineConflictingHeaderAgainstTrace takes a trace from one provider and a divergent header that
   275  // it has received from another and preforms verifySkipping at the heights of each of the intermediate
   276  // headers in the trace until it reaches the divergentHeader. 1 of 2 things can happen.
   277  //
   278  // 1. The light client verifies a header that is different to the intermediate header in the trace. This
   279  //    is the bifurcation point and the light client can create evidence from it
   280  // 2. The source stops responding, doesn't have the block or sends an invalid header in which case we
   281  //    return the error and remove the witness
   282  //
   283  // CONTRACT:
   284  // 1. Trace can not be empty len(trace) > 0
   285  // 2. The last block in the trace can not be of a lower height than the target block
   286  //    trace[len(trace)-1].Height >= targetBlock.Height
   287  // 3. The
   288  func (c *Client) examineConflictingHeaderAgainstTrace(
   289  	ctx context.Context,
   290  	trace []*types.LightBlock,
   291  	targetBlock *types.LightBlock,
   292  	source provider.Provider, now time.Time,
   293  ) ([]*types.LightBlock, *types.LightBlock, error) {
   294  
   295  	var (
   296  		previouslyVerifiedBlock, sourceBlock *types.LightBlock
   297  		sourceTrace                          []*types.LightBlock
   298  		err                                  error
   299  	)
   300  
   301  	if targetBlock.Height < trace[0].Height {
   302  		return nil, nil, fmt.Errorf("target block has a height lower than the trusted height (%d < %d)",
   303  			targetBlock.Height, trace[0].Height)
   304  	}
   305  
   306  	for idx, traceBlock := range trace {
   307  		// this case only happens in a forward lunatic attack. We treat the block with the
   308  		// height directly after the targetBlock as the divergent block
   309  		if traceBlock.Height > targetBlock.Height {
   310  			// sanity check that the time of the traceBlock is indeed less than that of the targetBlock. If the trace
   311  			// was correctly verified we should expect monotonically increasing time. This means that if the block at
   312  			// the end of the trace has a lesser time than the target block then all blocks in the trace should have a
   313  			// lesser time
   314  			if traceBlock.Time.After(targetBlock.Time) {
   315  				return nil, nil,
   316  					errors.New("sanity check failed: expected traceblock to have a lesser time than the target block")
   317  			}
   318  
   319  			// before sending back the divergent block and trace we need to ensure we have verified
   320  			// the final gap between the previouslyVerifiedBlock and the targetBlock
   321  			if previouslyVerifiedBlock.Height != targetBlock.Height {
   322  				sourceTrace, err = c.verifySkipping(ctx, source, previouslyVerifiedBlock, targetBlock, now)
   323  				if err != nil {
   324  					return nil, nil, fmt.Errorf("verifySkipping of conflicting header failed: %w", err)
   325  				}
   326  			}
   327  			return sourceTrace, traceBlock, nil
   328  		}
   329  
   330  		// get the corresponding block from the source to verify and match up against the traceBlock
   331  		if traceBlock.Height == targetBlock.Height {
   332  			sourceBlock = targetBlock
   333  		} else {
   334  			sourceBlock, err = source.LightBlock(ctx, traceBlock.Height)
   335  			if err != nil {
   336  				return nil, nil, fmt.Errorf("failed to examine trace: %w", err)
   337  			}
   338  		}
   339  
   340  		// The first block in the trace MUST be the same to the light block that the source produces
   341  		// else we cannot continue with verification.
   342  		if idx == 0 {
   343  			if shash, thash := sourceBlock.Hash(), traceBlock.Hash(); !bytes.Equal(shash, thash) {
   344  				return nil, nil, fmt.Errorf("trusted block is different to the source's first block (%X = %X)",
   345  					thash, shash)
   346  			}
   347  			previouslyVerifiedBlock = sourceBlock
   348  			continue
   349  		}
   350  
   351  		// we check that the source provider can verify a block at the same height of the
   352  		// intermediate height
   353  		sourceTrace, err = c.verifySkipping(ctx, source, previouslyVerifiedBlock, sourceBlock, now)
   354  		if err != nil {
   355  			return nil, nil, fmt.Errorf("verifySkipping of conflicting header failed: %w", err)
   356  		}
   357  		// check if the headers verified by the source has diverged from the trace
   358  		if shash, thash := sourceBlock.Hash(), traceBlock.Hash(); !bytes.Equal(shash, thash) {
   359  			// Bifurcation point found!
   360  			return sourceTrace, traceBlock, nil
   361  		}
   362  
   363  		// headers are still the same. update the previouslyVerifiedBlock
   364  		previouslyVerifiedBlock = sourceBlock
   365  	}
   366  
   367  	// We have reached the end of the trace. This should never happen. This can only happen if one of the stated
   368  	// prerequisites to this function were not met. Namely that either trace[len(trace)-1].Height < targetBlock.Height
   369  	// or that trace[i].Hash() != targetBlock.Hash()
   370  	return nil, nil, errNoDivergence
   371  
   372  }
   373  
   374  // getTargetBlockOrLatest gets the latest height, if it is greater than the target height then it queries
   375  // the target heght else it returns the latest. returns true if it successfully managed to acquire the target
   376  // height.
   377  func (c *Client) getTargetBlockOrLatest(
   378  	ctx context.Context,
   379  	height int64,
   380  	witness provider.Provider,
   381  ) (bool, *types.LightBlock, error) {
   382  	lightBlock, err := witness.LightBlock(ctx, 0)
   383  	if err != nil {
   384  		return false, nil, err
   385  	}
   386  
   387  	if lightBlock.Height == height {
   388  		// the witness has caught up to the height of the provider's signed header. We
   389  		// can resume with checking the hashes.
   390  		return true, lightBlock, nil
   391  	}
   392  
   393  	if lightBlock.Height > height {
   394  		// the witness has caught up. We recursively call the function again. However in order
   395  		// to avoud a wild goose chase where the witness sends us one header below and one header
   396  		// above the height we set a timeout to the context
   397  		lightBlock, err := witness.LightBlock(ctx, height)
   398  		return true, lightBlock, err
   399  	}
   400  
   401  	return false, lightBlock, nil
   402  }
   403  
   404  // newLightClientAttackEvidence determines the type of attack and then forms the evidence filling out
   405  // all the fields such that it is ready to be sent to a full node.
   406  func newLightClientAttackEvidence(conflicted, trusted, common *types.LightBlock) *types.LightClientAttackEvidence {
   407  	ev := &types.LightClientAttackEvidence{ConflictingBlock: conflicted}
   408  	// if this is an equivocation or amnesia attack, i.e. the validator sets are the same, then we
   409  	// return the height of the conflicting block else if it is a lunatic attack and the validator sets
   410  	// are not the same then we send the height of the common header.
   411  	if ev.ConflictingHeaderIsInvalid(trusted.Header) {
   412  		ev.CommonHeight = common.Height
   413  		ev.Timestamp = common.Time
   414  		ev.TotalVotingPower = common.ValidatorSet.TotalVotingPower()
   415  	} else {
   416  		ev.CommonHeight = trusted.Height
   417  		ev.Timestamp = trusted.Time
   418  		ev.TotalVotingPower = trusted.ValidatorSet.TotalVotingPower()
   419  	}
   420  	ev.ByzantineValidators = ev.GetByzantineValidators(common.ValidatorSet, trusted.SignedHeader)
   421  	return ev
   422  }