1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
use std::{thread, time::Duration};

use tracing::{debug, warn};

use tendermint::{block::signed_header::SignedHeader, crypto::Sha256, merkle::MerkleHash};
use tendermint_light_client::light_client::TargetOrLatest;
use tendermint_light_client::verifier::errors::ErrorExt;
use tendermint_light_client::verifier::types::LightBlock;

use crate::conflict::GatheredEvidence;

use super::{
    error::Error, gather_evidence_from_conflicting_headers, provider::Provider, trace::Trace,
};

/// A divergence between the primary and a witness that has been detected in [`detect_divergence`].
#[derive(Clone, Debug)]
pub struct Divergence {
    /// The evidence of a misbehaviour that has been gathered from the conflicting headers
    pub evidence: GatheredEvidence,
    /// The conflicting light block that was returned by the witness
    pub challenging_block: LightBlock,
}

/// Given a primary trace and a witness, detect any divergence between the two,
/// by querying the witness for the same header as the last header in the primary trace
/// (ie. the target block), and comparing the hashes.
///
/// If the hashes match, then no divergence has been detected and the target block can be trusted.
///
/// If the hashes do not match, then the witness has provided a conflicting header.
/// This could possibly imply an attack on the light client.
/// In this case, we need to verify the witness's header using the same skipping verification
/// and then we need to find the point that the headers diverge and examine this for any evidence of
/// an attack. We then attempt to find the bifurcation point and if successful construct the
/// evidence of an attack to report to the witness.
pub async fn detect_divergence<H>(
    primary: Option<&Provider>,
    witness: &mut Provider,
    primary_trace: Vec<LightBlock>,
    max_clock_drift: Duration,
    max_block_lag: Duration,
) -> Result<Option<Divergence>, Error>
where
    H: Sha256 + MerkleHash + Default,
{
    let primary_trace = Trace::new(primary_trace)?;

    let last_verified_block = primary_trace.last();
    let last_verified_header = &last_verified_block.signed_header;

    debug!(
        end_block_height = %last_verified_header.header.height,
        end_block_hash = %last_verified_header.header.hash(),
        length = primary_trace.len(),
        "Running detector against primary trace"
    );

    let result = compare_new_header_with_witness(
        last_verified_header,
        witness,
        max_clock_drift,
        max_block_lag,
    );

    match result {
        // No divergence found
        Ok(()) => Ok(None),

        // We have conflicting headers. This could possibly imply an attack on the light client.
        // First we need to verify the witness's header using the same skipping verification and then we
        // 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 valid evidence.
        Err(CompareError::ConflictingHeaders(challenging_block)) => {
            warn!(
                witness = %witness.peer_id(),
                height  = %challenging_block.height(),
                "Found conflicting headers between primary and witness"
            );

            // Gather the evidence to report from the conflicting headers
            let evidence = gather_evidence_from_conflicting_headers::<H>(
                primary,
                witness,
                &primary_trace,
                &challenging_block,
            )
            .await?;

            Ok(Some(Divergence {
                evidence,
                challenging_block: *challenging_block,
            }))
        },

        Err(CompareError::BadWitness) => {
            // These are all melevolent errors and should result in removing the witness
            debug!(witness = %witness.peer_id(), "witness returned an error during header comparison, removing...");

            Err(Error::bad_witness())
        },

        Err(CompareError::Other(e)) => {
            // Benign errors which can be ignored
            debug!(witness = %witness.peer_id(), "error in light block request to witness: {e}");

            Err(Error::light_client(e))
        },
    }
}

/// An error that arised when comparing a header from the primary with a header from a witness
/// with [`compare_new_header_with_witness`].
#[derive(Debug)]
pub enum CompareError {
    /// There may have been an attack on this light client
    ConflictingHeaders(Box<LightBlock>),
    /// The witness has either not responded, doesn't have the header or has given us an invalid one
    BadWitness,
    /// Some other error has occurred, this is likely a benign error
    Other(tendermint_light_client::errors::Error),
}

/// Takes the verified header from the primary and compares it with a
/// header from a specified witness. The function can return one of three errors:
///
/// 1: `CompareError::ConflictingHeaders`: there may have been an attack on this light client
/// 2: `CompareError::BadWitness`: the witness has either not responded, doesn't have the header or has given us an invalid one
/// 3: `CompareError::Other`: some other error has occurred, this is likely a benign error
///
/// Note: In the case of an invalid header we remove the witness
///
/// 3: nil -> the hashes of the two headers match
pub fn compare_new_header_with_witness(
    new_header: &SignedHeader,
    witness: &mut Provider,
    max_clock_drift: Duration,
    max_block_lag: Duration,
) -> Result<(), CompareError> {
    let light_block = check_against_witness(new_header, witness, max_clock_drift, max_block_lag)?;

    if light_block.signed_header.header.hash() != new_header.header.hash() {
        return Err(CompareError::ConflictingHeaders(Box::new(light_block)));
    }

    Ok(())
}

fn check_against_witness(
    sh: &SignedHeader,
    witness: &mut Provider,
    max_clock_drift: Duration,
    max_block_lag: Duration,
) -> Result<LightBlock, CompareError> {
    let _span =
        tracing::debug_span!("check_against_witness", witness = %witness.peer_id()).entered();

    let light_block = witness.fetch_light_block(sh.header.height);

    match light_block {
        // No error means we move on to checking the hash of the two headers
        Ok(lb) => Ok(lb),

        // The witness hasn't been helpful in comparing headers, we mark the response and continue
        // comparing with the rest of the witnesses
        Err(e) if e.detail().is_io() => {
            debug!("The witness hasn't been helpful in comparing headers");

            Err(CompareError::BadWitness)
        },

        // 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
        Err(e) if e.detail().is_height_too_high() => {
            debug!("The witness' head of the blockchain is lower than the height of the primary");

            let light_block = witness
                .get_target_block_or_latest(sh.header.height)
                .map_err(|_| CompareError::BadWitness)?;

            let light_block = match light_block {
                // 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
                TargetOrLatest::Target(light_block) => return Ok(light_block),

                // Otherwise we continue with the checks
                TargetOrLatest::Latest(light_block) => light_block,
            };

            // The witness' last header is below the primary's header.
            // We check the times to see if the blocks have conflicting times
            debug!("The witness' last header is below the primary's header");

            if !light_block.time().before(sh.header.time) {
                return Err(CompareError::ConflictingHeaders(Box::new(light_block)));
            }

            // 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
            let wait_time = 2 * max_clock_drift + max_block_lag;
            debug!("The witness is behind. We wait for {wait_time:?}");

            thread::sleep(wait_time);

            let light_block = witness
                .get_target_block_or_latest(sh.header.height)
                .map_err(|_| CompareError::BadWitness)?;

            let light_block = match light_block {
                // If the witness caught up and has returned a block of the target height then we can
                // return and continue to verify the hashes
                TargetOrLatest::Target(light_block) => return Ok(light_block),

                // Otherwise we continue with the checks
                TargetOrLatest::Latest(light_block) => light_block,
            };

            // The witness still doesn't have a block at the height of the primary.
            // Check if there is a conflicting time
            if !light_block.time().before(sh.header.time) {
                return Err(CompareError::ConflictingHeaders(Box::new(light_block)));
            }

            // 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 an 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
            Err(CompareError::BadWitness)
        },

        Err(other) => Err(CompareError::Other(other)),
    }
}