[mono-move] Add the MonoMove Aptos transaction executor - #20181
Conversation
c2d1b0b to
42693dd
Compare
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>
9ec9bc5 to
7156378
Compare
| entry: &EntryFunction, | ||
| ty_args: InternedTypeList, | ||
| ) -> Result<(), PayloadFailure> { | ||
| // TODO(completeness): entry-function validation — `entry` visibility, |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
| 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)? { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
|
|
||
| /// 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>>>; |
There was a problem hiding this comment.
Btw, for block-stm we actually want an API like this:
- we already have an efficient way to process groups (so VM callingborrow_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.
There was a problem hiding this comment.
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.
| - **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 |
There was a problem hiding this comment.
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?
vgao1996
left a comment
There was a problem hiding this comment.
@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:
- The write set materialization protocol
- Resource group caching and handling
|
|
||
| /// 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>>>; |
There was a problem hiding this comment.
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.
| 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)? { |
There was a problem hiding this comment.
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.
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>
c0e6feb to
cec6322
Compare
c3f8df7 to
faf0661
Compare
There was a problem hiding this comment.
@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
left a comment
There was a problem hiding this comment.
2/n calls.rs + sys_calls.rs
vgao1996
left a comment
There was a problem hiding this comment.
3/n a bunch of smaller issues (e2e, vm_status, txn_output).
What's left: the providers.
7918e0a to
d44826c
Compare
`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.
d44826c to
a0af702
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ 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(), | ||
| } |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit a0af702. Configure here.
mono-move benchmark gate5 regression(s) beyond ±3% noise band
|
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
✅ Forge suite
|
✅ Forge suite
|
✅ Forge suite
|


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 unmaterializedTxnOutcome. Rebased onto currentmain, so #20242, #20254 and #20289 are all in.Design choices:
TransactionOutputis a separate, fallible step (materialize()), and the legacyVMStatusmapping is quarantined next to it. The read-write set has no defined iteration order, so the drain gathers every failure it hits andMaterializationErrorsorts them on construction — what gets reported cannot depend on that order.DiscardReason/ExecutionStatus, both carrying aMoveExecutionFailureand theExecutionStageit happened in — illegal states unrepresentable;VMStatusconstructed only at the projection edge, which is also where a failure's legitimacy is judged. Aborts carry theAbortLocationthe 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.failed_transaction_cleanup, and it keeps the executor's control flow out of the legacy status projection — nothing on the execution path calls intomaterialize/, so that step is genuinely skippable for Block-STM.ModuleProvider(code) and the runtime'sResourceProvider(reads); materialization needs one extra trait for resource groups. Block-STM later supplies production implementations behind the same traits; theStateView-backed ones (for tests, simulation, replay) live in a separate crate.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-typesandaptos-vm)SessionIdand the prologue/epilogue argument enums were private toaptos-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 inaptos-typesand 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 mirrortransaction_validation.move. And every VM rolled its own decoder for Move's abort-code convention, even thoughaptos_types::erroralready hadcanonical()to build a code and the correctly-named categories;split_canonicalis the missing inverse. That retires a misnomer along the way:aptos-vmcalled category0x2LIMIT_EXCEEDED, whicherror.movedoes not define -- it isOUT_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 aTODO(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 theTxn/OrderlessTxnhashes to guard that. Its fourTransactionMetadata-taking constructors became primitive-taking ones, with convenience methods onTransactionMetadataitself;move_vm_extstill re-exportsSessionId, 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):executor.rscalls.rs,sys_calls.rserrors.rs,outcome.rsTxnOutcomematerialize/txn_output.rsrenders effects into aTransactionOutput;vm_status.rsprojects ontoVMStatusproviders.rsAptosDataProvidertrait (resource groups)aptos-state-view-providers/StateView-backed data layers for sequential executionnew_idle(),invoke→prepare_call,unmetered(),WriteClass/serialize_valuepub, tag interning in coreOpen questions for reviewers
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_idleskipsverify_function; nocheck_gaspreflight. 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 labelledTODO(security, ..)and listed inAGENTS.mdas 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
FakeExecutorgenesis (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-typesandaptos-vmunit suites pass; fmt/clippy clean. TheSessionIdmove 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 unmaterializedTxnOutcomeplus optionalmaterialize()toTransactionOutput/VMStatus.mono-move-aptos-state-view-providersimplements module/resource reads (including resource groups) fromStateViewfor tests and sequential tools.Consensus-visible sharing:
SessionId,PrologueArgs/EpilogueArgs, and prologue abort codes move intoaptos-types;split_canonicalreplaces per-VM abort decoding (includingOUT_OF_RANGEfor sequence-too-big / can’t-pay-fee cases).aptos-vmdrops its localsession_idmodule and usesTransactionMetadatasession-id helpers andTransactionPayload::script_hash().MonoMove runtime/core:
InterpreterContext::new_idle,prepare_call,unmetered, signer ref args, public write-set draining (WriteClass,serialize_value), andintern_type_tag/ struct-tag interning; descriptor lookup for materialization.Differential e2e tests compare legacy AptosVM vs the new executor on
FakeExecutorgenesis (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.