Skip to content

refactor: Extract post-conditions from stackslib into separate crate - #7478

Open
jbencin-stacks wants to merge 13 commits into
stacks-network:mainfrom
jbencin-stacks:refactor/stacks-postconditions-crate
Open

refactor: Extract post-conditions from stackslib into separate crate#7478
jbencin-stacks wants to merge 13 commits into
stacks-network:mainfrom
jbencin-stacks:refactor/stacks-postconditions-crate

Conversation

@jbencin-stacks

Copy link
Copy Markdown
Contributor

Description

Extract post-conditions from stackslib into separate crate (stacks-postconditions), so they can be built for WASM targets

Applicable issues

Additional info (benefits, drawbacks, caveats)

Checklist

  • Test coverage for new or modified code paths
  • For new Clarity features or consensus changes, add property tests (see docs/property-testing.md)
  • Changelog fragment(s) or "no changelog" label added (see changelog.d/README.md)
  • Required documentation changes (e.g., rpc/openapi.yaml for RPC endpoints, event-dispatcher.md for new events)
  • New clarity functions have corresponding PR in clarity-benchmarking repo

@jbencin-stacks
jbencin-stacks marked this pull request as draft July 31, 2026 21:12
@jbencin-stacks
jbencin-stacks force-pushed the refactor/stacks-postconditions-crate branch from e65c562 to 856cdd7 Compare August 3, 2026 17:50
jbencin-stacks and others added 5 commits August 3, 2026 15:45
…tion

Post-conditions constrain the assets a transaction may move. That check is a
static function of the declared post-conditions, an `AssetMap`, the origin
principal and the epoch — it needs no database or chainstate, and nothing about
it is specific to Clarity as a source language. Hosting it in `stackslib` made
it unreachable from wasm, and hosting it in `clarity` would tie it to one
contract language as we look at supporting others.

Move `check_transaction_postconditions` and its `HashableClarityValue` helper
into a new `stacks-postconditions` crate, leaving a thin adapter in `stackslib`
that projects `StacksAccount` onto the origin principal the check needs.

The crate still depends on `clarity` for `AssetMap` and `VmExecutionError`, so
this does not yet decouple from the Clarity VM; it isolates the logic so that
the remaining step is a change to one crate rather than a change spread across
`stackslib`.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The three epoch-activation rules for post-conditions (`Originator` mode and
NFT `MaybeSent` requiring Stacks 3.4, `Staking`/`Pox` requiring Stacks 4.0)
were duplicated verbatim between `process_transaction_precheck` and
`StacksBlock::validate_transaction_static_epoch`. Like the checker moved in the
previous commit they are pure functions of the post-conditions, the mode and
the epoch, with no chainstate dependency.

Add `check_post_conditions_supported_in_epoch`, returning a typed
`UnsupportedPostCondition` rather than a formatted message so both call sites
keep their existing error channel and log level. This collapses the two copies
into one predicate, and matters for wasm consumers: because
`check_transaction_postconditions` evaluates explicit post-conditions in every
epoch, a caller running only that function would return a pass/fail verdict for
a transaction mainnet rejects outright.

Behavior is unchanged, including the message text at both call sites. The
`Display` impl carries the "before Stacks X.Y" phrasing that
`process_transaction_precheck` puts in its error, and `subject()` exposes the
offending feature on its own so `validate_transaction_static_epoch` can keep
naming the current epoch in its log.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Move the tests that exercise the checker in isolation — a hand-built `AssetMap`
plus a list of post-conditions — alongside the code they cover. The
`process_transaction` pipeline tests that reach the checker through the node
(fee handling, receipts, rollback) stay in `stackslib`.

This is a pure move. The only changes to the test bodies are mechanical: the
`make_account` helper is gone, since the relocated check takes a
`&PrincipalData` rather than a `&StacksAccount`, so each case passes the origin
principal directly instead of fabricating a nonce and balance the check ignored.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
`AssetMap::to_table()` excludes the stacking map and the pox-action set, so the
staking and PoX coverage checks are hand-rolled loops reachable only through
their own path. Several branches in them had no test:

- `Originator` mode for staking/PoX coverage. Every existing test used
  `Deny`/`Allow`, leaving the non-origin skip in
  `enforce_unchecked_assets_for_principal` unexercised.
- `PostConditionPrincipal::Standard` for `Staking`/`Pox`. All existing tests
  used `::Origin`, so non-origin resolution through `to_principal_data` was
  untested for these two variants.
- The `amount_staked == 0` skip in the staking coverage loop.
- What the epoch gate does *not* gate: explicit `Staking`/`Pox` post-conditions
  are evaluated in every epoch, and only the coverage requirement is gated.
  Existing pre-4.0 cases used empty post-condition lists.

Also cover `check_post_conditions_supported_in_epoch` on both sides of the
Stacks 3.4 and 4.0 boundaries, and add a test showing the admission and
asset-movement checks are independent, so callers need both.

Correct stale terminology in the PoX test comments, which referred to
"unstaking" / "MaybeUnstaked" from an earlier naming iteration; the condition
codes are `PoxConditionCode::{NotPerformed, MaybePerformed, Performed}` over
general PoX actions.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@jbencin-stacks
jbencin-stacks force-pushed the refactor/stacks-postconditions-crate branch from 856cdd7 to cdbf6ca Compare August 3, 2026 19:45
@jbencin-stacks
jbencin-stacks marked this pull request as ready for review August 3, 2026 20:41
brice-stacks
brice-stacks previously approved these changes Aug 4, 2026

@brice-stacks brice-stacks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment thread changelog.d/stacks-postconditions-crate.changed Outdated

@benjamin-stacks benjamin-stacks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM overall, just a few smaller things.

Comment thread stacks-postconditions/src/lib.rs Outdated
Comment thread stacks-postconditions/src/lib.rs Outdated
Comment thread stacks-postconditions/src/tests.rs Outdated
Comment thread stacks-postconditions/src/lib.rs
Comment thread stacks-postconditions/src/lib.rs Outdated
brice-stacks
brice-stacks previously approved these changes Aug 7, 2026

@brice-stacks brice-stacks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@cylewitruk-stacks

cylewitruk-stacks commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Tbh, I would prefer to see this and the transaction types which previously ended up under stacks-codec colocated within a stacks-transactions crate; similar to what I did in this structural PoC: cylewitruk-stacks#4. Otherwise, I think the scope of this crate is too narrow and isn't pulling its weight, contributing to the dependency-direction spaghetti.

Of course, to keep the diff manageable, that could be achieved by just renaming this new crate to stacks-transactions and we can move the other types over in another PR.

Thoughts?

@jbencin-stacks

jbencin-stacks commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

@cylewitruk-stacks I think that makes sense, but if we're trying to minimize diffs here, then I should move it to crates/stacks-transactions, right? Since that's where it'll eventually go

@dhaney-stacks
dhaney-stacks requested review from hstove-stacks and removed request for benjamin-stacks August 13, 2026 16:27
@jbencin-stacks

Copy link
Copy Markdown
Contributor Author

@cylewitruk-stacks Looking into this more, it seems that stacks-postconditions currently requires the entire Clarity VM (it doesn't actually need the VM, but some types that aren't in clarity-types), whereas stacks-codec does not, so that would add a big dependency for any consumer that just want's the codec

Having a larger number of smaller crates won't be an issue once we clean up the top level and organize them. Maybe we can have:

  • crates/stacks-transactions/codec
  • crates/stacks-transactions/postconditions

But let's save that for another PR

@cylewitruk-stacks

cylewitruk-stacks commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

but if we're trying to minimize diffs here, then I should move it to crates/stacks-transactions, right? Since that's where it'll eventually go

Yeah, exactly what I was thinking :)

@cylewitruk-stacks Looking into this more, it seems that stacks-postconditions currently requires the entire Clarity VM (it doesn't actually need the VM, but some types that aren't in clarity-types), whereas stacks-codec does not, so that would add a big dependency for any consumer that just want's the codec

Having a larger number of smaller crates won't be an issue once we clean up the top level and organize them. Maybe we can have:

  • crates/stacks-transactions/codec
  • crates/stacks-transactions/postconditions

But let's save that for another PR

Right, so, I think that:

  • AssetMap and AssetMapEntry could be moved into clarity-types rather easily and was probably a miss (I haven't deep-dived this, but they look to only depend on std and other types in clarity-types at a glance).
  • Instead of using the two error types from clarity, post-conditions should have their own PostConditionError (or whatever) error type if being extracted, and then consumers within clarity would map those to their own relevant vm-error type.

Assuming my assumptions hold, I think that would be the cleaner structural direction, and the moving of AssetMap etc. could be done in a separate PR to keep this PR's diff focused. What do you think?

@jbencin-stacks

Copy link
Copy Markdown
Contributor Author

Instead of using the two error types from clarity, post-conditions should have their own PostConditionError (or whatever)

Yeah, eventually we'd like postconditions to be completely independent of Clarity so that we could use them with other languages (like Rust compiled to Wasm). My first attempt at this refactor moved them into clarity, and Brice pointed that out

I don't have a strong opinion one way or the other whether this in theory belongs in the same crate as the codec, but for now I don't think it makes sense because of the Clarity dependency. We can revisit later once the dependency is eliminated

@cylewitruk-stacks

cylewitruk-stacks commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Instead of using the two error types from clarity, post-conditions should have their own PostConditionError (or whatever)

Yeah, eventually we'd like postconditions to be completely independent of Clarity so that we could use them with other languages (like Rust compiled to Wasm). My first attempt at this refactor moved them into clarity, and Brice pointed that out

I don't have a strong opinion one way or the other whether this in theory belongs in the same crate as the codec, but for now I don't think it makes sense because of the Clarity dependency. We can revisit later once the dependency is eliminated

The clarity dependency is trivially removed; see #7513. Then you'd handle the remaining error types in the same way I did with the newly introduced AssetMapError there -- by having the caller map the error to their own error variant.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants