- package light
-
- import (
- "bytes"
- "context"
- "errors"
- "fmt"
- "time"
-
- "github.com/tendermint/tendermint/light/provider"
- "github.com/tendermint/tendermint/types"
- )
-
- // The detector component of the light client detect and handles attacks on the light client.
- // More info here:
- // 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.
- //
- // 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
- // is returned it verifies and examines the conflicting header against the verified
- // trace that was produced from the primary. If successful it produces two sets of evidence
- // and sends them to the opposite provider before halting.
- //
- // If there are no conflictinge headers, the light client deems the verified target header
- // trusted and saves it to the trusted store.
- func (c *Client) detectDivergence(ctx context.Context, primaryTrace []*types.LightBlock, now time.Time) error {
- if primaryTrace == nil || len(primaryTrace) < 2 {
- return errors.New("nil or single block primary trace")
- }
- var (
- headerMatched bool
- lastVerifiedHeader = primaryTrace[len(primaryTrace)-1].SignedHeader
- witnessesToRemove = make([]int, 0)
- )
- c.logger.Debug("Running detector against trace", "endBlockHeight", lastVerifiedHeader.Height,
- "endBlockHash", lastVerifiedHeader.Hash, "length", len(primaryTrace))
-
- c.providerMutex.Lock()
- defer c.providerMutex.Unlock()
-
- if len(c.witnesses) == 0 {
- return errNoWitnesses{}
- }
-
- // launch one goroutine per witness to retrieve the light block of the target height
- // and compare it with the header from the primary
- errc := make(chan error, len(c.witnesses))
- for i, witness := range c.witnesses {
- go c.compareNewHeaderWithWitness(ctx, errc, lastVerifiedHeader, witness, i)
- }
-
- // handle errors from the header comparisons as they come in
- for i := 0; i < cap(errc); i++ {
- err := <-errc
-
- switch e := err.(type) {
- case nil: // at least one header matched
- headerMatched = true
- case errConflictingHeaders:
- // We have conflicting headers. This could possibly imply an attack on the light client.
- // First we need to verify the witness's header using the same skipping verification and then we
- // need to find the point that the headers diverge and examine this for any evidence of an attack.
- //
- // We combine these actions together, verifying the witnesses headers and outputting the trace
- // which captures the bifurcation point and if successful provides the information to create
- supportingWitness := c.witnesses[e.WitnessIndex]
- witnessTrace, primaryBlock, err := c.examineConflictingHeaderAgainstTrace(
- ctx,
- primaryTrace,
- e.Block.SignedHeader,
- supportingWitness,
- now,
- )
- if err != nil {
- c.logger.Info("Error validating witness's divergent header", "witness", supportingWitness, "err", err)
- witnessesToRemove = append(witnessesToRemove, e.WitnessIndex)
- continue
- }
- // if this is an equivocation or amnesia attack, i.e. the validator sets are the same, then we
- // return the height of the conflicting block else if it is a lunatic attack and the validator sets
- // are not the same then we send the height of the common header.
- commonHeight := primaryBlock.Height
- if isInvalidHeader(witnessTrace[len(witnessTrace)-1].Header, primaryBlock.Header) {
- // height of the common header
- commonHeight = witnessTrace[0].Height
- }
-
- // We are suspecting that the primary is faulty, hence we hold the witness as the source of truth
- // and generate evidence against the primary that we can send to the witness
- primaryEv := &types.LightClientAttackEvidence{
- ConflictingBlock: primaryBlock,
- CommonHeight: commonHeight, // the first block in the bisection is common to both providers
- }
- c.logger.Error("Attempted attack detected. Sending evidence againt primary by witness", "ev", primaryEv,
- "primary", c.primary, "witness", supportingWitness)
- c.sendEvidence(ctx, primaryEv, supportingWitness)
-
- if primaryBlock.Commit.Round != witnessTrace[len(witnessTrace)-1].Commit.Round {
- c.logger.Info("The light client has detected, and prevented, an attempted amnesia attack." +
- " We think this attack is pretty unlikely, so if you see it, that's interesting to us." +
- " Can you let us know by opening an issue through https://github.com/tendermint/tendermint/issues/new?")
- }
-
- // This may not be valid because the witness itself is at fault. So now we reverse it, examining the
- // trace provided by the witness and holding the primary as the source of truth. Note: primary may not
- // respond but this is okay as we will halt anyway.
- primaryTrace, witnessBlock, err := c.examineConflictingHeaderAgainstTrace(
- ctx,
- witnessTrace,
- primaryBlock.SignedHeader,
- c.primary,
- now,
- )
- if err != nil {
- c.logger.Info("Error validating primary's divergent header", "primary", c.primary, "err", err)
- return ErrLightClientAttack
- }
- // if this is an equivocation or amnesia attack, i.e. the validator sets are the same, then we
- // return the height of the conflicting block else if it is a lunatic attack and the validator sets
- // are not the same then we send the height of the common header.
- commonHeight = primaryBlock.Height
- if isInvalidHeader(primaryTrace[len(primaryTrace)-1].Header, witnessBlock.Header) {
- // height of the common header
- commonHeight = primaryTrace[0].Height
- }
-
- // We now use the primary trace to create evidence against the witness and send it to the primary
- witnessEv := &types.LightClientAttackEvidence{
- ConflictingBlock: witnessBlock,
- CommonHeight: commonHeight, // the first block in the bisection is common to both providers
- }
- c.logger.Error("Sending evidence against witness by primary", "ev", witnessEv,
- "primary", c.primary, "witness", supportingWitness)
- c.sendEvidence(ctx, witnessEv, c.primary)
- // We return the error and don't process anymore witnesses
- return ErrLightClientAttack
-
- case errBadWitness:
- c.logger.Info("Witness returned an error during header comparison", "witness", c.witnesses[e.WitnessIndex],
- "err", err)
- // if witness sent us an invalid header, then remove it. If it didn't respond or couldn't find the block, then we
- // ignore it and move on to the next witness
- if _, ok := e.Reason.(provider.ErrBadLightBlock); ok {
- c.logger.Info("Witness sent us invalid header / vals -> removing it", "witness", c.witnesses[e.WitnessIndex])
- witnessesToRemove = append(witnessesToRemove, e.WitnessIndex)
- }
- }
- }
-
- for _, idx := range witnessesToRemove {
- c.removeWitness(idx)
- }
-
- // 1. If we had at least one witness that returned the same header then we
- // conclude that we can trust the header
- if headerMatched {
- return nil
- }
-
- // 2. ELse all witnesses have either not responded, don't have the block or sent invalid blocks.
- return ErrFailedHeaderCrossReferencing
- }
-
- // compareNewHeaderWithWitness takes the verified header from the primary and compares it with a
- // header from a specified witness. The function can return one of three errors:
- //
- // 1: errConflictingHeaders -> there may have been an attack on this light client
- // 2: errBadWitness -> the witness has either not responded, doesn't have the header or has given us an invalid one
- // Note: In the case of an invalid header we remove the witness
- // 3: nil -> the hashes of the two headers match
- func (c *Client) compareNewHeaderWithWitness(ctx context.Context, errc chan error, h *types.SignedHeader,
- witness provider.Provider, witnessIndex int) {
-
- lightBlock, err := witness.LightBlock(ctx, h.Height)
- if err != nil {
- errc <- errBadWitness{Reason: err, WitnessIndex: witnessIndex}
- return
- }
-
- if !bytes.Equal(h.Hash(), lightBlock.Hash()) {
- errc <- errConflictingHeaders{Block: lightBlock, WitnessIndex: witnessIndex}
- }
-
- c.logger.Debug("Matching header received by witness", "height", h.Height, "witness", witnessIndex)
- errc <- nil
- }
-
- // sendEvidence sends evidence to a provider on a best effort basis.
- func (c *Client) sendEvidence(ctx context.Context, ev *types.LightClientAttackEvidence, receiver provider.Provider) {
- err := receiver.ReportEvidence(ctx, ev)
- if err != nil {
- c.logger.Error("Failed to report evidence to provider", "ev", ev, "provider", receiver)
- }
- }
-
- // examineConflictingHeaderAgainstTrace takes a trace from one provider and a divergent header that
- // it has received from another and preforms verifySkipping at the heights of each of the intermediate
- // headers in the trace until it reaches the divergentHeader. 1 of 2 things can happen.
- //
- // 1. The light client verifies a header that is different to the intermediate header in the trace. This
- // is the bifurcation point and the light client can create evidence from it
- // 2. The source stops responding, doesn't have the block or sends an invalid header in which case we
- // return the error and remove the witness
- func (c *Client) examineConflictingHeaderAgainstTrace(
- ctx context.Context,
- trace []*types.LightBlock,
- divergentHeader *types.SignedHeader,
- source provider.Provider, now time.Time) ([]*types.LightBlock, *types.LightBlock, error) {
-
- var previouslyVerifiedBlock *types.LightBlock
-
- for idx, traceBlock := range trace {
- // The first block in the trace MUST be the same to the light block that the source produces
- // else we cannot continue with verification.
- sourceBlock, err := source.LightBlock(ctx, traceBlock.Height)
- if err != nil {
- return nil, nil, err
- }
-
- if idx == 0 {
- if shash, thash := sourceBlock.Hash(), traceBlock.Hash(); !bytes.Equal(shash, thash) {
- return nil, nil, fmt.Errorf("trusted block is different to the source's first block (%X = %X)",
- thash, shash)
- }
- previouslyVerifiedBlock = sourceBlock
- continue
- }
-
- // we check that the source provider can verify a block at the same height of the
- // intermediate height
- trace, err := c.verifySkipping(ctx, source, previouslyVerifiedBlock, sourceBlock, now)
- if err != nil {
- return nil, nil, fmt.Errorf("verifySkipping of conflicting header failed: %w", err)
- }
- // check if the headers verified by the source has diverged from the trace
- if shash, thash := sourceBlock.Hash(), traceBlock.Hash(); !bytes.Equal(shash, thash) {
- // Bifurcation point found!
- return trace, traceBlock, nil
- }
-
- // headers are still the same. update the previouslyVerifiedBlock
- previouslyVerifiedBlock = sourceBlock
- }
-
- // We have reached the end of the trace without observing a divergence. The last header is thus different
- // from the divergent header that the source originally sent us, then we return an error.
- return nil, nil, fmt.Errorf("source provided different header to the original header it provided (%X != %X)",
- previouslyVerifiedBlock.Hash(), divergentHeader.Hash())
-
- }
-
- // isInvalidHeader takes a trusted header and matches it againt a conflicting header
- // to determine whether the conflicting header was the product of a valid state transition
- // or not. If it is then all the deterministic fields of the header should be the same.
- // If not, it is an invalid header and constitutes a lunatic attack.
- func isInvalidHeader(trusted, conflicting *types.Header) bool {
- return !bytes.Equal(trusted.ValidatorsHash, conflicting.ValidatorsHash) ||
- !bytes.Equal(trusted.NextValidatorsHash, conflicting.NextValidatorsHash) ||
- !bytes.Equal(trusted.ConsensusHash, conflicting.ConsensusHash) ||
- !bytes.Equal(trusted.AppHash, conflicting.AppHash) ||
- !bytes.Equal(trusted.LastResultsHash, conflicting.LastResultsHash)
- }
|