Skip to main content

soldb_debugger/
condition.rs

1//! Conditions on a breakpoint: `break TestContract.sol:30 if counter > 4`.
2//!
3//! A condition is a comparison, or several joined by `&&` and `||`, over values the
4//! debugger can actually read at a step: state variables through the storage layout
5//! (including `balances[0xabc]` and `config.limit`), the arguments of the frame being
6//! entered, and a few facts about the step itself (`pc`, `gas`, `depth`, `op`, `step`).
7//! There is no arithmetic and no calls: a debugger that evaluated Solidity would have to
8//! execute it, and this crate never executes anything.
9//!
10//! A condition that cannot be evaluated — a slot the transaction never touched, a name
11//! nothing defines, a comparison between a number and a string — does not stop, and says
12//! why through [`Evaluation::Unavailable`]. That is deliberate: stopping on an unreadable
13//! condition would be a false positive, and silently treating it as false would hide the
14//! reason it never fires.
15
16use soldb_ethdebug::{parse_word, StorageLayout, Word};
17
18use crate::state::{state_value, StorageWords};
19use crate::stepping::{Frame, StepMap};
20use crate::DebugValueStatus;
21
22/// A parsed breakpoint condition.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct Condition {
25    /// `||` groups of `&&` terms: the condition holds when any group holds.
26    groups: Vec<Vec<Comparison>>,
27    text: String,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
31struct Comparison {
32    left: Operand,
33    /// `None` for a bare operand, which holds when it is true or non-zero.
34    operator: Option<Operator>,
35    right: Option<Operand>,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39enum Operator {
40    Equal,
41    NotEqual,
42    Less,
43    LessOrEqual,
44    Greater,
45    GreaterOrEqual,
46}
47
48impl Operator {
49    fn parse(text: &str) -> Option<Self> {
50        match text {
51            "==" => Some(Self::Equal),
52            "!=" => Some(Self::NotEqual),
53            "<" => Some(Self::Less),
54            "<=" => Some(Self::LessOrEqual),
55            ">" => Some(Self::Greater),
56            ">=" => Some(Self::GreaterOrEqual),
57            _ => None,
58        }
59    }
60
61    fn holds(self, ordering: std::cmp::Ordering) -> bool {
62        use std::cmp::Ordering;
63        match self {
64            Self::Equal => ordering == Ordering::Equal,
65            Self::NotEqual => ordering != Ordering::Equal,
66            Self::Less => ordering == Ordering::Less,
67            Self::LessOrEqual => ordering != Ordering::Greater,
68            Self::Greater => ordering == Ordering::Greater,
69            Self::GreaterOrEqual => ordering != Ordering::Less,
70        }
71    }
72}
73
74/// One side of a comparison.
75#[derive(Debug, Clone, PartialEq, Eq)]
76enum Operand {
77    /// A number, address, or `0x` word written in the condition.
78    Literal(Word),
79    Bool(bool),
80    /// A quoted string, which only compares equal to another string.
81    Text(String),
82    /// A name to look up: a state variable path, a frame argument, or a step fact.
83    Name(String),
84}
85
86/// A value a condition can compare.
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub enum Value {
89    /// A 256-bit word, and whether its type is signed.
90    Word(Word, bool),
91    Bool(bool),
92    Text(String),
93}
94
95/// What a condition evaluated to at one step.
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub enum Evaluation {
98    True,
99    False,
100    /// The condition could not be evaluated here, and why.
101    Unavailable(String),
102}
103
104impl Condition {
105    /// Parses `counter > 4`, `amount == 5 && pc != 12`, or `active`.
106    pub fn parse(input: &str) -> Result<Self, String> {
107        let text = input.trim().to_owned();
108        if text.is_empty() {
109            return Err("a condition cannot be empty".to_owned());
110        }
111        let mut groups = Vec::new();
112        for group in text.split("||") {
113            let mut terms = Vec::new();
114            for term in group.split("&&") {
115                terms.push(Comparison::parse(term.trim())?);
116            }
117            groups.push(terms);
118        }
119        Ok(Self { groups, text })
120    }
121
122    /// The condition as the user wrote it.
123    #[must_use]
124    pub fn text(&self) -> &str {
125        &self.text
126    }
127
128    /// Whether the condition holds at the step `context` describes.
129    #[must_use]
130    pub fn evaluate(&self, context: &ConditionContext<'_>) -> Evaluation {
131        let mut unavailable = None;
132        for group in &self.groups {
133            let mut holds = true;
134            for comparison in group {
135                match comparison.evaluate(context) {
136                    Evaluation::True => {}
137                    Evaluation::False => {
138                        holds = false;
139                        break;
140                    }
141                    Evaluation::Unavailable(reason) => {
142                        unavailable.get_or_insert(reason);
143                        holds = false;
144                        break;
145                    }
146                }
147            }
148            if holds {
149                return Evaluation::True;
150            }
151        }
152        match unavailable {
153            Some(reason) => Evaluation::Unavailable(reason),
154            None => Evaluation::False,
155        }
156    }
157}
158
159impl Comparison {
160    fn parse(input: &str) -> Result<Self, String> {
161        if input.is_empty() {
162            return Err("a condition cannot have an empty term".to_owned());
163        }
164        // The two-character operators first, so `<=` does not parse as `<`.
165        for symbol in ["==", "!=", "<=", ">=", "<", ">"] {
166            let Some(index) = find_operator(input, symbol) else {
167                continue;
168            };
169            let (left, rest) = input.split_at(index);
170            let right = &rest[symbol.len()..];
171            let operator = Operator::parse(symbol).expect("listed operator");
172            return Ok(Self {
173                left: Operand::parse(left.trim())?,
174                operator: Some(operator),
175                right: Some(Operand::parse(right.trim())?),
176            });
177        }
178        Ok(Self {
179            left: Operand::parse(input)?,
180            operator: None,
181            right: None,
182        })
183    }
184
185    fn evaluate(&self, context: &ConditionContext<'_>) -> Evaluation {
186        let left = match context.resolve(&self.left) {
187            Ok(value) => value,
188            Err(reason) => return Evaluation::Unavailable(reason),
189        };
190        let (Some(operator), Some(right)) = (self.operator, self.right.as_ref()) else {
191            // A bare operand: true, or a non-zero word.
192            return match left {
193                Value::Bool(value) => Evaluation::from(value),
194                Value::Word(word, _) => Evaluation::from(word != [0_u8; 32]),
195                Value::Text(text) => Evaluation::from(!text.is_empty()),
196            };
197        };
198        let right = match context.resolve(right) {
199            Ok(value) => value,
200            Err(reason) => return Evaluation::Unavailable(reason),
201        };
202        match compare(&left, &right) {
203            Ok(ordering) => Evaluation::from(operator.holds(ordering)),
204            Err(reason) => Evaluation::Unavailable(reason),
205        }
206    }
207}
208
209impl From<bool> for Evaluation {
210    fn from(value: bool) -> Self {
211        if value {
212            Self::True
213        } else {
214            Self::False
215        }
216    }
217}
218
219/// Compares two values, or says why they cannot be compared.
220fn compare(left: &Value, right: &Value) -> Result<std::cmp::Ordering, String> {
221    match (left, right) {
222        (Value::Bool(left), Value::Bool(right)) => Ok(left.cmp(right)),
223        (Value::Text(left), Value::Text(right)) => Ok(left.cmp(right)),
224        (Value::Word(left, left_signed), Value::Word(right, right_signed)) => {
225            if *left_signed || *right_signed {
226                return Ok(signed_value(left).cmp(&signed_value(right)));
227            }
228            Ok(left.cmp(right))
229        }
230        // A bool compares with 0 and 1, which is how a user writes `active == 1`.
231        (Value::Bool(left), Value::Word(right, _)) => Ok(word_of_bool(*left).cmp(right)),
232        (Value::Word(left, _), Value::Bool(right)) => Ok(left.cmp(&word_of_bool(*right))),
233        _ => Err("a number and a string cannot be compared".to_owned()),
234    }
235}
236
237/// A word as a signed 256-bit number, for ordering: the sign bit inverts the order of the
238/// two halves, so a negative value sorts below every non-negative one.
239fn signed_value(word: &Word) -> (bool, Word) {
240    let negative = word[0] & 0x80 != 0;
241    // Negatives compare among themselves in the same direction as their two's complement
242    // bit patterns, so only the halves need separating.
243    (!negative, *word)
244}
245
246fn word_of_bool(value: bool) -> Word {
247    let mut word = [0_u8; 32];
248    word[31] = u8::from(value);
249    word
250}
251
252/// Finds `symbol` outside a `[...]` index or a quoted string, so `balances[0x1] == 2`
253/// splits at the right place.
254fn find_operator(input: &str, symbol: &str) -> Option<usize> {
255    let bytes = input.as_bytes();
256    let mut depth = 0_i32;
257    let mut quoted = false;
258    for index in 0..bytes.len() {
259        match bytes[index] {
260            b'"' => quoted = !quoted,
261            b'[' if !quoted => depth += 1,
262            b']' if !quoted => depth -= 1,
263            _ => {}
264        }
265        if quoted || depth != 0 {
266            continue;
267        }
268        if input[index..].starts_with(symbol) {
269            // `<` must not match the `<` of `<=`, which the caller tries first, and `=`
270            // of `==` must not split `!=`.
271            return Some(index);
272        }
273    }
274    None
275}
276
277impl Operand {
278    fn parse(input: &str) -> Result<Self, String> {
279        let input = input.trim();
280        if input.is_empty() {
281            return Err("a comparison is missing one side".to_owned());
282        }
283        if let Some(text) = input
284            .strip_prefix('"')
285            .and_then(|rest| rest.strip_suffix('"'))
286        {
287            return Ok(Self::Text(text.to_owned()));
288        }
289        match input {
290            "true" => return Ok(Self::Bool(true)),
291            "false" => return Ok(Self::Bool(false)),
292            _ => {}
293        }
294        if input.starts_with("0x") || input.bytes().all(|byte| byte.is_ascii_digit()) {
295            return parse_word(input)
296                .map(Self::Literal)
297                .map_err(|error| error.to_string());
298        }
299        Ok(Self::Name(input.to_owned()))
300    }
301}
302
303/// Everything a condition can read at one step.
304pub struct ConditionContext<'a> {
305    map: &'a StepMap,
306    step: usize,
307    words: Option<StorageWords<'a>>,
308    op: &'a str,
309    pc: u64,
310    gas: u64,
311    depth: u64,
312    /// The frame being entered or executing, for reading its arguments by name.
313    frame: Option<&'a Frame>,
314}
315
316impl<'a> ConditionContext<'a> {
317    /// The context at `step`, given the trace step's own facts.
318    #[must_use]
319    pub fn new(
320        map: &'a StepMap,
321        step: usize,
322        trace_step: &'a soldb_core::TraceStep,
323        words: Option<StorageWords<'a>>,
324    ) -> Self {
325        Self {
326            map,
327            step,
328            words,
329            op: &trace_step.op,
330            pc: trace_step.pc,
331            gas: trace_step.gas,
332            depth: trace_step.depth,
333            frame: None,
334        }
335    }
336
337    /// Reads the arguments of this frame by name, when the trace proved them.
338    #[must_use]
339    pub fn with_frame(mut self, frame: Option<&'a Frame>) -> Self {
340        self.frame = frame;
341        self
342    }
343
344    fn resolve(&self, operand: &Operand) -> Result<Value, String> {
345        match operand {
346            Operand::Literal(word) => Ok(Value::Word(*word, false)),
347            Operand::Bool(value) => Ok(Value::Bool(*value)),
348            Operand::Text(text) => Ok(Value::Text(text.clone())),
349            Operand::Name(name) => self.value(name),
350        }
351    }
352
353    /// The value of a name: a fact about the step, an argument of the frame, or a state
354    /// variable read through the storage layout.
355    fn value(&self, name: &str) -> Result<Value, String> {
356        match name {
357            "pc" => return Ok(Value::Word(word_of_u64(self.pc), false)),
358            "gas" => return Ok(Value::Word(word_of_u64(self.gas), false)),
359            "depth" => return Ok(Value::Word(word_of_u64(self.depth), false)),
360            "step" => {
361                return Ok(Value::Word(word_of_u64(self.step as u64), false));
362            }
363            "op" => return Ok(Value::Text(self.op.to_owned())),
364            _ => {}
365        }
366        if let Some(argument) = self.frame.and_then(|frame| {
367            frame
368                .arguments
369                .iter()
370                .find(|argument| argument.name == name)
371        }) {
372            let word = parse_word(argument.value.raw.as_deref().unwrap_or("0x0"))
373                .map_err(|error| error.to_string())?;
374            return Ok(Value::Word(word, argument.ty.starts_with("int")));
375        }
376        let Some(layout) = self.layout() else {
377            return Err(format!(
378                "`{name}` is not a step value or an argument here, and no storage layout is loaded to look it up as a state variable"
379            ));
380        };
381        let Some(words) = self.words.as_ref() else {
382            return Err(format!(
383                "no storage was recorded, so `{name}` cannot be read"
384            ));
385        };
386        let variable = state_value(layout, words, name)?;
387        if variable.value.status == DebugValueStatus::Unavailable {
388            return Err(format!("`{name}` is {}", variable.value.display));
389        }
390        let Some(raw) = variable.value.raw.as_deref() else {
391            return Err(format!(
392                "`{name}` is a {}, which a condition cannot compare",
393                variable.ty
394            ));
395        };
396        let word = parse_word(raw).map_err(|error| error.to_string())?;
397        Ok(Value::Word(word, variable.ty.starts_with("int")))
398    }
399
400    fn layout(&self) -> Option<&StorageLayout> {
401        self.map
402            .contract_at_step(self.step)?
403            .storage_layout
404            .as_ref()
405    }
406}
407
408fn word_of_u64(value: u64) -> Word {
409    let mut word = [0_u8; 32];
410    word[24..].copy_from_slice(&value.to_be_bytes());
411    word
412}
413
414#[cfg(test)]
415mod tests {
416    use super::{Comparison, Condition, Evaluation, Operand, Operator, Value};
417
418    fn parse(input: &str) -> Condition {
419        Condition::parse(input).expect(input)
420    }
421
422    #[test]
423    fn parses_comparisons_and_groups() {
424        let condition = parse("counter > 4");
425        assert_eq!(condition.text(), "counter > 4");
426        assert_eq!(condition.groups.len(), 1);
427        assert_eq!(
428            condition.groups[0][0],
429            Comparison {
430                left: Operand::Name("counter".to_owned()),
431                operator: Some(Operator::Greater),
432                right: Some(Operand::Literal(super::word_of_u64(4))),
433            }
434        );
435
436        // `<=` and `>=` are not read as `<` and `>`.
437        let condition = parse("gas <= 100 && depth >= 1");
438        assert_eq!(condition.groups[0].len(), 2);
439        assert_eq!(condition.groups[0][0].operator, Some(Operator::LessOrEqual));
440        assert_eq!(
441            condition.groups[0][1].operator,
442            Some(Operator::GreaterOrEqual)
443        );
444
445        // An index is not split at its contents.
446        let condition = parse("balances[0x00000000000000000000000000000000000000aa] == 25");
447        assert_eq!(
448            condition.groups[0][0].left,
449            Operand::Name("balances[0x00000000000000000000000000000000000000aa]".to_owned())
450        );
451
452        let condition = parse("active || counter == 0");
453        assert_eq!(condition.groups.len(), 2);
454        assert_eq!(condition.groups[0][0].operator, None);
455
456        assert!(Condition::parse("").is_err());
457        assert!(Condition::parse("counter >").is_err());
458        assert!(Condition::parse("a && ").is_err());
459    }
460
461    #[test]
462    fn compares_words_bools_and_text() {
463        let one = super::word_of_u64(1);
464        let two = super::word_of_u64(2);
465        assert_eq!(
466            super::compare(&Value::Word(one, false), &Value::Word(two, false)),
467            Ok(std::cmp::Ordering::Less)
468        );
469        // Signed values order by their sign first: -1 is below 1.
470        let minus_one = [0xff_u8; 32];
471        assert_eq!(
472            super::compare(&Value::Word(minus_one, true), &Value::Word(one, true)),
473            Ok(std::cmp::Ordering::Less)
474        );
475        // Unsigned, the same word is the largest there is.
476        assert_eq!(
477            super::compare(&Value::Word(minus_one, false), &Value::Word(one, false)),
478            Ok(std::cmp::Ordering::Greater)
479        );
480        assert_eq!(
481            super::compare(&Value::Bool(true), &Value::Word(one, false)),
482            Ok(std::cmp::Ordering::Equal)
483        );
484        assert_eq!(
485            super::compare(
486                &Value::Text("SSTORE".to_owned()),
487                &Value::Text("SSTORE".to_owned())
488            ),
489            Ok(std::cmp::Ordering::Equal)
490        );
491        assert!(super::compare(&Value::Text("a".to_owned()), &Value::Bool(true)).is_err());
492    }
493
494    #[test]
495    fn evaluation_of_groups_short_circuits_and_reports_the_first_reason() {
496        let unavailable = Evaluation::Unavailable("no".to_owned());
497        assert_ne!(unavailable, Evaluation::False);
498        assert_eq!(Evaluation::from(true), Evaluation::True);
499        assert_eq!(Evaluation::from(false), Evaluation::False);
500    }
501}