github.com/lazyledger/lazyledger-core@v0.35.0-dev.0.20210613111200-4c651f053571/light/detector.go (about) 1 package light 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "time" 9 10 "github.com/lazyledger/lazyledger-core/light/provider" 11 "github.com/lazyledger/lazyledger-core/types" 12 ) 13 14 // The detector component of the light client detect 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 conflicting 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 68 supportingWitness := c.witnesses[e.WitnessIndex] 69 witnessTrace, primaryBlock, err := c.examineConflictingHeaderAgainstTrace( 70 ctx, 71 primaryTrace, 72 e.Block.SignedHeader, 73 supportingWitness, 74 now, 75 ) 76 if err != nil { 77 c.logger.Info("Error validating witness's divergent header", "witness", supportingWitness, "err", err) 78 witnessesToRemove = append(witnessesToRemove, e.WitnessIndex) 79 continue 80 } 81 82 // We are suspecting that the primary is faulty, hence we hold the witness as the source of truth 83 // and generate evidence against the primary that we can send to the witness 84 primaryEv := newLightClientAttackEvidence(primaryBlock, witnessTrace[len(witnessTrace)-1], witnessTrace[0]) 85 c.logger.Error("Attempted attack detected. Sending evidence againt primary by witness", "ev", primaryEv, 86 "primary", c.primary, "witness", supportingWitness) 87 c.sendEvidence(ctx, primaryEv, supportingWitness) 88 89 if primaryBlock.Commit.Round != witnessTrace[len(witnessTrace)-1].Commit.Round { 90 c.logger.Info("The light client has detected, and prevented, an attempted amnesia attack." + 91 " We think this attack is pretty unlikely, so if you see it, that's interesting to us." + 92 " Can you let us know by opening an issue through https://github.com/tendermint/tendermint/issues/new?") 93 } 94 95 // This may not be valid because the witness itself is at fault. So now we reverse it, examining the 96 // trace provided by the witness and holding the primary as the source of truth. Note: primary may not 97 // respond but this is okay as we will halt anyway. 98 primaryTrace, witnessBlock, err := c.examineConflictingHeaderAgainstTrace( 99 ctx, 100 witnessTrace, 101 primaryBlock.SignedHeader, 102 c.primary, 103 now, 104 ) 105 if err != nil { 106 c.logger.Info("Error validating primary's divergent header", "primary", c.primary, "err", err) 107 return ErrLightClientAttack 108 } 109 110 // We now use the primary trace to create evidence against the witness and send it to the primary 111 witnessEv := newLightClientAttackEvidence(witnessBlock, primaryTrace[len(primaryTrace)-1], primaryTrace[0]) 112 c.logger.Error("Sending evidence against witness by primary", "ev", witnessEv, 113 "primary", c.primary, "witness", supportingWitness) 114 c.sendEvidence(ctx, witnessEv, c.primary) 115 // We return the error and don't process anymore witnesses 116 return ErrLightClientAttack 117 118 case errBadWitness: 119 c.logger.Info("Witness returned an error during header comparison", "witness", c.witnesses[e.WitnessIndex], 120 "err", err) 121 // if witness sent us an invalid header, then remove it. If it didn't respond or couldn't find the block, then we 122 // ignore it and move on to the next witness 123 if _, ok := e.Reason.(provider.ErrBadLightBlock); ok { 124 c.logger.Info("Witness sent us invalid header / vals -> removing it", "witness", c.witnesses[e.WitnessIndex]) 125 witnessesToRemove = append(witnessesToRemove, e.WitnessIndex) 126 } 127 } 128 } 129 130 for _, idx := range witnessesToRemove { 131 c.removeWitness(idx) 132 } 133 134 // 1. If we had at least one witness that returned the same header then we 135 // conclude that we can trust the header 136 if headerMatched { 137 return nil 138 } 139 140 // 2. ELse all witnesses have either not responded, don't have the block or sent invalid blocks. 141 return ErrFailedHeaderCrossReferencing 142 } 143 144 // compareNewHeaderWithWitness takes the verified header from the primary and compares it with a 145 // header from a specified witness. The function can return one of three errors: 146 // 147 // 1: errConflictingHeaders -> there may have been an attack on this light client 148 // 2: errBadWitness -> the witness has either not responded, doesn't have the header or has given us an invalid one 149 // Note: In the case of an invalid header we remove the witness 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 }