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’)