Skip to content

[mono-move] Add the MonoMove Aptos transaction executor - #20181

Merged
vgao1996 merged 13 commits into
mainfrom
mono-move-aptos-vm-v2
Aug 10, 2026
Merged

[mono-move] Add the MonoMove Aptos transaction executor#20181
vgao1996 merged 13 commits into
mainfrom
mono-move-aptos-vm-v2

Conversation

@vgao1996

@vgao1996 vgao1996 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Description

New crate mono-move/aptos-transaction-executor: the AptosVM transaction layer on the MonoMove VM. Executes one user transaction (prologue → entry function → epilogue) single-threaded, returning an unmaterialized TxnOutcome. Rebased onto current main, so #20242, #20254 and #20289 are all in.

Design choices:

  • One session, one checkpoint. No respawned sessions or per-stage change sets; failed payloads roll back to the post-prologue checkpoint. Prologue/epilogue run unmetered via a scoped meter swap — no gas resets.
  • Deferred materialization. The executor returns raw VM effects; rendering them into a TransactionOutput is a separate, fallible step (materialize()), and the legacy VMStatus mapping is quarantined next to it. The read-write set has no defined iteration order, so the drain gathers every failure it hits and MaterializationError sorts them on construction — what gets reported cannot depend on that order.
  • Typed outcomes. The driver uses DiscardReason/ExecutionStatus, both carrying a MoveExecutionFailure and the ExecutionStage it happened in — illegal states unrepresentable; VMStatus constructed only at the projection edge, which is also where a failure's legitimacy is judged. Aborts carry the AbortLocation the runtime reports, so the executor records where an abort came from rather than the status mapper asserting it afterwards; the prologue's validation table is guarded on that location instead of being applied to every abort regardless of module.
  • Past the prologue, the transaction always commits and pays. Neither a payload VM error nor a misbehaving epilogue discards: the payload's effects are dropped, the epilogue reruns against the state the prologue left, and only a failing rerun discards. This matches failed_transaction_cleanup, and it keeps the executor's control flow out of the legacy status projection — nothing on the execution path calls into materialize/, so that step is genuinely skippable for Block-STM.
  • The executor owns no data access. It borrows ModuleProvider (code) and the runtime's ResourceProvider (reads); materialization needs one extra trait for resource groups. Block-STM later supplies production implementations behind the same traits; the StateView-backed ones (for tests, simulation, replay) live in a separate crate.
  • No block executor. Block semantics belong to the Block-STM workstream (obligations documented in AGENTS.md).

Scope: entry functions and the versioned prologue/epilogue only; resource groups work end to end; gas incomplete (uncalibrated, no IO/storage fees). Scripts, multisig, publishing, keyless/AA, orderless, arg validation: inline TODOs.

Shared types (touches aptos-types and aptos-vm)

SessionId and the prologue/epilogue argument enums were private to aptos-vm, so this executor initially mirrored them. Both are byte-exact-critical — SessionId's BCS crypto-hash seeds AUID generation, and the framework deserializes the arg enums directly — so the copies were a consensus hazard. They now live in aptos-types and both VMs share them.

Two more duplications go the same way. The 13 prologue abort codes were defined per VM, so they now sit next to the argument enums in aptos_types::transaction::validation -- both mirror transaction_validation.move. And every VM rolled its own decoder for Move's abort-code convention, even though aptos_types::error already had canonical() to build a code and the correctly-named categories; split_canonical is the missing inverse. That retires a misnomer along the way: aptos-vm called category 0x2 LIMIT_EXCEEDED, which error.move does not define -- it is OUT_OF_RANGE.

One finding worth flagging: nothing checks the Rust codes still agree with the Move constants they mirror, and they have drifted. The framework calls 1010 PROLOGUE_EFEE_PAYER_NOT_ENABLED, and no Move constant carries 1011. Left as a TODO(testing) -- it needs a framework owner, since a dead code means a prologue abort nothing translates.

SessionId's hash is unchanged: the derived hasher seeds from the serde name, not the module path, and a new test pins the Txn/OrderlessTxn hashes to guard that. Its four TransactionMetadata-taking constructors became primitive-taking ones, with convenience methods on TransactionMetadata itself; move_vm_ext still re-exports SessionId, so existing imports are untouched. With it shared, this executor seeds AUIDs the way the legacy VM does — from the payload session's id — which is what makes comparison testing possible.

Reading map (start at executor.rs, everything else hangs off it):

File What's there
executor.rs The lifecycle driver: preflight → prologue → payload → epilogue
calls.rs, sys_calls.rs Running one Move function; the unmetered prologue/epilogue calls
errors.rs, outcome.rs The typed outcome taxonomy and TxnOutcome
materialize/ txn_output.rs renders effects into a TransactionOutput; vm_status.rs projects onto VMStatus
providers.rs The AptosDataProvider trait (resource groups)
aptos-state-view-providers/ StateView-backed data layers for sequential execution
runtime/core diffs new_idle(), invokeprepare_call, unmetered(), WriteClass/serialize_value pub, tag interning in core

Open questions for reviewers

  • Resource groups and the VM boundary. Should reads carry their storage location (resolving groups once, on read), and how much of groups should the VM layer see at all? The executor itself never uses the group trait today — only materialization does.
  • Where the write-set drain belongs. The runtime's SessionEffects::write_set() is group-unaware and models a single function run (aborts drop all writes), while a transaction abort still commits the epilogue's fee writes. Unifying the two needs a design doc.

Known gaps (fixes coming while in draft)

Multisig payloads slip past the preflight as plain entry functions; type args are not resolved/ability-checked; new_idle skips verify_function; no check_gas preflight. Argument placement now rejects what the legacy VM rejects — function values, references to non-signers, uninstantiated type parameters, and signers that do not lead the parameter list — but as a backstop: rejections surface as invariant violations rather than a signature error, and which structs are constructible from an argument is still unchecked. The attacker-reachable gaps — entry visibility, the remaining argument admissibility, check_gas — are labelled TODO(security, ..) and listed in AGENTS.md as gates that must land before a real coordinator drives this executor; today the only caller is the e2e harness.

How Has This Been Tested?

Differential e2e vs the legacy VM on FakeExecutor genesis (statuses including abort codes and locations, write sets, events; only gas-fee slots masked): p2p transfer, insufficient-balance abort, fee-payer drained (epilogue retry), nonexistent entry function, two dependent transfers applied sequentially. Runtime, differential, aptos-types and aptos-vm unit suites pass; fmt/clippy clean. The SessionId move is covered by the hash-stability and constructor tests described above; broader legacy-VM coverage is left to CI.

🤖 Generated with Claude Code


Note

High Risk
Touches consensus-critical SessionId hashing and shared prologue abort mapping while adding a new execution path; AGENTS.md lists pre-coordinator security gaps (entry visibility, check_gas, argument validation) that are not fully enforced yet.

Overview
Introduces mono-move-aptos-transaction-executor, which runs a single user transaction on MonoMove (versioned prologue → entry payload → epilogue) in one interpreter session with checkpoint/rollback, unmetered system calls, and an unmaterialized TxnOutcome plus optional materialize() to TransactionOutput / VMStatus. mono-move-aptos-state-view-providers implements module/resource reads (including resource groups) from StateView for tests and sequential tools.

Consensus-visible sharing: SessionId, PrologueArgs / EpilogueArgs, and prologue abort codes move into aptos-types; split_canonical replaces per-VM abort decoding (including OUT_OF_RANGE for sequence-too-big / can’t-pay-fee cases). aptos-vm drops its local session_id module and uses TransactionMetadata session-id helpers and TransactionPayload::script_hash().

MonoMove runtime/core: InterpreterContext::new_idle, prepare_call, unmetered, signer ref args, public write-set draining (WriteClass, serialize_value), and intern_type_tag / struct-tag interning; descriptor lookup for materialization.

Differential e2e tests compare legacy AptosVM vs the new executor on FakeExecutor genesis (writes/events; gas-sensitive slots masked).

Reviewed by Cursor Bugbot for commit a0af702. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread third_party/move/mono-move/aptos-vm-v2/src/publish.rs Outdated
@vgao1996
vgao1996 force-pushed the mono-move-aptos-vm-v2 branch from c2d1b0b to 42693dd Compare July 18, 2026 03:36
@vgao1996 vgao1996 changed the title [mono-move] Add aptos-vm-v2 transaction layer [mono-move] Add the MonoMove Aptos transaction executor Jul 22, 2026
vgao1996 added a commit that referenced this pull request Jul 23, 2026
Reimplements the AptosVM transaction-execution layer on the MonoMove VM:
prologue -> entry-function payload -> epilogue in one session with
checkpoint-based rollback, producing an unmaterialized typed conclusion
(TxnOutcome). Legacy statuses and outputs are constructed only in the
legacy/ projection module; mock StateView-backed data providers live in
the separate mono-move-aptos-mock-providers crate. See PR #20181 for
the full description.

Squash of 6 commits for rebase onto main (which absorbed this branch's
SessionEffects/finish/gas_balance runtime additions via #20242).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vgao1996
vgao1996 force-pushed the mono-move-aptos-vm-v2 branch from 9ec9bc5 to 7156378 Compare July 23, 2026 11:43
@vgao1996
vgao1996 marked this pull request as ready for review July 23, 2026 17:17
entry: &EntryFunction,
ty_args: InternedTypeList,
) -> Result<(), PayloadFailure> {
// TODO(completeness): entry-function validation — `entry` visibility,

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.

Medium — User payloads can invoke non-entry framework functions

call_function directly starts whichever function name appears in the signed EntryFunction payload. There is no entry-visibility check, argument admissibility validation, or rejection of unused signer arguments on this path. The legacy executor validates transaction arguments and calls is_entry_or_err() before execution. Consequently, once the declared block-coordinator consumer uses this API, an ordinary signed transaction can invoke public(friend) functions such as 0x1::reconfiguration::reconfigure directly, bypassing the framework call boundary and forcing epoch reconfiguration. The current tree only invokes this executor from its E2E harness, so this is not yet a live network path.

// ============================ Preflight =============================
// Reject what this executor cannot execute, before touching any state.
//
// TODO(metering): pre-flight gas checks (`check_gas`): txn size bounds,

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.

Medium — This transaction path accepts unbounded underpriced execution

The executor intentionally skips the legacy check_gas preflight and then uses the user-supplied max_gas_amount as its MonoMove budget. versioned_prologue only verifies that the payer can cover gas_unit_price * max_gas_amount, so a zero gas price passes that check; the later fee statement also fixes IO and storage fees at zero. A block-coordinator integration of this public API would therefore accept transactions outside the configured size, gas-limit, and gas-price bounds and allow free high-budget execution and state growth. The current tree only invokes this executor from its E2E harness, so this is not yet a live network path.

@vgao1996
vgao1996 requested a review from georgemitenkov July 23, 2026 17:43

@georgemitenkov georgemitenkov 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.

1/n

Comment thread third_party/move/mono-move/aptos-transaction-executor/Cargo.toml Outdated
Comment thread third_party/move/mono-move/core/src/prepared_module.rs
Comment thread third_party/move/mono-move/core/src/prepared_module.rs Outdated
Comment thread third_party/move/mono-move/aptos-mock-providers/AGENTS.md Outdated
Comment thread third_party/move/mono-move/aptos-mock-providers/AGENTS.md Outdated
Comment thread third_party/move/mono-move/aptos-mock-providers/src/lib.rs Outdated
Comment thread third_party/move/mono-move/aptos-mock-providers/src/lib.rs Outdated
Comment thread third_party/move/mono-move/aptos-state-view-providers/src/lib.rs Outdated
Comment thread third_party/move/mono-move/aptos-mock-providers/src/lib.rs Outdated
Comment thread third_party/move/mono-move/aptos-mock-providers/src/lib.rs Outdated

@georgemitenkov georgemitenkov 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.

2/n

Comment thread third_party/move/mono-move/aptos-transaction-executor/src/legacy/mod.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/legacy/materialize.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/legacy/materialize.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/legacy/materialize.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/legacy/materialize.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/legacy/materialize.rs Outdated
let mut group_ops: HashMap<StateKey, BTreeMap<StructTag, MemberOp>> = HashMap::new();

for (key, class) in effects.read_write_set.writes() {
match provider.locate_key(key)? {

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.

We should have info about groups when we read, would it be better to cache it on read-side? It might be quite difficult to refactor current post-processing flow later

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Again, this is related to some of the other discussions on the resource group API. Here's how I see it currently:

  • The location should always be cached, in one form or another. (I believe I'm already doing at least partial caching in the existing mock impls)
  • It is a separate question whether we want the reads/writes themselves to carry the location, which then gets preserved by the Mono Move VM so we can use them here. Honestly, feels like that's gonna add quite some unnecessary complexity to the pure-Move layer, but I'll study it a bit more.

Comment thread third_party/move/mono-move/aptos-transaction-executor/src/legacy/materialize.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/legacy/materialize.rs Outdated

/// The stored members of the group behind `group_key`, as execution read
/// them; write-set publication merges member writes into them.
fn group_members(&self, group_key: &StateKey) -> Result<Arc<BTreeMap<StructTag, Bytes>>>;

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.

Btw, for block-stm we actually want an API like this:

- we already have an efficient way to process groups (so VM calling borrow_global<T> can call into provider with the group tag directly. We do not want to expose members because this was the precise problem killing all the parallelism that was solved before FA rollout.

@vgao1996 vgao1996 Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hmm... this one is interesting. Let me study this a bit more.

One thing to clarify is that the transaction executor itself does NOT actually use this trait at all, but just the regular resource provider, so I don't think we are repeating the parallelism-killing issue.

This trait is currently solely used for write set materialization, and this particular method is for merging resource group members back into group writes. This does make it weird though -- I'm feeling that it may not even belong here, but can't think of an obviously better place at the moment.

Comment thread third_party/move/mono-move/aptos-transaction-executor/src/natives.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/metadata.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/outcome.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/outcome.rs Outdated
- **The write-set drain in `legacy/materialize.rs` is a stopgap.** Real
publication (modification detection, storage metadata, refunds) will be
built inside the runtime by a separate workstream. Do not use the runtime's
`SessionEffects::write_set()` here in the meantime: it does not handle

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.

I think we do need to use SessionEffects::write_set() here, resource group handling seems very heavy weight and is mixed with infra setup. This is something we should think more about and have a concrete design doc how to move forward?

Comment thread third_party/move/mono-move/aptos-transaction-executor/src/sys_calls.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/calls.rs
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/calls.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/calls.rs
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/calls.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/calls.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/calls.rs

@vgao1996 vgao1996 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@georgemitenkov Thanks for the detailed review!

I'll make the easy changes first and then we can continue discussing the trickier design questions.

Here's the big things I think we'll need to revisit:

  1. The write set materialization protocol
  2. Resource group caching and handling

Comment thread third_party/move/mono-move/aptos-state-view-providers/src/lib.rs
Comment thread third_party/move/mono-move/aptos-mock-providers/src/lib.rs Outdated
Comment thread third_party/move/mono-move/aptos-mock-providers/AGENTS.md Outdated
Comment thread third_party/move/mono-move/core/src/prepared_module.rs Outdated

/// The stored members of the group behind `group_key`, as execution read
/// them; write-set publication merges member writes into them.
fn group_members(&self, group_key: &StateKey) -> Result<Arc<BTreeMap<StructTag, Bytes>>>;

@vgao1996 vgao1996 Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hmm... this one is interesting. Let me study this a bit more.

One thing to clarify is that the transaction executor itself does NOT actually use this trait at all, but just the regular resource provider, so I don't think we are repeating the parallelism-killing issue.

This trait is currently solely used for write set materialization, and this particular method is for merging resource group members back into group writes. This does make it weird though -- I'm feeling that it may not even belong here, but can't think of an obviously better place at the moment.

Comment thread third_party/move/mono-move/aptos-transaction-executor/src/legacy/mod.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/legacy/materialize.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/legacy/materialize.rs Outdated
let mut group_ops: HashMap<StateKey, BTreeMap<StructTag, MemberOp>> = HashMap::new();

for (key, class) in effects.read_write_set.writes() {
match provider.locate_key(key)? {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Again, this is related to some of the other discussions on the resource group API. Here's how I see it currently:

  • The location should always be cached, in one form or another. (I believe I'm already doing at least partial caching in the existing mock impls)
  • It is a separate question whether we want the reads/writes themselves to carry the location, which then gets preserved by the Mono Move VM so we can use them here. Honestly, feels like that's gonna add quite some unnecessary complexity to the pure-Move layer, but I'll study it a bit more.

Comment thread third_party/move/mono-move/aptos-transaction-executor/src/legacy/materialize.rs Outdated
vgao1996 added a commit that referenced this pull request Jul 29, 2026
Reimplements the AptosVM transaction-execution layer on the MonoMove VM:
prologue -> entry-function payload -> epilogue in one session with
checkpoint-based rollback, producing an unmaterialized typed conclusion
(TxnOutcome). Legacy statuses and outputs are constructed only in the
legacy/ projection module; mock StateView-backed data providers live in
the separate mono-move-aptos-mock-providers crate. See PR #20181 for
the full description.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vgao1996
vgao1996 force-pushed the mono-move-aptos-vm-v2 branch 3 times, most recently from c0e6feb to cec6322 Compare July 29, 2026 23:45
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/calls.rs
@vgao1996
vgao1996 requested review from wrwg and zekun000 as code owners July 30, 2026 00:17
@vgao1996
vgao1996 force-pushed the mono-move-aptos-vm-v2 branch from c3f8df7 to faf0661 Compare July 30, 2026 00:23
Comment thread aptos-move/aptos-vm/src/transaction_metadata.rs Outdated
Comment thread types/src/transaction/validation_args.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/materialize/mod.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/materialize/mod.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/natives.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/metadata.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/outcome.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/outcome.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/outcome.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/providers.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/providers.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/errors.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/errors.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/errors.rs Outdated
Comment thread third_party/move/mono-move/aptos-transaction-executor/src/errors.rs Outdated
Comment thread third_party/move/mono-move/aptos-state-view-providers/src/lib.rs

@vgao1996 vgao1996 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@georgemitenkov thanks a lot for this new batch of feedback!

1/n Reworked the error hierarchy, again. Inspired by your suggestions, I managed to unify the similarly-shaped ones, and added an enum to distinguish errors from different stages. This addresses all your comments in this file, and ends up being much cleaner.

@vgao1996 vgao1996 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

2/n calls.rs + sys_calls.rs

@vgao1996 vgao1996 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

3/n a bunch of smaller issues (e2e, vm_status, txn_output).

What's left: the providers.

@vgao1996
vgao1996 force-pushed the mono-move-aptos-vm-v2 branch from 7918e0a to d44826c Compare August 10, 2026 22:31
@vgao1996
vgao1996 enabled auto-merge (squash) August 10, 2026 22:33
vgao1996 and others added 13 commits August 10, 2026 22:46
`SessionId`, the prologue/epilogue argument enums, the prologue abort codes,
and the decoding of Move's canonical error convention were all private to
`aptos-vm`, so a second execution layer meant duplicating them. Each is
byte-exact-critical or must track `transaction_validation.move`, making a
copy a consensus hazard rather than a nuisance.

`SessionId` and the argument enums move to `aptos-types`, shared by both VMs.
The hash is unchanged: the derived hasher seeds from the serde name, not the
module path, and a new test pins the `Txn`/`OrderlessTxn` hashes to guard
that. `SessionId`'s four `TransactionMetadata`-taking constructors become
primitive-taking, with convenience methods on `TransactionMetadata` itself,
and `move_vm_ext` still re-exports it so existing imports are untouched.

The 13 prologue abort codes join the argument enums, since both mirror
`transaction_validation.move`. Nothing outside `aptos-vm` imported them.

`aptos_types::error` already mirrored every `error.move` category with the
right names, plus `canonical()` to build a code from a category and reason --
but had no way to take one apart, so each VM rolled its own splitter.
`split_canonical` is that inverse. It also retires a misnomer: `aptos-vm`
called category `0x2` `LIMIT_EXCEEDED`, which `error.move` does not define.
The value is `OUT_OF_RANGE`, what the epilogue's balance assert raises, so
using the shared constants removes the wrong name rather than renaming it.

Nothing checks that the Rust codes still agree with the Move constants they
mirror, and they have already drifted: the framework calls 1010
`PROLOGUE_EFEE_PAYER_NOT_ENABLED`, and no Move constant carries 1011 at all.
Left as a TODO(testing) -- resolving it needs a framework owner, since a
stale name is cosmetic but a dead code is a prologue abort nothing
translates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New crate `mono-move/aptos-transaction-executor`: the AptosVM transaction
layer on the MonoMove VM. Executes one user transaction -- versioned prologue
-> entry function -> versioned epilogue -- single-threaded, returning an
unmaterialized `TxnOutcome`.

Design decisions:

- One session, one checkpoint. No respawned sessions or per-stage change
  sets; a failed payload rolls the heap and read-write set back to the
  post-prologue checkpoint, so prologue effects such as nonce insertion
  survive. Prologue and epilogue run unmetered via a scoped meter swap.
- Deferred materialization. The executor hands back raw VM effects;
  rendering them into a `TransactionOutput` is a separate, fallible step.
  Nothing on the execution path calls into `materialize/`, so a block
  coordinator that reads effects directly can skip it entirely.
- Past the prologue, the transaction always commits and pays. Neither a
  payload VM error nor a misbehaving epilogue discards: the payload's
  effects are dropped, the epilogue reruns against the state the prologue
  left, and only a failing rerun discards -- mirroring
  `failed_transaction_cleanup`.
- Typed outcomes. `DiscardReason`/`ExecutionStatus` make illegal states
  unrepresentable, and aborts carry their own `AbortLocation` so the
  executor records where an abort came from instead of the status mapper
  asserting it afterwards.
- The executor owns no data access. It borrows `ModuleProvider` for code and
  the runtime's `ResourceProvider` for reads; only materialization needs the
  extra resource-group trait. The `StateView`-backed implementations live in
  the sibling `mono-move-aptos-state-view-providers` crate, and Block-STM
  will supply the production ones behind the same traits.

Materialization failures are gathered rather than raised where hit -- the
read-write set has no defined iteration order, so `MaterializationError`
sorts its reasons on construction and reports all of them.

Scope: entry functions only; resource groups work end to end. Gas is
incomplete (uncalibrated units, no IO or storage fees). Scripts, multisig,
publishing, keyless/AA, orderless and argument validation are inline TODOs.
The attacker-reachable gaps -- entry visibility, argument admissibility,
`check_gas` -- are labelled `TODO(security, ..)` and listed in AGENTS.md as
gates that must land before a real coordinator drives this executor; today
the only caller is the e2e harness.

Runtime and core gain what the executor needs: `new_idle()`, `prepare_call`,
`unmetered()`, public `WriteClass`/`serialize_value`, and struct-tag
interning.

Tested by differential e2e against the legacy VM on `FakeExecutor` genesis
-- statuses, write sets and events, masking only the gas-fee slots: p2p
transfer, insufficient balance, fee payer drained (the epilogue retry),
nonexistent entry function, and two dependent transfers applied
sequentially.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The runtime now carries the aborting module on `RuntimeStatus::Aborted`, so
the executor can report where an abort happened instead of reconstructing it
downstream. `location` flows through the whole failure taxonomy --
`CallStatus::Abort` into the prologue, payload and epilogue abort variants,
and out through `ExecutionStatus::Abort`.

Payload aborts no longer report `AbortLocation::Script`. The epilogue's
hardcoded `0x1::transaction_validation` is gone too: the abort supplies its
own location, so the executor constructs none at all.

`prologue_failure_to_status` now guards the validation table on that
location. Previously any prologue abort was matched against it regardless of
which module raised it; an abort from elsewhere is reported as unexpected,
which is also what the legacy VM falls back to. Its `transaction_limits` and
multisig branches are deliberately not ported -- multisig payloads are
rejected before execution and the prologue is always called with no
transaction-limits request, so porting them would duplicate a dozen
constants for unreachable code. Left as a TODO(completeness).

The insufficient-balance differential test was passing only because it
skipped the location; it compares it now, so the payload abort is verified
against the legacy VM's module rather than assumed. Comparing `AbortInfo`
still needs the executor to resolve it from module metadata.

`runtime_error_to_status` still cannot produce `ExecutionFailure`: unlike an
abort, `RuntimeError` carries neither location, function nor code offset, so
that TODO now says it waits on the runtime.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The consensus-visibility warning on `SessionId` moves from the module doc to
the enum itself, where someone adding a variant will see it, and says what
the rule actually requires: a new session kind is a new variant.

`TransactionPayload::script_hash` matches exhaustively instead of falling
through on `_`, so a new executable kind has to be considered here.

The state-view provider's caches use `FxHashMap`; the keys are interned
pointers and state keys with a precomputed hash, so the default SipHash buys
nothing. Its arena doc no longer names `ExternalHeap` -- what matters is that
reads hand out pointers into it.

`AGENTS.md` drops the scope preamble and the interface walkthrough, and the
design-decision prose becomes directions an agent can follow: assume the
latest feature set, do not port legacy paths, do not treat a gas mismatch as
a regression.

The idle interpreter registers gain a TODO for the dangling `func` pointer,
which leans on `is_idle` rather than the `NonNull` invariant. The fix is to
lift long-lived transaction state out so an invocation runs against
`&mut Context` and no idle state exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`ResourceReadWriteSet::writes` became public, exposing an iteration order
that callers must not depend on -- a fact stated only in a doc comment.
`writes_unordered` puts it at every call site instead.

Not `unsafe`, as suggested on review: that marks memory safety, and no
`unsafe` block becomes sound because the caller sorted. Overloading it for
a determinism contract would train readers to write SAFETY comments that
assert nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`VMRegisters::new`/`idle` took a raw `*mut u8` and claimed in a SAFETY
comment that it pointed into an allocation far larger than
`FRAME_METADATA_SIZE` -- something a bare pointer cannot carry. They take
`&MemoryRegion` now, so the one remaining unsafe block sits in
`root_frame_base`, where the length is checked rather than asserted in
prose.

Review suggested inlining the helpers instead; that would put the unsafe
block at all three call sites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`PrologueFailure`, `PayloadFailure` and `EpilogueFailure` described the same
two outcomes -- a Move abort, or a VM error -- so they become one
`MoveExecutionFailure`. The prologue's variant only looked different because
`run_prologue` still stringified its `VMInternalError`; that is fixed too,
the same loss already corrected for the epilogue.

`ExecutionStatus` was a re-encoding of the same pair plus success, so it
becomes `Success` or a failure carrying the `ExecutionStage` it happened in.
That deletes the match in the driver that existed only to translate between
the two shapes, and `DiscardReason` folds its `Prologue`/`Epilogue` variants
into the same pair.

The stage is what removes `RecoveredEpilogueFailure`, which review found
hard to reason about: an epilogue failure had to be pre-wrapped in a
dedicated error type so the status mapper could recover, by downcast, the
fact that it came from the epilogue. Recording the stage says it directly.
The driver no longer decides which epilogue aborts are legitimate --
`executed_vm_status` does, alongside every other status decision -- and a
misbehaving epilogue still maps to `UNEXPECTED_ERROR_FROM_KNOWN_MOVE_FUNCTION`
as the legacy VM reports it.

The stage's epilogue cases are variants rather than a `&'static str`, since
the three runs differ in outcome: only the one after a successful payload
commits.

Doc fixes from the same review: a status other than success is not what
makes a transaction charge the fee (they all do), an abort is not
necessarily the epilogue's balance check, and the note claiming the fee
abort is the only legitimate epilogue failure is gone rather than left to
go stale.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Argument placement took whatever the signature named, so a payload could
pass a reference to a non-signer, an uninstantiated type parameter, or a
function value -- the last of which lets a transaction hand a callee
something like `minter: || Coin`. The legacy VM rejects all three outright
(`transaction_arg_validation`), and now so does this.

Signers must also lead the parameter list, matching the legacy check that
validates only the parameters after the leading signers: `(signer, u64,
signer)` was accepted here and is not valid.

This is a backstop at placement time, not the real validation: the
rejections surface as invariant violations rather than a signature error,
and which structs are constructible from a transaction argument is still
unchecked. Both are recorded where they happen.

The signer reference was assembled byte by byte, which is what raised the
question of endianness. `write_fat_ptr` in core already does this, so the
runtime gains `set_root_ref_arg` to reach it; the pointer is written
directly, so there is no byte order to get wrong. It is `unsafe`, unlike
`writes_unordered`: a dangling target here really is undefined behaviour.

`CallStatus` was a copy of `RuntimeStatus` down to the field names, and only
existed to be converted back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The signer buffers were built twice with different shapes -- sender and fee
payer for validation, sender and secondaries for the payload -- and passed
around as bare slices. `TxnSigners` owns both constructors, so a fee payer
becoming optional or multisig bringing several senders changes one place
rather than every call site.

`call_system_function` and `call_validation_function` gain the `_unmetered`
suffix: that system code does not consume the transaction's gas budget was
stated only in a doc comment, and it is the one thing a reader must not
miss.

Serializing the validation arguments no longer panics. The parameter is any
`impl Serialize`, so the signature promises nothing about it, and the
function already returns a `VMInternalError`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The abort location the prologue table is guarded on was rebuilt, allocating
an `Identifier`, on every discarded transaction.

Nothing in the executor logs. The legacy VM logs at the point of status
conversion, which does not carry over: materialization is skippable, so a
coordinator reading effects directly would never reach it. The TODO sits
where failures are detected instead.

Also records what the differential test does not cover -- the sender's store
is masked, so a wrong debit there would pass -- and that the write-set
drain's collect-then-sort may want revisiting against fail-fast and write
limits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The metadata cache hands out an `Arc` purely to share a deserialized value;
nothing ever takes a `Weak` to it, so the weak count is dead weight.
`aptos-types` already depends on triomphe and uses it in
`block_executor::value`.

Only one of the 41 call sites names the `Arc` type -- the rest deref -- and
that one becomes `as_deref`, which is agnostic to which `Arc` this is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`group_members` hands out a shared handle only so callers can read it past
the provider's `RefCell` borrow; the weak count it carries is never used.
With the module metadata switched over too, `std::sync::Arc` is gone from
both crates.

Also records two deferred cleanups: the group-member map could be
`shared-dsa`'s `UnorderedSet` to make its unordered iteration explicit, and
`nominal_tag` should become a cached method on the context, which would let
the state-view providers stop open-coding the same conversion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Group resolution built a StructTag on every resource read just to name the
defining module; the interned module id is enough. Resource reads also
recomputed pointer offsets to republish a descriptor lowering had already
published, so look that one up instead.
@vgao1996
vgao1996 force-pushed the mono-move-aptos-vm-v2 branch from d44826c to a0af702 Compare August 10, 2026 22:47

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a0af702. Configure here.

location: location.clone(),
code: *code,
message: message.clone(),
}

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.

Epilogue fee abort skips location

Medium Severity

Epilogue can’t-pay-fee handling accepts any abort whose canonical code matches OUT_OF_RANGE + ECANT_PAY_GAS_DEPOSIT, without checking AbortLocation. Prologue mapping in the same file guards on the validation module, and legacy convert_epilogue_error does too. A same-coded abort from another module would be reported as a kept MoveAbort instead of an unexpected framework failure.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a0af702. Configure here.

@github-actions

Copy link
Copy Markdown
Contributor

mono-move benchmark gate

5 regression(s) beyond ±3% noise band

1 ok · 1 improved · 0 new · 0 absent (threshold T = ±3%, criterion mean CI vs main)

Benchmark mean Δ 95% CI median (main → PR) Verdict
fib/mono +0.3% [+0.1%, +0.4%] 6.76ms → 6.78ms ok
nested_loop/mono +5.6% [+5.5%, +5.7%] 10.91ms → 11.52ms regression
merge_sort/mono +8.5% [+8.3%, +8.8%] 1.02ms → 1.11ms regression
bst/mono +12.8% [+12.5%, +13.1%] 3.19ms → 3.61ms regression
match_sum/mono +5.2% [+5.0%, +5.5%] 23.30ms → 24.51ms regression
int_arith_loop/mono_u64 +7.9% [+6.6%, +9.1%] 228.51µs → 247.10µs regression
int_arith_loop/mono_i64 -9.4% [-9.7%, -9.2%] 411.13µs → 372.98µs improved

Improvements are not failures. main rebaselines on merge, so the next PR compares against the faster code automatically.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Forge suite realistic_env_max_load success on a0af70249c38603793643268eb3eecaae47bcab9

two traffics test: inner traffic : committed: 14190.54 txn/s, latency: 1260.16 ms, (p50: 1200 ms, p70: 1300, p90: 1500 ms, p99: 2000 ms), latency samples: 5299460
two traffics test : committed: 100.01 txn/s, latency: 695.09 ms, (p50: 600 ms, p70: 700, p90: 900 ms, p99: 1700 ms), latency samples: 1640
Latency breakdown for phase 0: ["MempoolToBlockCreation: max: 0.482, avg: 0.453", "ConsensusProposalToOrdered: max: 0.115, avg: 0.111", "ConsensusOrderedToCommit: max: 0.162, avg: 0.152", "ConsensusProposalToCommit: max: 0.275, avg: 0.263"]
Max non-epoch-change gap was: 0 rounds at version 0 (avg 0.00) [limit 4], 0.50s no progress at version 4771529 (avg 0.06s) [limit 15].
Max epoch-change gap was: 0 rounds at version 0 (avg 0.00) [limit 4], 0.59s no progress at version 2828840 (avg 0.59s) [limit 16].
Test Ok

@github-actions

Copy link
Copy Markdown
Contributor

✅ Forge suite compat success on ab95480b1d56078238d2eb01e846fcbd8e5f9f80 ==> a0af70249c38603793643268eb3eecaae47bcab9

Compatibility test results for ab95480b1d56078238d2eb01e846fcbd8e5f9f80 ==> a0af70249c38603793643268eb3eecaae47bcab9 (PR)
1. Check liveness of validators at old version: ab95480b1d56078238d2eb01e846fcbd8e5f9f80
compatibility::simple-validator-upgrade::liveness-check : committed: 15306.41 txn/s, latency: 2248.82 ms, (p50: 2200 ms, p70: 2400, p90: 3100 ms, p99: 4000 ms), latency samples: 496940
2. Upgrading first Validator to new version: a0af70249c38603793643268eb3eecaae47bcab9
compatibility::simple-validator-upgrade::single-validator-upgrade : committed: 6142.60 txn/s, latency: 5530.45 ms, (p50: 6100 ms, p70: 6200, p90: 6300 ms, p99: 6700 ms), latency samples: 213120
3. Upgrading rest of first batch to new version: a0af70249c38603793643268eb3eecaae47bcab9
compatibility::simple-validator-upgrade::half-validator-upgrade : committed: 6212.91 txn/s, latency: 5456.53 ms, (p50: 6000 ms, p70: 6200, p90: 6200 ms, p99: 6300 ms), latency samples: 218280
4. upgrading second batch to new version: a0af70249c38603793643268eb3eecaae47bcab9
compatibility::simple-validator-upgrade::rest-validator-upgrade : committed: 9968.93 txn/s, latency: 3381.60 ms, (p50: 3600 ms, p70: 3700, p90: 3900 ms, p99: 4100 ms), latency samples: 325540
5. check swarm health
Compatibility test for ab95480b1d56078238d2eb01e846fcbd8e5f9f80 ==> a0af70249c38603793643268eb3eecaae47bcab9 passed
Test Ok

@github-actions

Copy link
Copy Markdown
Contributor

✅ Forge suite framework_upgrade success on ab95480b1d56078238d2eb01e846fcbd8e5f9f80 ==> a0af70249c38603793643268eb3eecaae47bcab9

Compatibility test results for ab95480b1d56078238d2eb01e846fcbd8e5f9f80 ==> a0af70249c38603793643268eb3eecaae47bcab9 (PR)
Upgrade the nodes to version: a0af70249c38603793643268eb3eecaae47bcab9
framework_upgrade::framework-upgrade::full-framework-upgrade : committed: 2393.06 txn/s, submitted: 2400.95 txn/s, failed submission: 7.89 txn/s, expired: 7.89 txn/s, latency: 1158.74 ms, (p50: 1100 ms, p70: 1200, p90: 1700 ms, p99: 2300 ms), latency samples: 218443
framework_upgrade::framework-upgrade::full-framework-upgrade : committed: 2322.02 txn/s, submitted: 2329.52 txn/s, failed submission: 7.50 txn/s, expired: 7.50 txn/s, latency: 1206.22 ms, (p50: 1200 ms, p70: 1200, p90: 1800 ms, p99: 2600 ms), latency samples: 210442
5. check swarm health
Compatibility test for ab95480b1d56078238d2eb01e846fcbd8e5f9f80 ==> a0af70249c38603793643268eb3eecaae47bcab9 passed
Upgrade the remaining nodes to version: a0af70249c38603793643268eb3eecaae47bcab9
framework_upgrade::framework-upgrade::full-framework-upgrade : committed: 2309.37 txn/s, submitted: 2318.02 txn/s, failed submission: 8.65 txn/s, expired: 8.65 txn/s, latency: 1236.86 ms, (p50: 1200 ms, p70: 1400, p90: 1700 ms, p99: 2700 ms), latency samples: 208281
Test Ok

@vgao1996
vgao1996 merged commit fd5c21e into main Aug 10, 2026
@vgao1996
vgao1996 deleted the mono-move-aptos-vm-v2 branch August 10, 2026 23:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants