From a4b7018732b3ee7f60a3dd06a89869a57d52ba8a Mon Sep 17 00:00:00 2001 From: Callum Waters Date: Fri, 2 Oct 2020 20:05:15 +0200 Subject: [PATCH] light: expand on errors and docs (#5443) --- docs/tendermint-core/light-client.md | 31 ++++++++++++++++++++++------ light/detector.go | 22 +++++++++++++------- light/detector_test.go | 4 ++-- light/errors.go | 7 +++++++ 4 files changed, 48 insertions(+), 16 deletions(-) diff --git a/docs/tendermint-core/light-client.md b/docs/tendermint-core/light-client.md index 41d7ae761..1b07a51e9 100644 --- a/docs/tendermint-core/light-client.md +++ b/docs/tendermint-core/light-client.md @@ -13,9 +13,10 @@ package](https://pkg.go.dev/github.com/tendermint/tendermint/light?tab=doc). ## Overview -The objective of the light client protocol is to get a commit for a recent -block hash where the commit includes a majority of signatures from the last -known validator set. From there, all the application state is verifiable with +The light client protocol verifies headers by retrieving a chain of headers, +commits and validator sets from a trusted height to the target height, verifying +the signatures of each of these intermediary signed headers till it reaches the +target height. From there, all the application state is verifiable with [merkle proofs](https://github.com/tendermint/spec/blob/953523c3cb99fdb8c8f7a2d21e3a99094279e9de/spec/blockchain/encoding.md#iavl-tree). ## Properties @@ -30,11 +31,29 @@ known validator set. From there, all the application state is verifiable with name-registry without worrying about fork censorship attacks, without posting a commit and waiting for confirmations. It's fast, secure, and free! -## Where to obtain trusted height & hash +## Security + +A light client is initialized from a point of trust using [Trust Options](https://pkg.go.dev/github.com/tendermint/tendermint/light?tab=doc#TrustOptions), +a provider and a set of witnesses. This sets the trust period: the period that +full nodes should be accountable for faulty behavior and a trust level: the +fraction of validators in a validator set with which we trust that at least one +is correct. As Tendermint consensus can withstand 1/3 byzantine faults, this is +the default trust level, however, for greater security you can increase it (max: +1). + +Similar to a full node, light clients can also be subject to byzantine attacks. +A light client also runs a detector process which cross verifies headers from a +primary with witnesses. Therefore light clients should be set with enough witnesses. -[Trust Options](https://pkg.go.dev/github.com/tendermint/tendermint/light?tab=doc#TrustOptions) +If the light client observes a faulty provider it will report it to another provider +and return an error. + +In summary, the light client is not safe when a) more than the trust level of +validators are malicious and b) all witnesses are malicious. + +## Where to obtain trusted height & hash -One way to obtain semi-trusted hash & height is to query multiple full nodes +One way to obtain a semi-trusted hash & height is to query multiple full nodes and compare their hashes: ```bash diff --git a/light/detector.go b/light/detector.go index 9e58c1bc3..4a9570e13 100644 --- a/light/detector.go +++ b/light/detector.go @@ -90,13 +90,19 @@ func (c *Client) detectDivergence(ctx context.Context, primaryTrace []*types.Lig // 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 - ev := &types.LightClientAttackEvidence{ + primaryEv := &types.LightClientAttackEvidence{ ConflictingBlock: primaryBlock, CommonHeight: commonHeight, // the first block in the bisection is common to both providers } - c.logger.Error("Attack detected. Sending evidence againt primary by witness", "ev", ev, + c.logger.Error("Attempted attack detected. Sending evidence againt primary by witness", "ev", primaryEv, "primary", c.primary, "witness", supportingWitness) - c.sendEvidence(ctx, ev, 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 @@ -110,7 +116,7 @@ func (c *Client) detectDivergence(ctx context.Context, primaryTrace []*types.Lig ) if err != nil { c.logger.Info("Error validating primary's divergent header", "primary", c.primary, "err", err) - continue + return ErrLightClientAttack } // if this is an equivocation or amnesia attack, i.e. the validator sets are the same, then we // return the height of the conflicting block else if it is a lunatic attack and the validator sets @@ -122,15 +128,15 @@ func (c *Client) detectDivergence(ctx context.Context, primaryTrace []*types.Lig } // We now use the primary trace to create evidence against the witness and send it to the primary - ev = &types.LightClientAttackEvidence{ + witnessEv := &types.LightClientAttackEvidence{ ConflictingBlock: witnessBlock, CommonHeight: commonHeight, // the first block in the bisection is common to both providers } - c.logger.Error("Sending evidence against witness by primary", "ev", ev, + c.logger.Error("Sending evidence against witness by primary", "ev", witnessEv, "primary", c.primary, "witness", supportingWitness) - c.sendEvidence(ctx, ev, c.primary) + c.sendEvidence(ctx, witnessEv, c.primary) // We return the error and don't process anymore witnesses - return e + return ErrLightClientAttack case errBadWitness: c.logger.Info("Witness returned an error during header comparison", "witness", c.witnesses[e.WitnessIndex], diff --git a/light/detector_test.go b/light/detector_test.go index 4777d0c2d..b77d29ab3 100644 --- a/light/detector_test.go +++ b/light/detector_test.go @@ -63,7 +63,7 @@ func TestLightClientAttackEvidence_Lunatic(t *testing.T) { // Check verification returns an error. _, err = c.VerifyLightBlockAtHeight(ctx, 10, bTime.Add(1*time.Hour)) if assert.Error(t, err) { - assert.Contains(t, err.Error(), "does not match primary") + assert.Equal(t, err, light.ErrLightClientAttack) } // Check evidence was sent to both full nodes. @@ -137,7 +137,7 @@ func TestLightClientAttackEvidence_Equivocation(t *testing.T) { // Check verification returns an error. _, err = c.VerifyLightBlockAtHeight(ctx, 10, bTime.Add(1*time.Hour)) if assert.Error(t, err) { - assert.Contains(t, err.Error(), "does not match primary") + assert.Equal(t, err, light.ErrLightClientAttack) } // Check evidence was sent to both full nodes. diff --git a/light/errors.go b/light/errors.go index 7c9e7b36d..c61257ac4 100644 --- a/light/errors.go +++ b/light/errors.go @@ -65,6 +65,13 @@ func (e ErrVerificationFailed) Error() string { e.From, e.To, e.Reason) } +// ErrLightClientAttack is returned when the light client has detected an attempt +// to verify a false header and has sent the evidence to either a witness or primary. +var ErrLightClientAttack = errors.New("attempted attack detected." + + " Light client received valid conflicting header from witness." + + " Unable to verify header. Evidence has been sent to both providers." + + " Check logs for full evidence and trace") + // ----------------------------- INTERNAL ERRORS --------------------------------- // ErrConflictingHeaders is thrown when two conflicting headers are discovered.