1use soldb_core::TransactionTrace;
19use soldb_ethdebug::{parse_word, StorageLayout, Word};
20
21use crate::state::{state_value, StorageWords};
22use crate::stepping::{ContractDebugInfo, Frame, StepMap};
23use crate::DebugValueStatus;
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct Condition {
28 groups: Vec<Vec<Comparison>>,
30 text: String,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34struct Comparison {
35 left: Operand,
36 operator: Option<Operator>,
38 right: Option<Operand>,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42enum Operator {
43 Equal,
44 NotEqual,
45 Less,
46 LessOrEqual,
47 Greater,
48 GreaterOrEqual,
49}
50
51impl Operator {
52 fn parse(text: &str) -> Option<Self> {
53 match text {
54 "==" => Some(Self::Equal),
55 "!=" => Some(Self::NotEqual),
56 "<" => Some(Self::Less),
57 "<=" => Some(Self::LessOrEqual),
58 ">" => Some(Self::Greater),
59 ">=" => Some(Self::GreaterOrEqual),
60 _ => None,
61 }
62 }
63
64 fn holds(self, ordering: std::cmp::Ordering) -> bool {
65 use std::cmp::Ordering;
66 match self {
67 Self::Equal => ordering == Ordering::Equal,
68 Self::NotEqual => ordering != Ordering::Equal,
69 Self::Less => ordering == Ordering::Less,
70 Self::LessOrEqual => ordering != Ordering::Greater,
71 Self::Greater => ordering == Ordering::Greater,
72 Self::GreaterOrEqual => ordering != Ordering::Less,
73 }
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
79enum Operand {
80 Literal(Word),
82 Bool(bool),
83 Text(String),
85 Name(String),
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
92pub enum Value {
93 Word(Word, bool),
95 Bool(bool),
96 Text(String),
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
101pub enum Evaluation {
102 True,
103 False,
104 Unavailable(String),
106}
107
108impl Condition {
109 pub fn parse(input: &str) -> Result<Self, String> {
111 let text = input.trim().to_owned();
112 if text.is_empty() {
113 return Err("a condition cannot be empty".to_owned());
114 }
115 let mut groups = Vec::new();
116 for group in text.split("||") {
117 let mut terms = Vec::new();
118 for term in group.split("&&") {
119 terms.push(Comparison::parse(term.trim())?);
120 }
121 groups.push(terms);
122 }
123 Ok(Self { groups, text })
124 }
125
126 #[must_use]
128 pub fn text(&self) -> &str {
129 &self.text
130 }
131
132 #[must_use]
134 pub fn evaluate(&self, context: &ConditionContext<'_>) -> Evaluation {
135 let mut unavailable = None;
136 for group in &self.groups {
137 let mut holds = true;
138 for comparison in group {
139 match comparison.evaluate(context) {
140 Evaluation::True => {}
141 Evaluation::False => {
142 holds = false;
143 break;
144 }
145 Evaluation::Unavailable(reason) => {
146 unavailable.get_or_insert(reason);
147 holds = false;
148 break;
149 }
150 }
151 }
152 if holds {
153 return Evaluation::True;
154 }
155 }
156 match unavailable {
157 Some(reason) => Evaluation::Unavailable(reason),
158 None => Evaluation::False,
159 }
160 }
161}
162
163impl Comparison {
164 fn parse(input: &str) -> Result<Self, String> {
165 if input.is_empty() {
166 return Err("a condition cannot have an empty term".to_owned());
167 }
168 for symbol in ["==", "!=", "<=", ">=", "<", ">"] {
170 let Some(index) = find_operator(input, symbol) else {
171 continue;
172 };
173 let (left, rest) = input.split_at(index);
174 let right = &rest[symbol.len()..];
175 let operator = Operator::parse(symbol).expect("listed operator");
176 return Ok(Self {
177 left: Operand::parse(left.trim())?,
178 operator: Some(operator),
179 right: Some(Operand::parse(right.trim())?),
180 });
181 }
182 Ok(Self {
183 left: Operand::parse(input)?,
184 operator: None,
185 right: None,
186 })
187 }
188
189 fn evaluate(&self, context: &ConditionContext<'_>) -> Evaluation {
190 let left = match context.resolve(&self.left) {
191 Ok(value) => value,
192 Err(reason) => return Evaluation::Unavailable(reason),
193 };
194 let (Some(operator), Some(right)) = (self.operator, self.right.as_ref()) else {
195 return match left {
197 Value::Bool(value) => Evaluation::from(value),
198 Value::Word(word, _) => Evaluation::from(word != [0_u8; 32]),
199 Value::Text(text) => Evaluation::from(!text.is_empty()),
200 };
201 };
202 let right = match context.resolve(right) {
203 Ok(value) => value,
204 Err(reason) => return Evaluation::Unavailable(reason),
205 };
206 match compare(&left, &right) {
207 Ok(ordering) => Evaluation::from(operator.holds(ordering)),
208 Err(reason) => Evaluation::Unavailable(reason),
209 }
210 }
211}
212
213impl From<bool> for Evaluation {
214 fn from(value: bool) -> Self {
215 if value {
216 Self::True
217 } else {
218 Self::False
219 }
220 }
221}
222
223fn compare(left: &Value, right: &Value) -> Result<std::cmp::Ordering, String> {
225 match (left, right) {
226 (Value::Bool(left), Value::Bool(right)) => Ok(left.cmp(right)),
227 (Value::Text(left), Value::Text(right)) => Ok(left.cmp(right)),
228 (Value::Word(left, left_signed), Value::Word(right, right_signed)) => {
229 if *left_signed || *right_signed {
230 return Ok(signed_value(left).cmp(&signed_value(right)));
231 }
232 Ok(left.cmp(right))
233 }
234 (Value::Bool(left), Value::Word(right, _)) => Ok(word_of_bool(*left).cmp(right)),
236 (Value::Word(left, _), Value::Bool(right)) => Ok(left.cmp(&word_of_bool(*right))),
237 _ => Err("a number and a string cannot be compared".to_owned()),
238 }
239}
240
241fn signed_value(word: &Word) -> (bool, Word) {
244 let negative = word[0] & 0x80 != 0;
245 (!negative, *word)
248}
249
250fn word_of_bool(value: bool) -> Word {
251 let mut word = [0_u8; 32];
252 word[31] = u8::from(value);
253 word
254}
255
256fn find_operator(input: &str, symbol: &str) -> Option<usize> {
259 let bytes = input.as_bytes();
260 let mut depth = 0_i32;
261 let mut quoted = false;
262 for index in 0..bytes.len() {
263 match bytes[index] {
264 b'"' => quoted = !quoted,
265 b'[' if !quoted => depth += 1,
266 b']' if !quoted => depth -= 1,
267 _ => {}
268 }
269 if quoted || depth != 0 {
270 continue;
271 }
272 if input[index..].starts_with(symbol) {
273 return Some(index);
276 }
277 }
278 None
279}
280
281impl Operand {
282 fn parse(input: &str) -> Result<Self, String> {
283 let input = input.trim();
284 if input.is_empty() {
285 return Err("a comparison is missing one side".to_owned());
286 }
287 if let Some(text) = input
288 .strip_prefix('"')
289 .and_then(|rest| rest.strip_suffix('"'))
290 {
291 return Ok(Self::Text(text.to_owned()));
292 }
293 match input {
294 "true" => return Ok(Self::Bool(true)),
295 "false" => return Ok(Self::Bool(false)),
296 _ => {}
297 }
298 if input.starts_with("0x") || input.bytes().all(|byte| byte.is_ascii_digit()) {
299 return parse_word(input)
300 .map(Self::Literal)
301 .map_err(|error| error.to_string());
302 }
303 Ok(Self::Name(input.to_owned()))
304 }
305}
306
307pub struct ConditionContext<'a> {
309 map: &'a StepMap,
310 step: usize,
311 words: Option<StorageWords<'a>>,
312 op: &'a str,
313 pc: u64,
314 gas: u64,
315 depth: u64,
316 trace: Option<&'a TransactionTrace>,
318 frame: Option<&'a Frame>,
320}
321
322impl<'a> ConditionContext<'a> {
323 #[must_use]
325 pub fn new(
326 map: &'a StepMap,
327 step: usize,
328 trace_step: soldb_core::StepView<'a>,
329 words: Option<StorageWords<'a>>,
330 ) -> Self {
331 Self {
332 map,
333 step,
334 words,
335 op: trace_step.mnemonic(),
336 pc: trace_step.pc(),
337 gas: trace_step.gas(),
338 depth: trace_step.depth(),
339 trace: None,
340 frame: None,
341 }
342 }
343
344 #[must_use]
347 pub fn with_trace(mut self, trace: &'a TransactionTrace) -> Self {
348 self.trace = Some(trace);
349 self
350 }
351
352 #[must_use]
354 pub fn with_frame(mut self, frame: Option<&'a Frame>) -> Self {
355 self.frame = frame;
356 self
357 }
358
359 fn resolve(&self, operand: &Operand) -> Result<Value, String> {
360 match operand {
361 Operand::Literal(word) => Ok(Value::Word(*word, false)),
362 Operand::Bool(value) => Ok(Value::Bool(*value)),
363 Operand::Text(text) => Ok(Value::Text(text.clone())),
364 Operand::Name(name) => self.value(name),
365 }
366 }
367
368 fn value(&self, name: &str) -> Result<Value, String> {
371 match name {
372 "pc" => return Ok(Value::Word(word_of_u64(self.pc), false)),
373 "gas" => return Ok(Value::Word(word_of_u64(self.gas), false)),
374 "depth" => return Ok(Value::Word(word_of_u64(self.depth), false)),
375 "step" => {
376 return Ok(Value::Word(word_of_u64(self.step as u64), false));
377 }
378 "op" => return Ok(Value::Text(self.op.to_owned())),
379 _ => {}
380 }
381 if let Some(local) = self.local(name) {
382 return local;
383 }
384 if let Some(argument) = self.frame.and_then(|frame| {
385 frame
386 .arguments
387 .iter()
388 .find(|argument| argument.name == name)
389 }) {
390 let word = parse_word(argument.value.raw.as_deref().unwrap_or("0x0"))
391 .map_err(|error| error.to_string())?;
392 return Ok(Value::Word(word, argument.ty.starts_with("int")));
393 }
394 if let Some(index) = self.contract().and_then(|contract| {
395 contract
396 .types
397 .enum_literal(self.map.scope_at_step(self.step), name)
398 }) {
399 return Ok(Value::Word(word_of_u64(index), false));
400 }
401 let Some(layout) = self.layout() else {
402 return Err(format!(
403 "`{name}` is not a step value, a local, or an argument here, and no storage layout is loaded to look it up as a state variable"
404 ));
405 };
406 let Some(words) = self.words.as_ref() else {
407 return Err(format!(
408 "no storage was recorded, so `{name}` cannot be read"
409 ));
410 };
411 let variable = state_value(layout, words, name)?;
412 if variable.value.status == DebugValueStatus::Unavailable {
413 return Err(format!("`{name}` is {}", variable.value.display));
414 }
415 let Some(raw) = variable.value.raw.as_deref() else {
416 return Err(format!(
417 "`{name}` is a {}, which a condition cannot compare",
418 variable.ty
419 ));
420 };
421 let word = parse_word(raw).map_err(|error| error.to_string())?;
422 Ok(Value::Word(word, variable.ty.starts_with("int")))
423 }
424
425 fn local(&self, name: &str) -> Option<Result<Value, String>> {
431 let trace = self.trace?;
432 self.map
433 .local_condition_value(trace, self.step, self.words.as_ref(), name)
434 }
435
436 fn contract(&self) -> Option<&ContractDebugInfo> {
437 self.map.contract_at_step(self.step)
438 }
439
440 fn layout(&self) -> Option<&StorageLayout> {
441 self.contract()?.storage_layout.as_ref()
442 }
443}
444
445fn word_of_u64(value: u64) -> Word {
446 let mut word = [0_u8; 32];
447 word[24..].copy_from_slice(&value.to_be_bytes());
448 word
449}
450
451#[cfg(test)]
452mod tests {
453 use super::{Comparison, Condition, Evaluation, Operand, Operator, Value};
454
455 fn parse(input: &str) -> Condition {
456 Condition::parse(input).expect(input)
457 }
458
459 #[test]
460 fn parses_comparisons_and_groups() {
461 let condition = parse("counter > 4");
462 assert_eq!(condition.text(), "counter > 4");
463 assert_eq!(condition.groups.len(), 1);
464 assert_eq!(
465 condition.groups[0][0],
466 Comparison {
467 left: Operand::Name("counter".to_owned()),
468 operator: Some(Operator::Greater),
469 right: Some(Operand::Literal(super::word_of_u64(4))),
470 }
471 );
472
473 let condition = parse("gas <= 100 && depth >= 1");
475 assert_eq!(condition.groups[0].len(), 2);
476 assert_eq!(condition.groups[0][0].operator, Some(Operator::LessOrEqual));
477 assert_eq!(
478 condition.groups[0][1].operator,
479 Some(Operator::GreaterOrEqual)
480 );
481
482 let condition = parse("balances[0x00000000000000000000000000000000000000aa] == 25");
484 assert_eq!(
485 condition.groups[0][0].left,
486 Operand::Name("balances[0x00000000000000000000000000000000000000aa]".to_owned())
487 );
488
489 let condition = parse("active || counter == 0");
490 assert_eq!(condition.groups.len(), 2);
491 assert_eq!(condition.groups[0][0].operator, None);
492
493 assert!(Condition::parse("").is_err());
494 assert!(Condition::parse("counter >").is_err());
495 assert!(Condition::parse("a && ").is_err());
496 }
497
498 #[test]
499 fn compares_words_bools_and_text() {
500 let one = super::word_of_u64(1);
501 let two = super::word_of_u64(2);
502 assert_eq!(
503 super::compare(&Value::Word(one, false), &Value::Word(two, false)),
504 Ok(std::cmp::Ordering::Less)
505 );
506 let minus_one = [0xff_u8; 32];
508 assert_eq!(
509 super::compare(&Value::Word(minus_one, true), &Value::Word(one, true)),
510 Ok(std::cmp::Ordering::Less)
511 );
512 assert_eq!(
514 super::compare(&Value::Word(minus_one, false), &Value::Word(one, false)),
515 Ok(std::cmp::Ordering::Greater)
516 );
517 assert_eq!(
518 super::compare(&Value::Bool(true), &Value::Word(one, false)),
519 Ok(std::cmp::Ordering::Equal)
520 );
521 assert_eq!(
522 super::compare(
523 &Value::Text("SSTORE".to_owned()),
524 &Value::Text("SSTORE".to_owned())
525 ),
526 Ok(std::cmp::Ordering::Equal)
527 );
528 assert!(super::compare(&Value::Text("a".to_owned()), &Value::Bool(true)).is_err());
529 }
530
531 #[test]
532 fn evaluation_of_groups_short_circuits_and_reports_the_first_reason() {
533 let unavailable = Evaluation::Unavailable("no".to_owned());
534 assert_ne!(unavailable, Evaluation::False);
535 assert_eq!(Evaluation::from(true), Evaluation::True);
536 assert_eq!(Evaluation::from(false), Evaluation::False);
537 }
538}