mirror of
https://github.com/tendermint/tendermint.git
synced 2026-02-10 22:10:11 +00:00
Current versions of light client specs from tendermint-rs (#158)
* current versions of light client specs from tendermint-rs * markdown lint * linting * links * links * links Co-authored-by: Marko Baricevic <marbar3778@yahoo.com>
This commit is contained in:
219
rust-spec/lightclient/attacks/evidence-handling.md
Normal file
219
rust-spec/lightclient/attacks/evidence-handling.md
Normal file
@@ -0,0 +1,219 @@
|
||||
|
||||
# Light client attacks
|
||||
|
||||
We define a light client attack as detection of conflicting headers for a given height that can be verified
|
||||
starting from the trusted light block. A light client attack is defined in the context of interactions of
|
||||
light client with two peers. One of the peers (called primary) defines a trace of verified light blocks
|
||||
(primary trace) that are being checked against trace of the other peer (called witness) that we call
|
||||
witness trace.
|
||||
|
||||
A light client attack is defined by the primary and witness traces
|
||||
that have a common root (the same trusted light block for a common height) but forms
|
||||
conflicting branches (end of traces is for the same height but with different headers).
|
||||
Note that conflicting branches could be arbitrarily big as branches continue to diverge after
|
||||
a bifurcation point. We propose an approach that allows us to define a valid light client attack
|
||||
only with a common light block and a single conflicting light block. We rely on the fact that
|
||||
we assume that the primary is under suspicion (therefore not trusted) and that the witness plays
|
||||
support role to detect and process an attack (therefore trusted). Therefore, once a light client
|
||||
detects an attack, it needs to send to a witness only missing data (common height
|
||||
and conflicting light block) as it has its trace. Keeping light client attack data of constant size
|
||||
saves bandwidth and reduces an attack surface. As we will explain below, although in the context of
|
||||
light client core
|
||||
[verification](https://github.com/informalsystems/tendermint-rs/tree/master/docs/spec/lightclient/verification)
|
||||
the roles of primary and witness are clearly defined,
|
||||
in case of the attack, we run the same attack detection procedure twice where the roles are swapped.
|
||||
The rationale is that the light client does not know what peer is correct (on a right main branch)
|
||||
so it tries to create and submit an attack evidence to both peers.
|
||||
|
||||
Light client attack evidence consists of a conflicting light block and a common height.
|
||||
|
||||
```go
|
||||
type LightClientAttackEvidence struct {
|
||||
ConflictingBlock LightBlock
|
||||
CommonHeight int64
|
||||
}
|
||||
```
|
||||
|
||||
Full node can validate a light client attack evidence by executing the following procedure:
|
||||
|
||||
```go
|
||||
func IsValid(lcaEvidence LightClientAttackEvidence, bc Blockchain) boolean {
|
||||
commonBlock = GetLightBlock(bc, lcaEvidence.CommonHeight)
|
||||
if commonBlock == nil return false
|
||||
|
||||
// Note that trustingPeriod in ValidAndVerified is set to UNBONDING_PERIOD
|
||||
verdict = ValidAndVerified(commonBlock, lcaEvidence.ConflictingBlock)
|
||||
conflictingHeight = lcaEvidence.ConflictingBlock.Header.Height
|
||||
|
||||
return verdict == OK and bc[conflictingHeight].Header != lcaEvidence.ConflictingBlock.Header
|
||||
}
|
||||
```
|
||||
|
||||
## Light client attack creation
|
||||
|
||||
Given a trusted light block `trusted`, a light node executes the bisection algorithm to verify header
|
||||
`untrusted` at some height `h`. If the bisection algorithm succeeds, then the header `untrusted` is verified.
|
||||
Headers that are downloaded as part of the bisection algorithm are stored in a store and they are also in
|
||||
the verified state. Therefore, after the bisection algorithm successfully terminates we have a trace of
|
||||
the light blocks ([] LightBlock) we obtained from the primary that we call primary trace.
|
||||
|
||||
### Primary trace
|
||||
|
||||
The following invariant holds for the primary trace:
|
||||
|
||||
- Given a `trusted` light block, target height `h`, and `primary_trace` ([] LightBlock):
|
||||
*primary_trace[0] == trusted* and *primary_trace[len(primary_trace)-1].Height == h* and
|
||||
successive light blocks are passing light client verification logic.
|
||||
|
||||
### Witness with a conflicting header
|
||||
|
||||
The verified header at height `h` is cross-checked with every witness as part of
|
||||
[detection](https://github.com/informalsystems/tendermint-rs/tree/master/docs/spec/lightclient/detection).
|
||||
If a witness returns the conflicting header at the height `h` the following procedure is executed to verify
|
||||
if the conflicting header comes from the valid trace and if that's the case to create an attack evidence:
|
||||
|
||||
#### Helper functions
|
||||
|
||||
We assume the following helper functions:
|
||||
|
||||
```go
|
||||
// Returns trace of verified light blocks starting from rootHeight and ending with targetHeight.
|
||||
Trace(lightStore LightStore, rootHeight int64, targetHeight int64) LightBlock[]
|
||||
|
||||
// Returns validator set for the given height
|
||||
GetValidators(bc Blockchain, height int64) Validator[]
|
||||
|
||||
// Returns validator set for the given height
|
||||
GetValidators(bc Blockchain, height int64) Validator[]
|
||||
|
||||
// Return validator addresses for the given validators
|
||||
GetAddresses(vals Validator[]) ValidatorAddress[]
|
||||
```
|
||||
|
||||
```go
|
||||
func DetectLightClientAttacks(primary PeerID,
|
||||
primary_trace []LightBlock,
|
||||
witness PeerID) (LightClientAttackEvidence, LightClientAttackEvidence) {
|
||||
primary_lca_evidence, witness_trace = DetectLightClientAttack(primary_trace, witness)
|
||||
|
||||
witness_lca_evidence = nil
|
||||
if witness_trace != nil {
|
||||
witness_lca_evidence, _ = DetectLightClientAttack(witness_trace, primary)
|
||||
}
|
||||
return primary_lca_evidence, witness_lca_evidence
|
||||
}
|
||||
|
||||
func DetectLightClientAttack(trace []LightBlock, peer PeerID) (LightClientAttackEvidence, []LightBlock) {
|
||||
|
||||
lightStore = new LightStore().Update(trace[0], StateTrusted)
|
||||
|
||||
for i in 1..len(trace)-1 {
|
||||
lightStore, result = VerifyToTarget(peer, lightStore, trace[i].Header.Height)
|
||||
|
||||
if result == ResultFailure then return (nil, nil)
|
||||
|
||||
current = lightStore.Get(trace[i].Header.Height)
|
||||
|
||||
// if obtained header is the same as in the trace we continue with a next height
|
||||
if current.Header == trace[i].Header continue
|
||||
|
||||
// we have identified a conflicting header
|
||||
commonBlock = trace[i-1]
|
||||
conflictingBlock = trace[i]
|
||||
|
||||
return (LightClientAttackEvidence { conflictingBlock, commonBlock.Header.Height },
|
||||
Trace(lightStore, trace[i-1].Header.Height, trace[i].Header.Height))
|
||||
}
|
||||
return (nil, nil)
|
||||
}
|
||||
```
|
||||
|
||||
## Evidence handling
|
||||
|
||||
As part of on chain evidence handling, full nodes identifies misbehaving processes and informs
|
||||
the application, so they can be slashed. Note that only bonded validators should
|
||||
be reported to the application. There are three types of attacks that can be executed against
|
||||
Tendermint light client:
|
||||
|
||||
- lunatic attack
|
||||
- equivocation attack and
|
||||
- amnesia attack.
|
||||
|
||||
We now specify the evidence handling logic.
|
||||
|
||||
```go
|
||||
func detectMisbehavingProcesses(lcAttackEvidence LightClientAttackEvidence, bc Blockchain) []ValidatorAddress {
|
||||
assume IsValid(lcaEvidence, bc)
|
||||
|
||||
// lunatic light client attack
|
||||
if !isValidBlock(current.Header, conflictingBlock.Header) {
|
||||
conflictingCommit = lcAttackEvidence.ConflictingBlock.Commit
|
||||
bondedValidators = GetNextValidators(bc, lcAttackEvidence.CommonHeight)
|
||||
|
||||
return getSigners(conflictingCommit) intersection GetAddresses(bondedValidators)
|
||||
|
||||
// equivocation light client attack
|
||||
} else if current.Header.Round == conflictingBlock.Header.Round {
|
||||
conflictingCommit = lcAttackEvidence.ConflictingBlock.Commit
|
||||
trustedCommit = bc[conflictingBlock.Header.Height+1].LastCommit
|
||||
|
||||
return getSigners(trustedCommit) intersection getSigners(conflictingCommit)
|
||||
|
||||
// amnesia light client attack
|
||||
} else {
|
||||
HandleAmnesiaAttackEvidence(lcAttackEvidence, bc)
|
||||
}
|
||||
}
|
||||
|
||||
// Block validity in this context is defined by the trusted header.
|
||||
func isValidBlock(trusted Header, conflicting Header) boolean {
|
||||
return trusted.ValidatorsHash == conflicting.ValidatorsHash and
|
||||
trusted.NextValidatorsHash == conflicting.NextValidatorsHash and
|
||||
trusted.ConsensusHash == conflicting.ConsensusHash and
|
||||
trusted.AppHash == conflicting.AppHash and
|
||||
trusted.LastResultsHash == conflicting.LastResultsHash
|
||||
}
|
||||
|
||||
func getSigners(commit Commit) []ValidatorAddress {
|
||||
signers = []ValidatorAddress
|
||||
for (i, commitSig) in commit.Signatures {
|
||||
if commitSig.BlockIDFlag == BlockIDFlagCommit {
|
||||
signers.append(commitSig.ValidatorAddress)
|
||||
}
|
||||
}
|
||||
return signers
|
||||
}
|
||||
```
|
||||
|
||||
Note that amnesia attack evidence handling involves more complex processing, i.e., cannot be
|
||||
defined simply on amnesia attack evidence. We explain in the following section a protocol
|
||||
for handling amnesia attack evidence.
|
||||
|
||||
### Amnesia attack evidence handling
|
||||
|
||||
Detecting faulty processes in case of the amnesia attack is more complex and cannot be inferred
|
||||
purely based on attack evidence data. In this case, in order to detect misbehaving processes we need
|
||||
access to votes processes sent/received during the conflicting height. Therefore, amnesia handling assumes that
|
||||
validators persist all votes received and sent during multi-round heights (as amnesia attack
|
||||
is only possible in heights that executes over multiple rounds, i.e., commit round > 0).
|
||||
|
||||
To simplify description of the algorithm we assume existence of the trusted oracle called monitor that will
|
||||
drive the algorithm and output faulty processes at the end. Monitor can be implemented in a
|
||||
distributed setting as on-chain module. The algorithm works as follows:
|
||||
1) Monitor sends votesets request to validators of the conflicting height. Validators
|
||||
are expected to send their votesets within predefined timeout.
|
||||
2) Upon receiving votesets request, validators send their votesets to a monitor.
|
||||
2) Validators which have not sent its votesets within timeout are considered faulty.
|
||||
3) The preprocessing of the votesets is done. That means that the received votesets are analyzed
|
||||
and each vote (valid) sent by process p is added to the voteset of the sender p. This phase ensures that
|
||||
votes sent by faulty processes observed by at least one correct validator cannot be excluded from the analysis.
|
||||
4) Votesets of every validator are analyzed independently to decide whether the validator is correct or faulty.
|
||||
A faulty validators is the one where at least one of those invalid transitions is found:
|
||||
- More than one PREVOTE message is sent in a round
|
||||
- More than one PRECOMMIT message is sent in a round
|
||||
- PRECOMMIT message is sent without receiving +2/3 of voting-power equivalent
|
||||
appropriate PREVOTE messages
|
||||
- PREVOTE message is sent for the value V’ in round r’ and the PRECOMMIT message had
|
||||
been sent for the value V in round r by the same process (r’ > r) and there are no
|
||||
+2/3 of voting-power equivalent PREVOTE(vr, V’) messages (vr ≥ 0 and vr > r and vr < r’)
|
||||
as the justification for sending PREVOTE(r’, V’)
|
||||
Reference in New Issue
Block a user