@ -11,7 +11,7 @@ import (
"github.com/tendermint/tendermint/types"
)
// The detector component of the light client detect and handles attacks on the light client.
// The detector component of the light client detects and handles attacks on the light client.
// More info here:
// tendermint/docs/architecture/adr-047-handling-evidence-from-light-client.md
@ -20,7 +20,7 @@ import (
// It takes the target verified header and compares it with the headers of a set of
// witness providers that the light client is connected to. If a conflicting header
// is returned it verifies and examines the conflicting header against the verified
// trace that was produced from the primary. If successful it produces two sets of evidence
// trace that was produced from the primary. If successful, it produces two sets of evidence
// and sends them to the opposite provider before halting.
//
// If there are no conflictinge headers, the light client deems the verified target header
@ -64,56 +64,14 @@ func (c *Client) detectDivergence(ctx context.Context, primaryTrace []*types.Lig
// need to find the point that the headers diverge and examine this for any evidence of an attack.
//
// We combine these actions together, verifying the witnesses headers and outputting the trace
// which captures the bifurcation point and if successful provides the information to create
supportingWitness := c . witnesses [ e . WitnessIndex ]
witnessTrace , primaryBlock , err := c . examineConflictingHeaderAgainstTrace (
ctx ,
primaryTrace ,
e . Block . SignedHeader ,
supportingWitness ,
now ,
)
// which captures the bifurcation point and if successful provides the information to create valid evidence.
err := c . handleConflictingHeaders ( ctx , primaryTrace , e . Block , e . WitnessIndex , now )
if err != nil {
c . logger . Info ( "error validating witness's divergent header" , "witness" , supportingWitness , "err" , err )
witnessesToRemove = append ( witnessesToRemove , e . WitnessIndex )
continue
// return information of the attack
return err
}
// We are suspecting that the primary is faulty, hence we hold the witness as the source of truth
// and generate evidence against the primary that we can send to the witness
primaryEv := newLightClientAttackEvidence ( primaryBlock , witnessTrace [ len ( witnessTrace ) - 1 ] , witnessTrace [ 0 ] )
c . logger . Error ( "ATTEMPTED ATTACK DETECTED. Sending evidence againt primary by witness" , "ev" , primaryEv ,
"primary" , c . primary , "witness" , supportingWitness )
c . sendEvidence ( ctx , primaryEv , supportingWitness )
if primaryBlock . Commit . Round != witnessTrace [ len ( witnessTrace ) - 1 ] . Commit . Round {
c . logger . Info ( "The light client has detected, and prevented, an attempted amnesia attack." +
" We think this attack is pretty unlikely, so if you see it, that's interesting to us." +
" Can you let us know by opening an issue through https://github.com/tendermint/tendermint/issues/new?" )
}
// This may not be valid because the witness itself is at fault. So now we reverse it, examining the
// trace provided by the witness and holding the primary as the source of truth. Note: primary may not
// respond but this is okay as we will halt anyway.
primaryTrace , witnessBlock , err := c . examineConflictingHeaderAgainstTrace (
ctx ,
witnessTrace ,
primaryBlock . SignedHeader ,
c . primary ,
now ,
)
if err != nil {
c . logger . Info ( "Error validating primary's divergent header" , "primary" , c . primary , "err" , err )
return ErrLightClientAttack
}
// We now use the primary trace to create evidence against the witness and send it to the primary
witnessEv := newLightClientAttackEvidence ( witnessBlock , primaryTrace [ len ( primaryTrace ) - 1 ] , primaryTrace [ 0 ] )
c . logger . Error ( "Sending evidence against witness by primary" , "ev" , witnessEv ,
"primary" , c . primary , "witness" , supportingWitness )
c . sendEvidence ( ctx , witnessEv , c . primary )
// We return the error and don't process anymore witnesses
return ErrLightClientAttack
// if attempt to generate conflicting headers failed then remove witness
witnessesToRemove = append ( witnessesToRemove , e . WitnessIndex )
case errBadWitness :
c . logger . Info ( "witness returned an error during header comparison, removing..." ,
@ -135,7 +93,7 @@ func (c *Client) detectDivergence(ctx context.Context, primaryTrace []*types.Lig
return nil
}
// 2. EL se all witnesses have either not responded, don't have the block or sent invalid blocks.
// 2. El se all witnesses have either not responded, don't have the block or sent invalid blocks.
return ErrFailedHeaderCrossReferencing
}
@ -151,16 +109,76 @@ func (c *Client) compareNewHeaderWithWitness(ctx context.Context, errc chan erro
lightBlock , err := witness . LightBlock ( ctx , h . Height )
switch err {
// no error means we move on to checking the hash of the two headers
case nil :
break
// the witness hasn't been helpful in comparing headers, we mark the response and continue
// comparing with the rest of the witnesses
case provider . ErrNoResponse , provider . ErrLightBlockNotFound :
errc <- err
return
// the witness' head of the blockchain is lower than the height of the primary. This could be one of
// two things:
// 1) The witness is lagging behind
// 2) The primary may be performing a lunatic attack with a height and time in the future
case provider . ErrHeightTooHigh :
// The light client now asks for the latest header that the witness has
var isTargetHeight bool
isTargetHeight , lightBlock , err = c . getTargetBlockOrLatest ( ctx , h . Height , witness )
if err != nil {
errc <- err
return
}
// if the witness caught up and has returned a block of the target height then we can
// break from this switch case and continue to verify the hashes
if isTargetHeight {
break
}
// witness' last header is below the primary's header. We check the times to see if the blocks
// have conflicting times
if ! lightBlock . Time . Before ( h . Time ) {
errc <- errConflictingHeaders { Block : lightBlock , WitnessIndex : witnessIndex }
return
}
// the witness is behind. We wait for a period WAITING = 2 * DRIFT + LAG.
// This should give the witness ample time if it is a participating member
// of consensus to produce a block that has a time that is after the primary's
// block time. If not the witness is too far behind and the light client removes it
time . Sleep ( 2 * c . maxClockDrift + c . maxBlockLag )
isTargetHeight , lightBlock , err = c . getTargetBlockOrLatest ( ctx , h . Height , witness )
if err != nil {
errc <- errBadWitness { Reason : err , WitnessIndex : witnessIndex }
return
}
if isTargetHeight {
break
}
// the witness still doesn't have a block at the height of the primary.
// Check if there is a conflicting time
if ! lightBlock . Time . Before ( h . Time ) {
errc <- errConflictingHeaders { Block : lightBlock , WitnessIndex : witnessIndex }
return
}
// Following this request response procedure, the witness has been unable to produce a block
// that can somehow conflict with the primary's block. We thus conclude that the witness
// is too far behind and thus we return a no response error.
//
// NOTE: If the clock drift / lag has been miscalibrated it is feasible that the light client has
// drifted too far ahead for any witness to be able provide a comparable block and thus may allow
// for a malicious primary to attack it
errc <- provider . ErrNoResponse
return
default :
// all other errors (i.e. invalid block or unreliable provider) we mark the witness as bad
// and remove it
// all other errors (i.e. invalid block, closed connection or unreliable provider) we mark the
// witness as bad and remove it
errc <- errBadWitness { Reason : err , WitnessIndex : witnessIndex }
return
}
@ -181,6 +199,67 @@ func (c *Client) sendEvidence(ctx context.Context, ev *types.LightClientAttackEv
}
}
// handleConflictingHeaders handles the primary style of attack, which is where a primary and witness have
// two headers of the same height but with different hashes
func ( c * Client ) handleConflictingHeaders (
ctx context . Context ,
primaryTrace [ ] * types . LightBlock ,
challendingBlock * types . LightBlock ,
witnessIndex int ,
now time . Time ,
) error {
supportingWitness := c . witnesses [ witnessIndex ]
witnessTrace , primaryBlock , err := c . examineConflictingHeaderAgainstTrace (
ctx ,
primaryTrace ,
challendingBlock ,
supportingWitness ,
now ,
)
if err != nil {
c . logger . Info ( "error validating witness's divergent header" , "witness" , supportingWitness , "err" , err )
return nil
}
// We are suspecting that the primary is faulty, hence we hold the witness as the source of truth
// and generate evidence against the primary that we can send to the witness
commonBlock , trustedBlock := witnessTrace [ 0 ] , witnessTrace [ len ( witnessTrace ) - 1 ]
evidenceAgainstPrimary := newLightClientAttackEvidence ( primaryBlock , trustedBlock , commonBlock )
c . logger . Error ( "ATTEMPTED ATTACK DETECTED. Sending evidence againt primary by witness" , "ev" , evidenceAgainstPrimary ,
"primary" , c . primary , "witness" , supportingWitness )
c . sendEvidence ( ctx , evidenceAgainstPrimary , supportingWitness )
if primaryBlock . Commit . Round != witnessTrace [ len ( witnessTrace ) - 1 ] . Commit . Round {
c . logger . Info ( "The light client has detected, and prevented, an attempted amnesia attack." +
" We think this attack is pretty unlikely, so if you see it, that's interesting to us." +
" Can you let us know by opening an issue through https://github.com/tendermint/tendermint/issues/new?" )
}
// This may not be valid because the witness itself is at fault. So now we reverse it, examining the
// trace provided by the witness and holding the primary as the source of truth. Note: primary may not
// respond but this is okay as we will halt anyway.
primaryTrace , witnessBlock , err := c . examineConflictingHeaderAgainstTrace (
ctx ,
witnessTrace ,
primaryBlock ,
c . primary ,
now ,
)
if err != nil {
c . logger . Info ( "Error validating primary's divergent header" , "primary" , c . primary , "err" , err )
return ErrLightClientAttack
}
// We now use the primary trace to create evidence against the witness and send it to the primary
commonBlock , trustedBlock = primaryTrace [ 0 ] , primaryTrace [ len ( primaryTrace ) - 1 ]
evidenceAgainstWitness := newLightClientAttackEvidence ( witnessBlock , trustedBlock , commonBlock )
c . logger . Error ( "Sending evidence against witness by primary" , "ev" , evidenceAgainstWitness ,
"primary" , c . primary , "witness" , supportingWitness )
c . sendEvidence ( ctx , evidenceAgainstWitness , c . primary )
// We return the error and don't process anymore witnesses
return ErrLightClientAttack
}
// examineConflictingHeaderAgainstTrace takes a trace from one provider and a divergent header that
// it has received from another and preforms verifySkipping at the heights of each of the intermediate
// headers in the trace until it reaches the divergentHeader. 1 of 2 things can happen.
@ -189,22 +268,66 @@ func (c *Client) sendEvidence(ctx context.Context, ev *types.LightClientAttackEv
// is the bifurcation point and the light client can create evidence from it
// 2. The source stops responding, doesn't have the block or sends an invalid header in which case we
// return the error and remove the witness
//
// CONTRACT:
// 1. Trace can not be empty len(trace) > 0
// 2. The last block in the trace can not be of a lower height than the target block
// trace[len(trace)-1].Height >= targetBlock.Height
// 3. The
func ( c * Client ) examineConflictingHeaderAgainstTrace (
ctx context . Context ,
trace [ ] * types . LightBlock ,
divergentHeader * types . SignedHeader ,
source provider . Provider , now time . Time ) ( [ ] * types . LightBlock , * types . LightBlock , error ) {
targetBlock * types . LightBlock ,
source provider . Provider , now time . Time ,
) ( [ ] * types . LightBlock , * types . LightBlock , error ) {
var (
previouslyVerifiedBlock , sourceBlock * types . LightBlock
sourceTrace [ ] * types . LightBlock
err error
)
var previouslyVerifiedBlock * types . LightBlock
if targetBlock . Height < trace [ 0 ] . Height {
return nil , nil , fmt . Errorf ( "target block has a height lower than the trusted height (%d < %d)" ,
targetBlock . Height , trace [ 0 ] . Height )
}
for idx , traceBlock := range trace {
// The first block in the trace MUST be the same to the light block that the source produces
// else we cannot continue with verification.
sourceBlock , err := source . LightBlock ( ctx , traceBlock . Height )
if err != nil {
return nil , nil , err
// this case only happens in a forward lunatic attack. We treat the block with the
// height directly after the targetBlock as the divergent block
if traceBlock . Height > targetBlock . Height {
// sanity check that the time of the traceBlock is indeed less than that of the targetBlock. If the trace
// was correctly verified we should expect monotonically increasing time. This means that if the block at
// the end of the trace has a lesser time than the target block then all blocks in the trace should have a
// lesser time
if traceBlock . Time . After ( targetBlock . Time ) {
return nil , nil ,
errors . New ( "sanity check failed: expected traceblock to have a lesser time than the target block" )
}
// before sending back the divergent block and trace we need to ensure we have verified
// the final gap between the previouslyVerifiedBlock and the targetBlock
if previouslyVerifiedBlock . Height != targetBlock . Height {
sourceTrace , err = c . verifySkipping ( ctx , source , previouslyVerifiedBlock , targetBlock , now )
if err != nil {
return nil , nil , fmt . Errorf ( "verifySkipping of conflicting header failed: %w" , err )
}
}
return sourceTrace , traceBlock , nil
}
// get the corresponding block from the source to verify and match up against the traceBlock
if traceBlock . Height == targetBlock . Height {
sourceBlock = targetBlock
} else {
sourceBlock , err = source . LightBlock ( ctx , traceBlock . Height )
if err != nil {
return nil , nil , fmt . Errorf ( "failed to examine trace: %w" , err )
}
}
// The first block in the trace MUST be the same to the light block that the source produces
// else we cannot continue with verification.
if idx == 0 {
if shash , thash := sourceBlock . Hash ( ) , traceBlock . Hash ( ) ; ! bytes . Equal ( shash , thash ) {
return nil , nil , fmt . Errorf ( "trusted block is different to the source's first block (%X = %X)" ,
@ -216,25 +339,55 @@ func (c *Client) examineConflictingHeaderAgainstTrace(
// we check that the source provider can verify a block at the same height of the
// intermediate height
trace , err : = c . verifySkipping ( ctx , source , previouslyVerifiedBlock , sourceBlock , now )
sourceTrace , err = c . verifySkipping ( ctx , source , previouslyVerifiedBlock , sourceBlock , now )
if err != nil {
return nil , nil , fmt . Errorf ( "verifySkipping of conflicting header failed: %w" , err )
}
// check if the headers verified by the source has diverged from the trace
if shash , thash := sourceBlock . Hash ( ) , traceBlock . Hash ( ) ; ! bytes . Equal ( shash , thash ) {
// Bifurcation point found!
return t race, traceBlock , nil
return sourceT race, traceBlock , nil
}
// headers are still the same. update the previouslyVerifiedBlock
previouslyVerifiedBlock = sourceBlock
}
// We have reached the end of the trace without observing a divergence. The last header is thus different
// from the divergent header that the source originally sent us, then we return an error.
return nil , nil , fmt . Errorf ( "source provided different header to the original header it provided (%X != %X)" ,
previouslyVerifiedBlock . Hash ( ) , divergentHeader . Hash ( ) )
// We have reached the end of the trace. This should never happen. This can only happen if one of the stated
// prerequisites to this function were not met. Namely that either trace[len(trace)-1].Height < targetBlock.Height
// or that trace[i].Hash() != targetBlock.Hash()
return nil , nil , errNoDivergence
}
// getTargetBlockOrLatest gets the latest height, if it is greater than the target height then it queries
// the target heght else it returns the latest. returns true if it successfully managed to acquire the target
// height.
func ( c * Client ) getTargetBlockOrLatest (
ctx context . Context ,
height int64 ,
witness provider . Provider ,
) ( bool , * types . LightBlock , error ) {
lightBlock , err := witness . LightBlock ( ctx , 0 )
if err != nil {
return false , nil , err
}
if lightBlock . Height == height {
// the witness has caught up to the height of the provider's signed header. We
// can resume with checking the hashes.
return true , lightBlock , nil
}
if lightBlock . Height > height {
// the witness has caught up. We recursively call the function again. However in order
// to avoud a wild goose chase where the witness sends us one header below and one header
// above the height we set a timeout to the context
lightBlock , err := witness . LightBlock ( ctx , height )
return true , lightBlock , err
}
return false , lightBlock , nil
}
// newLightClientAttackEvidence determines the type of attack and then forms the evidence filling out