Skip to content

feat: include PC abort reason in tx receipt #6018

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 6, 2025
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE

- Added new `ValidateRejectCode` values to the `/v3/block_proposal` endpoint
- Added `StateMachineUpdateContent::V1` to support a vector of `StacksTransaction` expected to be replayed in subsequent Stacks blocks
- Include a reason string in the transaction receipt when a transaction is rolled back due to a post-condition. This should help users in understanding what went wrong.

### Changed

Expand Down
75 changes: 49 additions & 26 deletions clarity/src/vm/clarity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,30 @@ pub enum Error {
Interpreter(InterpreterError),
BadTransaction(String),
CostError(ExecutionCost, ExecutionCost),
AbortedByCallback(Option<Value>, AssetMap, Vec<StacksTransactionEvent>),
AbortedByCallback {
/// What the output value of the transaction would have been.
/// This will be a Some for contract-calls, and None for contract initialization txs.
output: Option<Value>,
/// The asset map which was evaluated by the abort callback
assets_modified: AssetMap,
/// The events from the transaction processing
tx_events: Vec<StacksTransactionEvent>,
/// A human-readable explanation for aborting the transaction
reason: String,
},
}

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
match self {
Error::CostError(ref a, ref b) => {
write!(f, "Cost Error: {} cost exceeded budget of {} cost", a, b)
write!(f, "Cost Error: {a} cost exceeded budget of {b} cost")
}
Error::Analysis(ref e) => fmt::Display::fmt(e, f),
Error::Parse(ref e) => fmt::Display::fmt(e, f),
Error::AbortedByCallback(..) => write!(f, "Post condition aborted transaction"),
Error::AbortedByCallback { reason, .. } => {
write!(f, "Post condition aborted transaction: {reason}")
}
Error::Interpreter(ref e) => fmt::Display::fmt(e, f),
Error::BadTransaction(ref s) => fmt::Display::fmt(s, f),
}
Expand All @@ -42,7 +54,7 @@ impl std::error::Error for Error {
fn cause(&self) -> Option<&dyn std::error::Error> {
match *self {
Error::CostError(ref _a, ref _b) => None,
Error::AbortedByCallback(..) => None,
Error::AbortedByCallback { .. } => None,
Error::Analysis(ref e) => Some(e),
Error::Parse(ref e) => Some(e),
Error::Interpreter(ref e) => Some(e),
Expand Down Expand Up @@ -168,16 +180,17 @@ pub trait TransactionConnection: ClarityConnection {
/// * the asset changes during `to_do` in an `AssetMap`
/// * the Stacks events during the transaction
///
/// and a `bool` value which is `true` if the `abort_call_back` caused the changes to abort.
/// and an optional string value which is the result of `abort_call_back`,
/// containing a human-readable reason for aborting the transaction.
///
/// If `to_do` returns an `Err` variant, then the changes are aborted.
fn with_abort_callback<F, A, R, E>(
&mut self,
to_do: F,
abort_call_back: A,
) -> Result<(R, AssetMap, Vec<StacksTransactionEvent>, bool), E>
) -> Result<(R, AssetMap, Vec<StacksTransactionEvent>, Option<String>), E>
where
A: FnOnce(&AssetMap, &mut ClarityDatabase) -> bool,
A: FnOnce(&AssetMap, &mut ClarityDatabase) -> Option<String>,
F: FnOnce(&mut OwnedEnvironment) -> Result<(R, AssetMap, Vec<StacksTransactionEvent>), E>,
E: From<InterpreterError>;

Expand Down Expand Up @@ -283,16 +296,16 @@ pub trait TransactionConnection: ClarityConnection {
.stx_transfer(from, to, amount, memo)
.map_err(Error::from)
},
|_, _| false,
|_, _| None,
)
.map(|(value, assets, events, _)| (value, assets, events))
}

/// Execute a contract call in the current block.
/// If an error occurs while processing the transaction, its modifications will be rolled back.
/// abort_call_back is called with an AssetMap and a ClarityDatabase reference,
/// if abort_call_back returns true, all modifications from this transaction will be rolled back.
/// otherwise, they will be committed (though they may later be rolled back if the block itself is rolled back).
/// If an error occurs while processing the transaction, its modifications will be rolled back.
/// `abort_call_back` is called with an `AssetMap` and a `ClarityDatabase` reference,
/// If `abort_call_back` returns `Some(reason)`, all modifications from this transaction will be rolled back.
/// Otherwise, they will be committed (though they may later be rolled back if the block itself is rolled back).
#[allow(clippy::too_many_arguments)]
fn run_contract_call<F>(
&mut self,
Expand All @@ -305,7 +318,7 @@ pub trait TransactionConnection: ClarityConnection {
max_execution_time: Option<std::time::Duration>,
) -> Result<(Value, AssetMap, Vec<StacksTransactionEvent>), Error>
where
F: FnOnce(&AssetMap, &mut ClarityDatabase) -> bool,
F: FnOnce(&AssetMap, &mut ClarityDatabase) -> Option<String>,
{
let expr_args: Vec<_> = args
.iter()
Expand All @@ -331,20 +344,25 @@ pub trait TransactionConnection: ClarityConnection {
},
abort_call_back,
)
.and_then(|(value, assets, events, aborted)| {
if aborted {
Err(Error::AbortedByCallback(Some(value), assets, events))
.and_then(|(value, assets_modified, tx_events, reason)| {
if let Some(reason) = reason {
Err(Error::AbortedByCallback {
output: Some(value),
assets_modified,
tx_events,
reason,
})
} else {
Ok((value, assets, events))
Ok((value, assets_modified, tx_events))
}
})
}

/// Initialize a contract in the current block.
/// If an error occurs while processing the initialization, it's modifications will be rolled back.
/// abort_call_back is called with an AssetMap and a ClarityDatabase reference,
/// if abort_call_back returns true, all modifications from this transaction will be rolled back.
/// otherwise, they will be committed (though they may later be rolled back if the block itself is rolled back).
/// `abort_call_back` is called with an `AssetMap` and a `ClarityDatabase` reference,
/// If `abort_call_back` returns `Some(reason)`, all modifications from this transaction will be rolled back.
/// Otherwise, they will be committed (though they may later be rolled back if the block itself is rolled back).
#[allow(clippy::too_many_arguments)]
fn initialize_smart_contract<F>(
&mut self,
Expand All @@ -357,9 +375,9 @@ pub trait TransactionConnection: ClarityConnection {
max_execution_time: Option<std::time::Duration>,
) -> Result<(AssetMap, Vec<StacksTransactionEvent>), Error>
where
F: FnOnce(&AssetMap, &mut ClarityDatabase) -> bool,
F: FnOnce(&AssetMap, &mut ClarityDatabase) -> Option<String>,
{
let (_, asset_map, events, aborted) = self.with_abort_callback(
let (_, assets_modified, tx_events, reason) = self.with_abort_callback(
|vm_env| {
if let Some(max_execution_time_duration) = max_execution_time {
vm_env
Expand All @@ -378,10 +396,15 @@ pub trait TransactionConnection: ClarityConnection {
},
abort_call_back,
)?;
if aborted {
Err(Error::AbortedByCallback(None, asset_map, events))
if let Some(reason) = reason {
Err(Error::AbortedByCallback {
output: None,
assets_modified,
tx_events,
reason,
})
} else {
Ok((asset_map, events))
Ok((assets_modified, tx_events))
}
}
}
2 changes: 1 addition & 1 deletion stackslib/src/chainstate/coordinator/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ pub fn setup_states_with_epochs(
Value::UInt(burnchain.pox_constants.reward_cycle_length as u128),
Value::UInt(burnchain.pox_constants.pox_rejection_fraction as u128),
],
|_, _| false,
|_, _| None,
None,
)
.expect("Failed to set burnchain parameters in PoX contract");
Expand Down
2 changes: 1 addition & 1 deletion stackslib/src/chainstate/nakamoto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4945,7 +4945,7 @@ impl NakamotoChainState {
&ast,
&contract_content,
None,
|_, _| false,
|_, _| None,
None,
)
.unwrap();
Expand Down
2 changes: 1 addition & 1 deletion stackslib/src/chainstate/nakamoto/signer_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ impl NakamotoSigners {
)
})
},
|_, _| false,
|_, _| None,
)
.expect("FATAL: failed to update signer stackerdb");

Expand Down
12 changes: 2 additions & 10 deletions stackslib/src/chainstate/stacks/boot/contract_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1177,7 +1177,7 @@ fn pox_2_delegate_extend_units() {
Value::UInt(25),
Value::UInt(0),
],
|_, _| false,
|_, _| None,
None,
)
})
Expand Down Expand Up @@ -1780,15 +1780,7 @@ fn test_deploy_smart_contract(
block.as_transaction(|tx| {
let (ast, analysis) =
tx.analyze_smart_contract(contract_id, version, content, ASTRules::PrecheckSize)?;
tx.initialize_smart_contract(
contract_id,
version,
&ast,
content,
None,
|_, _| false,
None,
)?;
tx.initialize_smart_contract(contract_id, version, &ast, content, None, |_, _| None, None)?;
tx.save_analysis(contract_id, &analysis)?;
return Ok(());
})
Expand Down
2 changes: 1 addition & 1 deletion stackslib/src/chainstate/stacks/boot/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,7 @@ impl StacksChainState {
)
})
},
|_, _| false,
|_, _| None,
)
.expect("FATAL: failed to handle PoX unlock");

Expand Down
6 changes: 3 additions & 3 deletions stackslib/src/chainstate/stacks/db/blocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4176,7 +4176,7 @@ impl StacksChainState {
&boot_code_id(active_pox_contract, mainnet),
"stack-stx",
&args,
|_, _| false,
|_, _| None,
None,
)
});
Expand Down Expand Up @@ -4385,7 +4385,7 @@ impl StacksChainState {
until_burn_height_val,
reward_addr_val,
],
|_, _| false,
|_, _| None,
None,
)
});
Expand Down Expand Up @@ -4492,7 +4492,7 @@ impl StacksChainState {
Value::UInt(round.clone().into()),
Value::UInt(reward_cycle.clone().into()),
],
|_, _| false,
|_, _| None,
None,
)
});
Expand Down
2 changes: 1 addition & 1 deletion stackslib/src/chainstate/stacks/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1624,7 +1624,7 @@ impl StacksChainState {
&contract,
"set-burnchain-parameters",
&params,
|_, _| false,
|_, _| None,
None,
)
.expect("Failed to set burnchain parameters in PoX contract");
Expand Down
Loading
Loading