Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions crates/cli/src/output/human.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@


use prism_core::types::report::DiagnosticReport;

use crate::output::renderers::{render_section_header, render_error_card, render_fix_list, BudgetBar};
use crate::output::renderers::{
render_cause_list, render_error_card, render_fix_list, render_section_header, BudgetBar,
};

pub fn print_report(report: &DiagnosticReport) -> anyhow::Result<()> {
println!("{}", render_error_card(report));
Expand Down Expand Up @@ -38,6 +38,11 @@ pub fn print_report(report: &DiagnosticReport) -> anyhow::Result<()> {
);
}

if !report.root_causes.is_empty() {
println!();
println!("{}", render_cause_list(&report.root_causes));
}

if !report.suggested_fixes.is_empty() {
println!();
println!("{}", render_fix_list(&report.suggested_fixes));
Expand Down
89 changes: 69 additions & 20 deletions crates/cli/src/output/renderers.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@


#![allow(dead_code)]

use crate::output::theme::ColorPalette;
use colored::Colorize;
use prism_core::types::report::{DiagnosticReport, TransactionContext};
use prism_core::types::report::{DiagnosticReport, RootCause, TransactionContext};
use prism_core::types::trace::ResourceProfile;
use tabled::{Table, Tabled};
use crate::output::theme::ColorPalette;

const BAR_WIDTH: usize = 10;
const HEAT_BLOCKS: [&str; 4] = ["░", "▒", "▓", "█"];
Expand All @@ -23,6 +21,10 @@ pub fn render_fix_list(fixes: &[prism_core::types::report::SuggestedFix]) -> Str
FixList::new(fixes).render()
}

pub fn render_cause_list(causes: &[RootCause]) -> String {
CauseList::new(causes).render()
}

pub fn render_state_diff_table(diff: &prism_core::types::trace::StateDiff) -> String {
StateDiffTable::new(diff).render()
}
Expand Down Expand Up @@ -64,12 +66,13 @@ impl<'a> ErrorCard<'a> {
let mut output = String::new();

let category_badge = format!("[{}]", self.report.error_category.to_uppercase());
let error_line = format!(
" {} ({})",
self.report.error_name, self.report.error_code
);
let error_line = format!(" {} ({})", self.report.error_name, self.report.error_code);

let max_width = error_line.len().max(self.report.summary.len()).max(category_badge.len()) + 4;
let max_width = error_line
.len()
.max(self.report.summary.len())
.max(category_badge.len())
+ 4;
let border = "█".repeat(max_width);

let border_colored = border.red().bold().to_string();
Expand All @@ -83,7 +86,11 @@ impl<'a> ErrorCard<'a> {

if let Some(contract_error) = &self.report.contract_error {
let component_line = format!("Component: {}", contract_error.contract_id);
output.push_str(&format!("{} {}\n", "█".red().bold(), component_line.white()));
output.push_str(&format!(
"{} {}\n",
"█".red().bold(),
component_line.white()
));
}

output.push_str(&format!("{} {}\n", "█".red().bold(), summary_colored));
Expand Down Expand Up @@ -128,6 +135,34 @@ impl<'a> FixList<'a> {
}
}

pub struct CauseList<'a> {
causes: &'a [RootCause],
}

impl<'a> CauseList<'a> {
pub fn new(causes: &'a [RootCause]) -> Self {
Self { causes }
}

pub fn render(&self) -> String {
if self.causes.is_empty() {
return String::new();
}

let mut output = String::new();
let palette = ColorPalette::default();

output.push_str(&palette.accent_text("COMMON CAUSES\n"));

for cause in self.causes {
let likelihood = format!("[{}]", cause.likelihood).cyan();
output.push_str(&format!(" - {} {}\n", likelihood, cause.description));
}

output
}
}

/// Renders a colored budget utilization bar for Soroban resource usage.
pub struct BudgetBar {
label: &'static str,
Expand Down Expand Up @@ -414,7 +449,8 @@ mod tests {
error_category: "Contract".to_string(),
error_code: 1,
error_name: "InsufficientBalance".to_string(),
summary: "The account does not have enough balance to complete this transaction.".to_string(),
summary: "The account does not have enough balance to complete this transaction."
.to_string(),
detailed_explanation: String::new(),
severity: Severity::Error,
root_causes: Vec::new(),
Expand Down Expand Up @@ -448,15 +484,13 @@ mod tests {

#[test]
fn heatmap_renders_function_names() {
let profile = make_profile(vec![
ResourceHotspot {
location: "transfer::invoke".to_string(),
cpu_instructions: 800_000,
cpu_percentage: 80.0,
memory_bytes: 300_000,
memory_percentage: 30.0,
},
]);
let profile = make_profile(vec![ResourceHotspot {
location: "transfer::invoke".to_string(),
cpu_instructions: 800_000,
cpu_percentage: 80.0,
memory_bytes: 300_000,
memory_percentage: 30.0,
}]);
let output = render_heatmap(&profile);
assert!(output.contains("transfer::invoke"));
}
Expand All @@ -470,6 +504,21 @@ mod tests {
assert!(rendered.contains("does not have enough balance"));
}

#[test]
fn cause_list_renders_common_causes() {
let causes = vec![RootCause {
description: "The transaction was submitted with an undersized resource budget."
.to_string(),
likelihood: "high".to_string(),
}];

let rendered = render_cause_list(&causes);

assert!(rendered.contains("COMMON CAUSES"));
assert!(rendered.contains("undersized resource budget"));
assert!(rendered.contains("high"));
}

#[test]
fn render_context_table_with_arguments() {
let context = TransactionContext {
Expand Down
35 changes: 32 additions & 3 deletions crates/core/src/decode/report.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@


use crate::decode::host_error::ClassifiedError;
use crate::taxonomy::loader::TaxonomyDatabase;
use crate::error::PrismResult;
use crate::taxonomy::loader::TaxonomyDatabase;
use crate::types::report::{DiagnosticReport, RootCause, Severity, SuggestedFix};

pub fn build_report(error: &ClassifiedError) -> PrismResult<DiagnosticReport> {
Expand Down Expand Up @@ -59,3 +57,34 @@ pub fn build_report(error: &ClassifiedError) -> PrismResult<DiagnosticReport> {
))
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::taxonomy::schema::ErrorCategory;

#[test]
fn tier1_common_causes_surface_in_decoded_report() {
let classified = ClassifiedError {
category: ErrorCategory::Budget,
error_code: 0,
is_contract_error: false,
contract_id: None,
raw_data: serde_json::Value::Null,
};

let report = build_report(&classified).expect("Report should build");

assert!(
!report.root_causes.is_empty(),
"Decoded report should include at least one common cause"
);
assert!(
report
.root_causes
.iter()
.any(|cause| cause.description.contains("loops")),
"Decoded report should include taxonomy common cause descriptions"
);
}
}
109 changes: 108 additions & 1 deletion crates/core/src/taxonomy/data/budget.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ source_module = "soroban-env-host/src/budget.rs"
id = "host.budget.limit_exceeded.cpu"
category = "budget"
code = 0
name = "LimitExceeded"
name = "CPUExceeded"
severity = "Error"
since_protocol = 20
summary = "The transaction exceeded its CPU instruction budget."
Expand Down Expand Up @@ -54,3 +54,110 @@ requires_upgrade = true
related_errors = ["host.budget.limit_exceeded.memory"]
source_file = "soroban-env-host/src/budget.rs"
documentation_url = "https://soroban.stellar.org/docs/fundamentals/fees-and-metering"

[[errors]]
id = "host.budget.insufficient_instructions"
category = "budget"
code = 1
name = "InsufficientInstructions"
severity = "Error"
since_protocol = 20
summary = "The transaction did not have enough CPU instructions allocated."
detailed_explanation = """
The transaction's resource declaration did not reserve enough CPU instructions for the contract
invocation to complete. This usually happens when submitted limits are stale, manually edited, or
lower than the values returned by simulation.
"""
related_errors = ["host.budget.limit_exceeded.cpu"]
source_file = "soroban-env-host/src/budget.rs"
documentation_url = "https://soroban.stellar.org/docs/fundamentals/fees-and-metering"

[[errors.common_causes]]
description = "The transaction was submitted with CPU limits lower than the simulated requirement"
likelihood = "high"

[[errors.common_causes]]
description = "Contract logic became more expensive after the transaction was prepared"
likelihood = "medium"

[[errors.suggested_fixes]]
description = "Re-run simulation and submit the transaction with the updated CPU instruction limit"
difficulty = "easy"
requires_upgrade = false

[[errors.suggested_fixes]]
description = "Reduce the amount of work performed by the contract invocation"
difficulty = "medium"
requires_upgrade = true

[[errors]]
id = "host.budget.insufficient_memory"
category = "budget"
code = 2
name = "InsufficientMemory"
severity = "Error"
since_protocol = 20
summary = "The transaction did not have enough memory allocated."
detailed_explanation = """
The transaction's resource declaration did not reserve enough memory for the host objects, contract
data, or intermediate values created during execution.
"""
related_errors = ["host.budget.limit_exceeded.cpu"]
source_file = "soroban-env-host/src/budget.rs"
documentation_url = "https://soroban.stellar.org/docs/fundamentals/fees-and-metering"

[[errors.common_causes]]
description = "The invocation builds large vectors, maps, bytes, or strings in memory"
likelihood = "high"

[[errors.common_causes]]
description = "A cross-contract call returned more data than the caller budgeted for"
likelihood = "medium"

[[errors.suggested_fixes]]
description = "Re-run simulation and submit the transaction with the updated memory limit"
difficulty = "easy"
requires_upgrade = false

[[errors.suggested_fixes]]
description = "Split large inputs or outputs into smaller contract calls"
difficulty = "medium"
requires_upgrade = true

[[errors]]
id = "host.budget.exceeded_limit"
category = "budget"
code = 8
name = "ExceededLimit"
severity = "Error"
since_protocol = 20
summary = "The transaction exceeded an allocated resource limit."
detailed_explanation = """
The host stopped execution because one of the metered resource limits was exceeded. The exact
resource may depend on the diagnostic events and the resource profile for the transaction.
"""
related_errors = [
"host.budget.limit_exceeded.cpu",
"host.budget.insufficient_instructions",
"host.budget.insufficient_memory",
]
source_file = "soroban-env-host/src/budget.rs"
documentation_url = "https://soroban.stellar.org/docs/fundamentals/fees-and-metering"

[[errors.common_causes]]
description = "The transaction used more CPU, memory, or ledger I/O than its declared resource limits allowed"
likelihood = "high"

[[errors.common_causes]]
description = "The resource footprint was estimated before inputs or contract state changed"
likelihood = "medium"

[[errors.suggested_fixes]]
description = "Simulate the transaction again and submit it with the returned resource limits"
difficulty = "easy"
requires_upgrade = false

[[errors.suggested_fixes]]
description = "Inspect the resource profile to identify the expensive contract call"
difficulty = "medium"
requires_upgrade = false
33 changes: 33 additions & 0 deletions crates/core/src/taxonomy/data/context.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,39 @@ name = "Context"
description = "Errors triggered by invalid execution context, such as calling host functions outside of a valid invocation or re-entrancy violations."
source_module = "soroban-env-host/src/host.rs"

[[errors]]
id = "host.context.unknown_error"
category = "context"
code = 0
name = "UnknownError"
severity = "Error"
since_protocol = 20
summary = "An unexpected Soroban runtime error occurred."
detailed_explanation = """
The host returned a context error that does not map to a more specific execution-context failure.
This can indicate malformed transaction data, an unsupported host state, or a platform issue.
"""
related_errors = ["host.context.internal_error"]
source_file = "soroban-env-host/src/host.rs"

[[errors.common_causes]]
description = "The transaction reached a host state that Prism cannot classify more specifically"
likelihood = "medium"

[[errors.common_causes]]
description = "Malformed or unsupported transaction data triggered a generic context failure"
likelihood = "medium"

[[errors.suggested_fixes]]
description = "Check diagnostic events and transaction metadata for the host operation that failed"
difficulty = "medium"
requires_upgrade = false

[[errors.suggested_fixes]]
description = "Reproduce the transaction on the same network and report it if the error persists"
difficulty = "medium"
requires_upgrade = false

[[errors]]
id = "host.context.invalid_action"
category = "context"
Expand Down
Loading