github.com/pure-x-eth/consensus_tm@v0.0.0-20230502163723-e3c2ff987250/light/detector.go (about) 1 package light 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "time" 9 10 "github.com/pure-x-eth/consensus_tm/light/provider" 11 "github.com/pure-x-eth/consensus_tm/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 // 113 // Note: In the case of an invalid header we remove the witness 114 // 115 // 3: nil -> the hashes of the two headers match 116 func (c *Client) compareNewHeaderWithWitness(ctx context.Context, errc chan error, h *types.SignedHeader, 117 witness provider.Provider, witnessIndex int) { 118 119 lightBlock, err := witness.LightBlock(ctx, h.Height) 120 switch err { 121 // no error means we move on to checking the hash of the two headers 122 case nil: 123 break 124 125 // the witness hasn't been helpful in comparing headers, we mark the response and continue 126 // comparing with the rest of the witnesses 127 case provider.ErrNoResponse, provider.ErrLightBlockNotFound, context.DeadlineExceeded, context.Canceled: 128 errc <- err 129 return 130 131 // the witness' head of the blockchain is lower than the height of the primary. This could be one of 132 // two things: 133 // 1) The witness is lagging behind 134 // 2) The primary may be performing a lunatic attack with a height and time in the future 135 case provider.ErrHeightTooHigh: 136 // The light client now asks for the latest header that the witness has 137 var isTargetHeight bool 138 isTargetHeight, lightBlock, err = c.getTargetBlockOrLatest(ctx, h.Height, witness) 139 if err != nil { 140 errc <- err 141 return 142 } 143 144 // if the witness caught up and has returned a block of the target height then we can 145 // break from this switch case and continue to verify the hashes 146 if isTargetHeight { 147 break 148 } 149 150 // witness' last header is below the primary's header. We check the times to see if the blocks 151 // have conflicting times 152 if !lightBlock.Time.Before(h.Time) { 153 errc <- errConflictingHeaders{Block: lightBlock, WitnessIndex: witnessIndex} 154 return 155 } 156 157 // the witness is behind. We wait for a period WAITING = 2 * DRIFT + LAG. 158 // This should give the witness ample time if it is a participating member 159 // of consensus to produce a block that has a time that is after the primary's 160 // block time. If not the witness is too far behind and the light client removes it 161 time.Sleep(2*c.maxClockDrift + c.maxBlockLag) 162 isTargetHeight, lightBlock, err = c.getTargetBlockOrLatest(ctx, h.Height, witness) 163 if err != nil { 164 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { 165 errc <- err 166 } else { 167 errc <- errBadWitness{Reason: err, WitnessIndex: witnessIndex} 168 } 169 return 170 } 171 if isTargetHeight { 172 break 173 } 174 175 // the witness still doesn't have a block at the height of the primary. 176 // Check if there is a conflicting time 177 if !lightBlock.Time.Before(h.Time) { 178 errc <- errConflictingHeaders{Block: lightBlock, WitnessIndex: witnessIndex} 179 return 180 } 181 182 // Following this request response procedure, the witness has been unable to produce a block 183 // that can somehow conflict with the primary's block. We thus conclude that the witness 184 // is too far behind and thus we return a no response error. 185 // 186 // NOTE: If the clock drift / lag has been miscalibrated it is feasible that the light client has 187 // drifted too far ahead for any witness to be able provide a comparable block and thus may allow 188 // for a malicious primary to attack it 189 errc <- provider.ErrNoResponse 190 return 191 192 default: 193 // all other errors (i.e. invalid block, closed connection or unreliable provider) we mark the 194 // witness as bad and remove it 195 errc <- errBadWitness{Reason: err, WitnessIndex: witnessIndex} 196 return 197 } 198 199 if !bytes.Equal(h.Hash(), lightBlock.Hash()) { 200 errc <- errConflictingHeaders{Block: lightBlock, WitnessIndex: witnessIndex} 201 } 202 203 c.logger.Debug("Matching header received by witness", "height", h.Height, "witness", witnessIndex) 204 errc <- nil 205 } 206 207 // sendEvidence sends evidence to a provider on a best effort basis. 208 func (c *Client) sendEvidence(ctx context.Context, ev *types.LightClientAttackEvidence, receiver provider.Provider) { 209 err := receiver.ReportEvidence(ctx, ev) 210 if err != nil { 211 c.logger.Error("Failed to report evidence to provider", "ev", ev, "provider", receiver) 212 } 213 } 214 215 // handleConflictingHeaders handles the primary style of attack, which is where a primary and witness have 216 // two headers of the same height but with different hashes 217 func (c *Client) handleConflictingHeaders( 218 ctx context.Context, 219 primaryTrace []*types.LightBlock, 220 challendingBlock *types.LightBlock, 221 witnessIndex int, 222 now time.Time, 223 ) error { 224 supportingWitness := c.witnesses[witnessIndex] 225 witnessTrace, primaryBlock, err := c.examineConflictingHeaderAgainstTrace( 226 ctx, 227 primaryTrace, 228 challendingBlock, 229 supportingWitness, 230 now, 231 ) 232 if err != nil { 233 c.logger.Info("error validating witness's divergent header", "witness", supportingWitness, "err", err) 234 return nil 235 } 236 237 // We are suspecting that the primary is faulty, hence we hold the witness as the source of truth 238 // and generate evidence against the primary that we can send to the witness 239 commonBlock, trustedBlock := witnessTrace[0], witnessTrace[len(witnessTrace)-1] 240 evidenceAgainstPrimary := newLightClientAttackEvidence(primaryBlock, trustedBlock, commonBlock) 241 c.logger.Error("ATTEMPTED ATTACK DETECTED. Sending evidence againt primary by witness", "ev", evidenceAgainstPrimary, 242 "primary", c.primary, "witness", supportingWitness) 243 c.sendEvidence(ctx, evidenceAgainstPrimary, supportingWitness) 244 245 if primaryBlock.Commit.Round != witnessTrace[len(witnessTrace)-1].Commit.Round { 246 c.logger.Info("The light client has detected, and prevented, an attempted amnesia attack." + 247 " We think this attack is pretty unlikely, so if you see it, that's interesting to us." + 248 " Can you let us know by opening an issue through https://github.com/pure-x-eth/consensus_tm/issues/new?") 249 } 250 251 // This may not be valid because the witness itself is at fault. So now we reverse it, examining the 252 // trace provided by the witness and holding the primary as the source of truth. Note: primary may not 253 // respond but this is okay as we will halt anyway. 254 primaryTrace, witnessBlock, err := c.examineConflictingHeaderAgainstTrace( 255 ctx, 256 witnessTrace, 257 primaryBlock, 258 c.primary, 259 now, 260 ) 261 if err != nil { 262 c.logger.Info("Error validating primary's divergent header", "primary", c.primary, "err", err) 263 return ErrLightClientAttack 264 } 265 266 // We now use the primary trace to create evidence against the witness and send it to the primary 267 commonBlock, trustedBlock = primaryTrace[0], primaryTrace[len(primaryTrace)-1] 268 evidenceAgainstWitness := newLightClientAttackEvidence(witnessBlock, trustedBlock, commonBlock) 269 c.logger.Error("Sending evidence against witness by primary", "ev", evidenceAgainstWitness, 270 "primary", c.primary, "witness", supportingWitness) 271 c.sendEvidence(ctx, evidenceAgainstWitness, c.primary) 272 // We return the error and don't process anymore witnesses 273 return ErrLightClientAttack 274 } 275 276 // examineConflictingHeaderAgainstTrace takes a trace from one provider and a divergent header that 277 // it has received from another and preforms verifySkipping at the heights of each of the intermediate 278 // headers in the trace until it reaches the divergentHeader. 1 of 2 things can happen. 279 // 280 // 1. The light client verifies a header that is different to the intermediate header in the trace. This 281 // is the bifurcation point and the light client can create evidence from it 282 // 2. The source stops responding, doesn't have the block or sends an invalid header in which case we 283 // return the error and remove the witness 284 // 285 // CONTRACT: 286 // 1. Trace can not be empty len(trace) > 0 287 // 2. The last block in the trace can not be of a lower height than the target block 288 // trace[len(trace)-1].Height >= targetBlock.Height 289 // 3. The 290 func (c *Client) examineConflictingHeaderAgainstTrace( 291 ctx context.Context, 292 trace []*types.LightBlock, 293 targetBlock *types.LightBlock, 294 source provider.Provider, now time.Time, 295 ) ([]*types.LightBlock, *types.LightBlock, error) { 296 297 var ( 298 previouslyVerifiedBlock, sourceBlock *types.LightBlock 299 sourceTrace []*types.LightBlock 300 err error 301 ) 302 303 if targetBlock.Height < trace[0].Height { 304 return nil, nil, fmt.Errorf("target block has a height lower than the trusted height (%d < %d)", 305 targetBlock.Height, trace[0].Height) 306 } 307 308 for idx, traceBlock := range trace { 309 // this case only happens in a forward lunatic attack. We treat the block with the 310 // height directly after the targetBlock as the divergent block 311 if traceBlock.Height > targetBlock.Height { 312 // sanity check that the time of the traceBlock is indeed less than that of the targetBlock. If the trace 313 // was correctly verified we should expect monotonically increasing time. This means that if the block at 314 // the end of the trace has a lesser time than the target block then all blocks in the trace should have a 315 // lesser time 316 if traceBlock.Time.After(targetBlock.Time) { 317 return nil, nil, 318 errors.New("sanity check failed: expected traceblock to have a lesser time than the target block") 319 } 320 321 // before sending back the divergent block and trace we need to ensure we have verified 322 // the final gap between the previouslyVerifiedBlock and the targetBlock 323 if previouslyVerifiedBlock.Height != targetBlock.Height { 324 sourceTrace, err = c.verifySkipping(ctx, source, previouslyVerifiedBlock, targetBlock, now) 325 if err != nil { 326 return nil, nil, fmt.Errorf("verifySkipping of conflicting header failed: %w", err) 327 } 328 } 329 return sourceTrace, traceBlock, nil 330 } 331 332 // get the corresponding block from the source to verify and match up against the traceBlock 333 if traceBlock.Height == targetBlock.Height { 334 sourceBlock = targetBlock 335 } else { 336 sourceBlock, err = source.LightBlock(ctx, traceBlock.Height) 337 if err != nil { 338 return nil, nil, fmt.Errorf("failed to examine trace: %w", err) 339 } 340 } 341 342 // The first block in the trace MUST be the same to the light block that the source produces 343 // else we cannot continue with verification. 344 if idx == 0 { 345 if shash, thash := sourceBlock.Hash(), traceBlock.Hash(); !bytes.Equal(shash, thash) { 346 return nil, nil, fmt.Errorf("trusted block is different to the source's first block (%X = %X)", 347 thash, shash) 348 } 349 previouslyVerifiedBlock = sourceBlock 350 continue 351 } 352 353 // we check that the source provider can verify a block at the same height of the 354 // intermediate height 355 sourceTrace, err = c.verifySkipping(ctx, source, previouslyVerifiedBlock, sourceBlock, now) 356 if err != nil { 357 return nil, nil, fmt.Errorf("verifySkipping of conflicting header failed: %w", err) 358 } 359 // check if the headers verified by the source has diverged from the trace 360 if shash, thash := sourceBlock.Hash(), traceBlock.Hash(); !bytes.Equal(shash, thash) { 361 // Bifurcation point found! 362 return sourceTrace, traceBlock, nil 363 } 364 365 // headers are still the same. update the previouslyVerifiedBlock 366 previouslyVerifiedBlock = sourceBlock 367 } 368 369 // We have reached the end of the trace. This should never happen. This can only happen if one of the stated 370 // prerequisites to this function were not met. Namely that either trace[len(trace)-1].Height < targetBlock.Height 371 // or that trace[i].Hash() != targetBlock.Hash() 372 return nil, nil, errNoDivergence 373 374 } 375 376 // getTargetBlockOrLatest gets the latest height, if it is greater than the target height then it queries 377 // the target heght else it returns the latest. returns true if it successfully managed to acquire the target 378 // height. 379 func (c *Client) getTargetBlockOrLatest( 380 ctx context.Context, 381 height int64, 382 witness provider.Provider, 383 ) (bool, *types.LightBlock, error) { 384 lightBlock, err := witness.LightBlock(ctx, 0) 385 if err != nil { 386 return false, nil, err 387 } 388 389 if lightBlock.Height == height { 390 // the witness has caught up to the height of the provider's signed header. We 391 // can resume with checking the hashes. 392 return true, lightBlock, nil 393 } 394 395 if lightBlock.Height > height { 396 // the witness has caught up. We recursively call the function again. However in order 397 // to avoud a wild goose chase where the witness sends us one header below and one header 398 // above the height we set a timeout to the context 399 lightBlock, err := witness.LightBlock(ctx, height) 400 return true, lightBlock, err 401 } 402 403 return false, lightBlock, nil 404 } 405 406 // newLightClientAttackEvidence determines the type of attack and then forms the evidence filling out 407 // all the fields such that it is ready to be sent to a full node. 408 func newLightClientAttackEvidence(conflicted, trusted, common *types.LightBlock) *types.LightClientAttackEvidence { 409 ev := &types.LightClientAttackEvidence{ConflictingBlock: conflicted} 410 // if this is an equivocation or amnesia attack, i.e. the validator sets are the same, then we 411 // return the height of the conflicting block else if it is a lunatic attack and the validator sets 412 // are not the same then we send the height of the common header. 413 if ev.ConflictingHeaderIsInvalid(trusted.Header) { 414 ev.CommonHeight = common.Height 415 ev.Timestamp = common.Time 416 ev.TotalVotingPower = common.ValidatorSet.TotalVotingPower() 417 } else { 418 ev.CommonHeight = trusted.Height 419 ev.Timestamp = trusted.Time 420 ev.TotalVotingPower = trusted.ValidatorSet.TotalVotingPower() 421 } 422 ev.ByzantineValidators = ev.GetByzantineValidators(common.ValidatorSet, trusted.SignedHeader) 423 return ev 424 }