Skip to content

Commit 16f50a8

Browse files
authored
Merge pull request #6018 from obycode/feat/propagate-vm-error
feat: include PC abort reason in tx receipt
2 parents 7d7e414 + e0875ed commit 16f50a8

File tree

20 files changed

+293
-197
lines changed

20 files changed

+293
-197
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
1212
- Added field `vm_error` to EventObserver transaction outputs
1313
- Added new `ValidateRejectCode` values to the `/v3/block_proposal` endpoint
1414
- Added `StateMachineUpdateContent::V1` to support a vector of `StacksTransaction` expected to be replayed in subsequent Stacks blocks
15+
- 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.
1516

1617
### Changed
1718

clarity/src/vm/clarity.rs

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,30 @@ pub enum Error {
2020
Interpreter(InterpreterError),
2121
BadTransaction(String),
2222
CostError(ExecutionCost, ExecutionCost),
23-
AbortedByCallback(Option<Value>, AssetMap, Vec<StacksTransactionEvent>),
23+
AbortedByCallback {
24+
/// What the output value of the transaction would have been.
25+
/// This will be a Some for contract-calls, and None for contract initialization txs.
26+
output: Option<Value>,
27+
/// The asset map which was evaluated by the abort callback
28+
assets_modified: AssetMap,
29+
/// The events from the transaction processing
30+
tx_events: Vec<StacksTransactionEvent>,
31+
/// A human-readable explanation for aborting the transaction
32+
reason: String,
33+
},
2434
}
2535

2636
impl fmt::Display for Error {
2737
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
28-
match *self {
38+
match self {
2939
Error::CostError(ref a, ref b) => {
30-
write!(f, "Cost Error: {} cost exceeded budget of {} cost", a, b)
40+
write!(f, "Cost Error: {a} cost exceeded budget of {b} cost")
3141
}
3242
Error::Analysis(ref e) => fmt::Display::fmt(e, f),
3343
Error::Parse(ref e) => fmt::Display::fmt(e, f),
34-
Error::AbortedByCallback(..) => write!(f, "Post condition aborted transaction"),
44+
Error::AbortedByCallback { reason, .. } => {
45+
write!(f, "Post condition aborted transaction: {reason}")
46+
}
3547
Error::Interpreter(ref e) => fmt::Display::fmt(e, f),
3648
Error::BadTransaction(ref s) => fmt::Display::fmt(s, f),
3749
}
@@ -42,7 +54,7 @@ impl std::error::Error for Error {
4254
fn cause(&self) -> Option<&dyn std::error::Error> {
4355
match *self {
4456
Error::CostError(ref _a, ref _b) => None,
45-
Error::AbortedByCallback(..) => None,
57+
Error::AbortedByCallback { .. } => None,
4658
Error::Analysis(ref e) => Some(e),
4759
Error::Parse(ref e) => Some(e),
4860
Error::Interpreter(ref e) => Some(e),
@@ -168,16 +180,17 @@ pub trait TransactionConnection: ClarityConnection {
168180
/// * the asset changes during `to_do` in an `AssetMap`
169181
/// * the Stacks events during the transaction
170182
///
171-
/// and a `bool` value which is `true` if the `abort_call_back` caused the changes to abort.
183+
/// and an optional string value which is the result of `abort_call_back`,
184+
/// containing a human-readable reason for aborting the transaction.
172185
///
173186
/// If `to_do` returns an `Err` variant, then the changes are aborted.
174187
fn with_abort_callback<F, A, R, E>(
175188
&mut self,
176189
to_do: F,
177190
abort_call_back: A,
178-
) -> Result<(R, AssetMap, Vec<StacksTransactionEvent>, bool), E>
191+
) -> Result<(R, AssetMap, Vec<StacksTransactionEvent>, Option<String>), E>
179192
where
180-
A: FnOnce(&AssetMap, &mut ClarityDatabase) -> bool,
193+
A: FnOnce(&AssetMap, &mut ClarityDatabase) -> Option<String>,
181194
F: FnOnce(&mut OwnedEnvironment) -> Result<(R, AssetMap, Vec<StacksTransactionEvent>), E>,
182195
E: From<InterpreterError>;
183196

@@ -283,16 +296,16 @@ pub trait TransactionConnection: ClarityConnection {
283296
.stx_transfer(from, to, amount, memo)
284297
.map_err(Error::from)
285298
},
286-
|_, _| false,
299+
|_, _| None,
287300
)
288301
.map(|(value, assets, events, _)| (value, assets, events))
289302
}
290303

291304
/// Execute a contract call in the current block.
292-
/// If an error occurs while processing the transaction, its modifications will be rolled back.
293-
/// abort_call_back is called with an AssetMap and a ClarityDatabase reference,
294-
/// if abort_call_back returns true, all modifications from this transaction will be rolled back.
295-
/// otherwise, they will be committed (though they may later be rolled back if the block itself is rolled back).
305+
/// If an error occurs while processing the transaction, its modifications will be rolled back.
306+
/// `abort_call_back` is called with an `AssetMap` and a `ClarityDatabase` reference,
307+
/// If `abort_call_back` returns `Some(reason)`, all modifications from this transaction will be rolled back.
308+
/// Otherwise, they will be committed (though they may later be rolled back if the block itself is rolled back).
296309
#[allow(clippy::too_many_arguments)]
297310
fn run_contract_call<F>(
298311
&mut self,
@@ -305,7 +318,7 @@ pub trait TransactionConnection: ClarityConnection {
305318
max_execution_time: Option<std::time::Duration>,
306319
) -> Result<(Value, AssetMap, Vec<StacksTransactionEvent>), Error>
307320
where
308-
F: FnOnce(&AssetMap, &mut ClarityDatabase) -> bool,
321+
F: FnOnce(&AssetMap, &mut ClarityDatabase) -> Option<String>,
309322
{
310323
let expr_args: Vec<_> = args
311324
.iter()
@@ -331,20 +344,25 @@ pub trait TransactionConnection: ClarityConnection {
331344
},
332345
abort_call_back,
333346
)
334-
.and_then(|(value, assets, events, aborted)| {
335-
if aborted {
336-
Err(Error::AbortedByCallback(Some(value), assets, events))
347+
.and_then(|(value, assets_modified, tx_events, reason)| {
348+
if let Some(reason) = reason {
349+
Err(Error::AbortedByCallback {
350+
output: Some(value),
351+
assets_modified,
352+
tx_events,
353+
reason,
354+
})
337355
} else {
338-
Ok((value, assets, events))
356+
Ok((value, assets_modified, tx_events))
339357
}
340358
})
341359
}
342360

343361
/// Initialize a contract in the current block.
344362
/// If an error occurs while processing the initialization, it's modifications will be rolled back.
345-
/// abort_call_back is called with an AssetMap and a ClarityDatabase reference,
346-
/// if abort_call_back returns true, all modifications from this transaction will be rolled back.
347-
/// otherwise, they will be committed (though they may later be rolled back if the block itself is rolled back).
363+
/// `abort_call_back` is called with an `AssetMap` and a `ClarityDatabase` reference,
364+
/// If `abort_call_back` returns `Some(reason)`, all modifications from this transaction will be rolled back.
365+
/// Otherwise, they will be committed (though they may later be rolled back if the block itself is rolled back).
348366
#[allow(clippy::too_many_arguments)]
349367
fn initialize_smart_contract<F>(
350368
&mut self,
@@ -357,9 +375,9 @@ pub trait TransactionConnection: ClarityConnection {
357375
max_execution_time: Option<std::time::Duration>,
358376
) -> Result<(AssetMap, Vec<StacksTransactionEvent>), Error>
359377
where
360-
F: FnOnce(&AssetMap, &mut ClarityDatabase) -> bool,
378+
F: FnOnce(&AssetMap, &mut ClarityDatabase) -> Option<String>,
361379
{
362-
let (_, asset_map, events, aborted) = self.with_abort_callback(
380+
let (_, assets_modified, tx_events, reason) = self.with_abort_callback(
363381
|vm_env| {
364382
if let Some(max_execution_time_duration) = max_execution_time {
365383
vm_env
@@ -378,10 +396,15 @@ pub trait TransactionConnection: ClarityConnection {
378396
},
379397
abort_call_back,
380398
)?;
381-
if aborted {
382-
Err(Error::AbortedByCallback(None, asset_map, events))
399+
if let Some(reason) = reason {
400+
Err(Error::AbortedByCallback {
401+
output: None,
402+
assets_modified,
403+
tx_events,
404+
reason,
405+
})
383406
} else {
384-
Ok((asset_map, events))
407+
Ok((assets_modified, tx_events))
385408
}
386409
}
387410
}

stackslib/src/chainstate/coordinator/tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ pub fn setup_states_with_epochs(
391391
Value::UInt(burnchain.pox_constants.reward_cycle_length as u128),
392392
Value::UInt(burnchain.pox_constants.pox_rejection_fraction as u128),
393393
],
394-
|_, _| false,
394+
|_, _| None,
395395
None,
396396
)
397397
.expect("Failed to set burnchain parameters in PoX contract");

stackslib/src/chainstate/nakamoto/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4945,7 +4945,7 @@ impl NakamotoChainState {
49454945
&ast,
49464946
&contract_content,
49474947
None,
4948-
|_, _| false,
4948+
|_, _| None,
49494949
None,
49504950
)
49514951
.unwrap();

stackslib/src/chainstate/nakamoto/signer_set.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ impl NakamotoSigners {
335335
)
336336
})
337337
},
338-
|_, _| false,
338+
|_, _| None,
339339
)
340340
.expect("FATAL: failed to update signer stackerdb");
341341

stackslib/src/chainstate/stacks/boot/contract_tests.rs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,7 +1177,7 @@ fn pox_2_delegate_extend_units() {
11771177
Value::UInt(25),
11781178
Value::UInt(0),
11791179
],
1180-
|_, _| false,
1180+
|_, _| None,
11811181
None,
11821182
)
11831183
})
@@ -1780,15 +1780,7 @@ fn test_deploy_smart_contract(
17801780
block.as_transaction(|tx| {
17811781
let (ast, analysis) =
17821782
tx.analyze_smart_contract(contract_id, version, content, ASTRules::PrecheckSize)?;
1783-
tx.initialize_smart_contract(
1784-
contract_id,
1785-
version,
1786-
&ast,
1787-
content,
1788-
None,
1789-
|_, _| false,
1790-
None,
1791-
)?;
1783+
tx.initialize_smart_contract(contract_id, version, &ast, content, None, |_, _| None, None)?;
17921784
tx.save_analysis(contract_id, &analysis)?;
17931785
return Ok(());
17941786
})

stackslib/src/chainstate/stacks/boot/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,7 @@ impl StacksChainState {
562562
)
563563
})
564564
},
565-
|_, _| false,
565+
|_, _| None,
566566
)
567567
.expect("FATAL: failed to handle PoX unlock");
568568

stackslib/src/chainstate/stacks/db/blocks.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4176,7 +4176,7 @@ impl StacksChainState {
41764176
&boot_code_id(active_pox_contract, mainnet),
41774177
"stack-stx",
41784178
&args,
4179-
|_, _| false,
4179+
|_, _| None,
41804180
None,
41814181
)
41824182
});
@@ -4385,7 +4385,7 @@ impl StacksChainState {
43854385
until_burn_height_val,
43864386
reward_addr_val,
43874387
],
4388-
|_, _| false,
4388+
|_, _| None,
43894389
None,
43904390
)
43914391
});
@@ -4492,7 +4492,7 @@ impl StacksChainState {
44924492
Value::UInt(round.clone().into()),
44934493
Value::UInt(reward_cycle.clone().into()),
44944494
],
4495-
|_, _| false,
4495+
|_, _| None,
44964496
None,
44974497
)
44984498
});

stackslib/src/chainstate/stacks/db/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1624,7 +1624,7 @@ impl StacksChainState {
16241624
&contract,
16251625
"set-burnchain-parameters",
16261626
&params,
1627-
|_, _| false,
1627+
|_, _| None,
16281628
None,
16291629
)
16301630
.expect("Failed to set burnchain parameters in PoX contract");

0 commit comments

Comments
 (0)