github.com/arcology-network/consensus-engine@v1.9.0/light/detector.go (about)

     1  package light
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"sort"
     9  	"time"
    10  
    11  	"github.com/arcology-network/consensus-engine/light/provider"
    12  	"github.com/arcology-network/consensus-engine/types"
    13  )
    14  
    15  // The detector component of the light client detect and handles attacks on the light client.
    16  // More info here:
    17  // tendermint/docs/architecture/adr-047-handling-evidence-from-light-client.md
    18  
    19  // detectDivergence is a second wall of defense for the light client.
    20  //
    21  // It takes the target verified header and compares it with the headers of a set of
    22  // witness providers that the light client is connected to. If a conflicting header
    23  // is returned it verifies and examines the conflicting header against the verified
    24  // trace that was produced from the primary. If successful it produces two sets of evidence
    25  // and sends them to the opposite provider before halting.
    26  //
    27  // If there are no conflictinge headers, the light client deems the verified target header
    28  // trusted and saves it to the trusted store.
    29  func (c *Client) detectDivergence(ctx context.Context, primaryTrace []*types.LightBlock, now time.Time) error {
    30  	if primaryTrace == nil || len(primaryTrace) < 2 {
    31  		return errors.New("nil or single block primary trace")
    32  	}
    33  	var (
    34  		headerMatched      bool
    35  		lastVerifiedHeader = primaryTrace[len(primaryTrace)-1].SignedHeader
    36  		witnessesToRemove  = make([]int, 0)
    37  	)
    38  	c.logger.Debug("Running detector against trace", "endBlockHeight", lastVerifiedHeader.Height,
    39  		"endBlockHash", lastVerifiedHeader.Hash, "length", len(primaryTrace))
    40  
    41  	c.providerMutex.Lock()
    42  	defer c.providerMutex.Unlock()
    43  
    44  	if len(c.witnesses) == 0 {
    45  		return errNoWitnesses{}
    46  	}
    47  
    48  	// launch one goroutine per witness to retrieve the light block of the target height
    49  	// and compare it with the header from the primary
    50  	errc := make(chan error, len(c.witnesses))
    51  	for i, witness := range c.witnesses {
    52  		go c.compareNewHeaderWithWitness(ctx, errc, lastVerifiedHeader, witness, i)
    53  	}
    54  
    55  	// handle errors from the header comparisons as they come in
    56  	for i := 0; i < cap(errc); i++ {
    57  		err := <-errc
    58  
    59  		switch e := err.(type) {
    60  		case nil: // at least one header matched
    61  			headerMatched = true
    62  		case errConflictingHeaders:
    63  			// We have conflicting headers. This could possibly imply an attack on the light client.
    64  			// First we need to verify the witness's header using the same skipping verification and then we
    65  			// need to find the point that the headers diverge and examine this for any evidence of an attack.
    66  			//
    67  			// We combine these actions together, verifying the witnesses headers and outputting the trace
    68  			// which captures the bifurcation point and if successful provides the information to create
    69  			supportingWitness := c.witnesses[e.WitnessIndex]
    70  			witnessTrace, primaryBlock, err := c.examineConflictingHeaderAgainstTrace(
    71  				ctx,
    72  				primaryTrace,
    73  				e.Block.SignedHeader,
    74  				supportingWitness,
    75  				now,
    76  			)
    77  			if err != nil {
    78  				c.logger.Info("Error validating witness's divergent header", "witness", supportingWitness, "err", err)
    79  				witnessesToRemove = append(witnessesToRemove, e.WitnessIndex)
    80  				continue
    81  			}
    82  
    83  			// We are suspecting that the primary is faulty, hence we hold the witness as the source of truth
    84  			// and generate evidence against the primary that we can send to the witness
    85  			primaryEv := newLightClientAttackEvidence(primaryBlock, witnessTrace[len(witnessTrace)-1], witnessTrace[0])
    86  			c.logger.Error("Attempted attack detected. Sending evidence againt primary by witness", "ev", primaryEv,
    87  				"primary", c.primary, "witness", supportingWitness)
    88  			c.sendEvidence(ctx, primaryEv, supportingWitness)
    89  
    90  			// This may not be valid because the witness itself is at fault. So now we reverse it, examining the
    91  			// trace provided by the witness and holding the primary as the source of truth. Note: primary may not
    92  			// respond but this is okay as we will halt anyway.
    93  			primaryTrace, witnessBlock, err := c.examineConflictingHeaderAgainstTrace(
    94  				ctx,
    95  				witnessTrace,
    96  				primaryBlock.SignedHeader,
    97  				c.primary,
    98  				now,
    99  			)
   100  			if err != nil {
   101  				c.logger.Info("Error validating primary's divergent header", "primary", c.primary, "err", err)
   102  				continue
   103  			}
   104  
   105  			// We now use the primary trace to create evidence against the witness and send it to the primary
   106  			witnessEv := newLightClientAttackEvidence(witnessBlock, primaryTrace[len(primaryTrace)-1], primaryTrace[0])
   107  			c.logger.Error("Sending evidence against witness by primary", "ev", witnessEv,
   108  				"primary", c.primary, "witness", supportingWitness)
   109  			c.sendEvidence(ctx, witnessEv, c.primary)
   110  			// We return the error and don't process anymore witnesses
   111  			return e
   112  
   113  		case errBadWitness:
   114  			c.logger.Info("Witness returned an error during header comparison", "witness", c.witnesses[e.WitnessIndex],
   115  				"err", err)
   116  			// if witness sent us an invalid header, then remove it. If it didn't respond or couldn't find the block, then we
   117  			// ignore it and move on to the next witness
   118  			if _, ok := e.Reason.(provider.ErrBadLightBlock); ok {
   119  				c.logger.Info("Witness sent us invalid header / vals -> removing it", "witness", c.witnesses[e.WitnessIndex])
   120  				witnessesToRemove = append(witnessesToRemove, e.WitnessIndex)
   121  			}
   122  		}
   123  	}
   124  
   125  	// we need to make sure that we remove witnesses by index in the reverse
   126  	// order so as to not affect the indexes themselves
   127  	sort.Ints(witnessesToRemove)
   128  	for i := len(witnessesToRemove) - 1; i >= 0; i-- {
   129  		c.removeWitness(witnessesToRemove[i])
   130  	}
   131  
   132  	// 1. If we had at least one witness that returned the same header then we
   133  	// conclude that we can trust the header
   134  	if headerMatched {
   135  		return nil
   136  	}
   137  
   138  	// 2. ELse all witnesses have either not responded, don't have the block or sent invalid blocks.
   139  	return ErrFailedHeaderCrossReferencing
   140  }
   141  
   142  // compareNewHeaderWithWitness takes the verified header from the primary and compares it with a
   143  // header from a specified witness. The function can return one of three errors:
   144  //
   145  // 1: errConflictingHeaders -> there may have been an attack on this light client
   146  // 2: errBadWitness -> the witness has either not responded, doesn't have the header or has given us an invalid one
   147  //
   148  //	Note: In the case of an invalid header we remove the witness
   149  //
   150  // 3: nil -> the hashes of the two headers match
   151  func (c *Client) compareNewHeaderWithWitness(ctx context.Context, errc chan error, h *types.SignedHeader,
   152  	witness provider.Provider, witnessIndex int) {
   153  
   154  	lightBlock, err := witness.LightBlock(ctx, h.Height)
   155  	if err != nil {
   156  		errc <- errBadWitness{Reason: err, WitnessIndex: witnessIndex}
   157  		return
   158  	}
   159  
   160  	if !bytes.Equal(h.Hash(), lightBlock.Hash()) {
   161  		errc <- errConflictingHeaders{Block: lightBlock, WitnessIndex: witnessIndex}
   162  	}
   163  
   164  	c.logger.Debug("Matching header received by witness", "height", h.Height, "witness", witnessIndex)
   165  	errc <- nil
   166  }
   167  
   168  // sendEvidence sends evidence to a provider on a best effort basis.
   169  func (c *Client) sendEvidence(ctx context.Context, ev *types.LightClientAttackEvidence, receiver provider.Provider) {
   170  	err := receiver.ReportEvidence(ctx, ev)
   171  	if err != nil {
   172  		c.logger.Error("Failed to report evidence to provider", "ev", ev, "provider", receiver)
   173  	}
   174  }
   175  
   176  // examineConflictingHeaderAgainstTrace takes a trace from one provider and a divergent header that
   177  // it has received from another and preforms verifySkipping at the heights of each of the intermediate
   178  // headers in the trace until it reaches the divergentHeader. 1 of 2 things can happen.
   179  //
   180  //  1. The light client verifies a header that is different to the intermediate header in the trace. This
   181  //     is the bifurcation point and the light client can create evidence from it
   182  //  2. The source stops responding, doesn't have the block or sends an invalid header in which case we
   183  //     return the error and remove the witness
   184  func (c *Client) examineConflictingHeaderAgainstTrace(
   185  	ctx context.Context,
   186  	trace []*types.LightBlock,
   187  	divergentHeader *types.SignedHeader,
   188  	source provider.Provider, now time.Time) ([]*types.LightBlock, *types.LightBlock, error) {
   189  
   190  	var previouslyVerifiedBlock *types.LightBlock
   191  
   192  	for idx, traceBlock := range trace {
   193  		// The first block in the trace MUST be the same to the light block that the source produces
   194  		// else we cannot continue with verification.
   195  		sourceBlock, err := source.LightBlock(ctx, traceBlock.Height)
   196  		if err != nil {
   197  			return nil, nil, err
   198  		}
   199  
   200  		if idx == 0 {
   201  			if shash, thash := sourceBlock.Hash(), traceBlock.Hash(); !bytes.Equal(shash, thash) {
   202  				return nil, nil, fmt.Errorf("trusted block is different to the source's first block (%X = %X)",
   203  					thash, shash)
   204  			}
   205  			previouslyVerifiedBlock = sourceBlock
   206  			continue
   207  		}
   208  
   209  		// we check that the source provider can verify a block at the same height of the
   210  		// intermediate height
   211  		trace, err := c.verifySkipping(ctx, source, previouslyVerifiedBlock, sourceBlock, now)
   212  		if err != nil {
   213  			return nil, nil, fmt.Errorf("verifySkipping of conflicting header failed: %w", err)
   214  		}
   215  		// check if the headers verified by the source has diverged from the trace
   216  		if shash, thash := sourceBlock.Hash(), traceBlock.Hash(); !bytes.Equal(shash, thash) {
   217  			// Bifurcation point found!
   218  			return trace, traceBlock, nil
   219  		}
   220  
   221  		// headers are still the same. update the previouslyVerifiedBlock
   222  		previouslyVerifiedBlock = sourceBlock
   223  	}
   224  
   225  	// We have reached the end of the trace without observing a divergence. The last header  is thus different
   226  	// from the divergent header that the source originally sent us, then we return an error.
   227  	return nil, nil, fmt.Errorf("source provided different header to the original header it provided (%X != %X)",
   228  		previouslyVerifiedBlock.Hash(), divergentHeader.Hash())
   229  
   230  }
   231  
   232  // newLightClientAttackEvidence determines the type of attack and then forms the evidence filling out
   233  // all the fields such that it is ready to be sent to a full node.
   234  func newLightClientAttackEvidence(conflicted, trusted, common *types.LightBlock) *types.LightClientAttackEvidence {
   235  	ev := &types.LightClientAttackEvidence{ConflictingBlock: conflicted}
   236  	// if this is an equivocation or amnesia attack, i.e. the validator sets are the same, then we
   237  	// return the height of the conflicting block else if it is a lunatic attack and the validator sets
   238  	// are not the same then we send the height of the common header.
   239  	if ev.ConflictingHeaderIsInvalid(trusted.Header) {
   240  		ev.CommonHeight = common.Height
   241  		ev.Timestamp = common.Time
   242  		ev.TotalVotingPower = common.ValidatorSet.TotalVotingPower()
   243  	} else {
   244  		ev.CommonHeight = trusted.Height
   245  		ev.Timestamp = trusted.Time
   246  		ev.TotalVotingPower = trusted.ValidatorSet.TotalVotingPower()
   247  	}
   248  	ev.ByzantineValidators = ev.GetByzantineValidators(common.ValidatorSet, trusted.SignedHeader)
   249  	return ev
   250  }