github.com/aakash4dev/cometbft@v0.38.2/spec/light-client/attacks/notes-on-evidence-handling.md (about)

     1  
     2  # Light client attacks
     3  
     4  We define a light client attack as detection of conflicting headers for a given height that can be verified
     5  starting from the trusted light block. A light client attack is defined in the context of interactions of
     6  light client with two peers. One of the peers (called primary) defines a trace of verified light blocks
     7  (primary trace) that are being checked against trace of the other peer (called witness) that we call
     8  witness trace.
     9  
    10  A light client attack is defined by the primary and witness traces
    11  that have a common root (the same trusted light block for a common height) but forms
    12  conflicting branches (end of traces is for the same height but with different headers).
    13  Note that conflicting branches could be arbitrarily big as branches continue to diverge after
    14  a bifurcation point. We propose an approach that allows us to define a valid light client attack
    15  only with a common light block and a single conflicting light block. We rely on the fact that
    16  we assume that the primary is under suspicion (therefore not trusted) and that the witness plays
    17  support role to detect and process an attack (therefore trusted). Therefore, once a light client
    18  detects an attack, it needs to send to a witness only missing data (common height
    19  and conflicting light block) as it has its trace. Keeping light client attack data of constant size
    20  saves bandwidth and reduces an attack surface. As we will explain below, although in the context of
    21  light client core
    22  [verification](https://github.com/aakash4dev/cometbft/tree/main/spec/light-client/verification)
    23  the roles of primary and witness are clearly defined,
    24  in case of the attack, we run the same attack detection procedure twice where the roles are swapped.
    25  The rationale is that the light client does not know what peer is correct (on a right main branch)
    26  so it tries to create and submit an attack evidence to both peers.
    27  
    28  Light client attack evidence consists of a conflicting light block and a common height.
    29  
    30  ```go
    31  type LightClientAttackEvidence struct {
    32      ConflictingBlock   LightBlock
    33      CommonHeight       int64
    34  }
    35  ```
    36  
    37  Full node can validate a light client attack evidence by executing the following procedure:
    38  
    39  ```go
    40  func IsValid(lcaEvidence LightClientAttackEvidence, bc Blockchain) boolean {
    41      commonBlock = GetLightBlock(bc, lcaEvidence.CommonHeight)
    42      if commonBlock == nil return false
    43  
    44      // Note that trustingPeriod in ValidAndVerified is set to UNBONDING_PERIOD
    45      verdict = ValidAndVerified(commonBlock, lcaEvidence.ConflictingBlock)
    46      conflictingHeight = lcaEvidence.ConflictingBlock.Header.Height
    47  
    48      return verdict == OK and bc[conflictingHeight].Header != lcaEvidence.ConflictingBlock.Header
    49  }
    50  ```
    51  
    52  ## Light client attack creation
    53  
    54  Given a trusted light block `trusted`, a light node executes the bisection algorithm to verify header
    55  `untrusted` at some height `h`. If the bisection algorithm succeeds, then the header `untrusted` is verified.
    56  Headers that are downloaded as part of the bisection algorithm are stored in a store and they are also in
    57  the verified state. Therefore, after the bisection algorithm successfully terminates we have a trace of
    58  the light blocks ([] LightBlock) we obtained from the primary that we call primary trace.
    59  
    60  ### Primary trace
    61  
    62  The following invariant holds for the primary trace:
    63  
    64  - Given a `trusted` light block, target height `h`, and `primary_trace` ([] LightBlock):
    65      *primary_trace[0] == trusted* and *primary_trace[len(primary_trace)-1].Height == h* and
    66      successive light blocks are passing light client verification logic.
    67  
    68  ### Witness with a conflicting header
    69  
    70  The verified header at height `h` is cross-checked with every witness as part of
    71  [detection](https://github.com/aakash4dev/cometbft/tree/main/spec/light-client/detection).
    72  If a witness returns the conflicting header at the height `h` the following procedure is executed to verify
    73  if the conflicting header comes from the valid trace and if that's the case to create an attack evidence:
    74  
    75  #### Helper functions
    76  
    77  We assume the following helper functions:
    78  
    79  ```go
    80  // Returns trace of verified light blocks starting from rootHeight and ending with targetHeight.
    81  Trace(lightStore LightStore, rootHeight int64, targetHeight int64) LightBlock[]
    82  
    83  // Returns validator set for the given height
    84  GetValidators(bc Blockchain, height int64) Validator[]
    85  
    86  // Returns validator set for the given height
    87  GetValidators(bc Blockchain, height int64) Validator[]
    88  
    89  // Return validator addresses for the given validators
    90  GetAddresses(vals Validator[]) ValidatorAddress[]
    91  ```
    92  
    93  ```go
    94  func DetectLightClientAttacks(primary PeerID,
    95                                primary_trace []LightBlock,
    96                                witness PeerID) (LightClientAttackEvidence, LightClientAttackEvidence) {
    97      primary_lca_evidence, witness_trace = DetectLightClientAttack(primary_trace, witness)
    98  
    99      witness_lca_evidence = nil
   100      if witness_trace != nil {
   101          witness_lca_evidence, _ = DetectLightClientAttack(witness_trace, primary)
   102      }
   103      return primary_lca_evidence, witness_lca_evidence
   104  }
   105  
   106  func DetectLightClientAttack(trace []LightBlock, peer PeerID) (LightClientAttackEvidence, []LightBlock) {
   107  
   108      lightStore = new LightStore().Update(trace[0], StateTrusted)
   109  
   110      for i in 1..len(trace)-1 {
   111          lightStore, result = VerifyToTarget(peer, lightStore, trace[i].Header.Height)
   112  
   113          if result == ResultFailure then return (nil, nil)
   114  
   115          current = lightStore.Get(trace[i].Header.Height)
   116  
   117          // if obtained header is the same as in the trace we continue with a next height
   118          if current.Header == trace[i].Header continue
   119  
   120          // we have identified a conflicting header
   121          commonBlock = trace[i-1]
   122          conflictingBlock = trace[i]
   123  
   124          return (LightClientAttackEvidence { conflictingBlock, commonBlock.Header.Height },
   125                  Trace(lightStore, trace[i-1].Header.Height, trace[i].Header.Height))
   126      }
   127      return (nil, nil)
   128  }
   129  ```
   130  
   131  ## Evidence handling
   132  
   133  As part of on chain evidence handling, full nodes identifies misbehaving processes and informs
   134  the application, so they can be slashed. Note that only bonded validators should
   135  be reported to the application. There are three types of attacks that can be executed against
   136  light client:
   137  
   138  - lunatic attack
   139  - equivocation attack and
   140  - amnesia attack.  
   141  
   142  We now specify the evidence handling logic.
   143  
   144  ```go
   145  func detectMisbehavingProcesses(lcAttackEvidence LightClientAttackEvidence, bc Blockchain) []ValidatorAddress {
   146     assume IsValid(lcaEvidence, bc)
   147  
   148     // lunatic light client attack
   149     if !isValidBlock(current.Header, conflictingBlock.Header) {
   150          conflictingCommit = lcAttackEvidence.ConflictingBlock.Commit
   151          bondedValidators = GetNextValidators(bc, lcAttackEvidence.CommonHeight)
   152  
   153          return getSigners(conflictingCommit) intersection GetAddresses(bondedValidators)
   154  
   155     // equivocation light client attack
   156     } else if current.Header.Round == conflictingBlock.Header.Round {
   157          conflictingCommit = lcAttackEvidence.ConflictingBlock.Commit
   158          trustedCommit = bc[conflictingBlock.Header.Height+1].LastCommit
   159  
   160          return getSigners(trustedCommit) intersection getSigners(conflictingCommit)
   161  
   162     // amnesia light client attack
   163     } else {
   164          HandleAmnesiaAttackEvidence(lcAttackEvidence, bc)
   165     }
   166  }
   167  
   168  // Block validity in this context is defined by the trusted header.
   169  func isValidBlock(trusted Header, conflicting Header) boolean {
   170      return trusted.ValidatorsHash == conflicting.ValidatorsHash and
   171             trusted.NextValidatorsHash == conflicting.NextValidatorsHash and
   172             trusted.ConsensusHash == conflicting.ConsensusHash and
   173             trusted.AppHash == conflicting.AppHash and
   174             trusted.LastResultsHash == conflicting.LastResultsHash
   175  }
   176  
   177  func getSigners(commit Commit) []ValidatorAddress {
   178      signers = []ValidatorAddress
   179      for (i, commitSig) in commit.Signatures {
   180          if commitSig.BlockIDFlag == BlockIDFlagCommit {
   181              signers.append(commitSig.ValidatorAddress)
   182          }
   183      }
   184      return signers
   185  }
   186  ```
   187  
   188  Note that amnesia attack evidence handling involves more complex processing, i.e., cannot be
   189  defined simply on amnesia attack evidence. We explain in the following section a protocol
   190  for handling amnesia attack evidence.
   191  
   192  ### Amnesia attack evidence handling
   193  
   194  Detecting faulty processes in case of the amnesia attack is more complex and cannot be inferred
   195  purely based on attack evidence data. In this case, in order to detect misbehaving processes we need
   196  access to votes processes sent/received during the conflicting height. Therefore, amnesia handling assumes that
   197  validators persist all votes received and sent during multi-round heights (as amnesia attack
   198  is only possible in heights that executes over multiple rounds, i.e., commit round > 0).  
   199  
   200  To simplify description of the algorithm we assume existence of the trusted oracle called monitor that will
   201  drive the algorithm and output faulty processes at the end. Monitor can be implemented in a
   202  distributed setting as on-chain module. The algorithm works as follows:
   203      1) Monitor sends votesets request to validators of the conflicting height. Validators
   204      are expected to send their votesets within predefined timeout.
   205      2) Upon receiving votesets request, validators send their votesets to a monitor.  
   206      2) Validators which have not sent its votesets within timeout are considered faulty.
   207      3) The preprocessing of the votesets is done. That means that the received votesets are analyzed
   208      and each vote (valid) sent by process p is added to the voteset of the sender p. This phase ensures that
   209      votes sent by faulty processes observed by at least one correct validator cannot be excluded from the analysis.
   210      4) Votesets of every validator are analyzed independently to decide whether the validator is correct or faulty.
   211         A faulty validators is the one where at least one of those invalid transitions is found:
   212              - More than one PREVOTE message is sent in a round
   213              - More than one PRECOMMIT message is sent in a round
   214              - PRECOMMIT message is sent without receiving +2/3 of voting-power equivalent
   215              appropriate PREVOTE messages
   216              - PREVOTE message is sent for the value V’ in round r’ and the PRECOMMIT message had
   217              been sent for the value V in round r by the same process (r’ > r) and there are no
   218              +2/3 of voting-power equivalent PREVOTE(vr, V’) messages (vr ≥ 0 and vr > r and vr < r’)
   219              as the justification for sending PREVOTE(r’, V’)