Architecture · Explanation
Move modules, resources, and calls
Singularity embeds a Move bytecode VM with an account-state adapter. It supports module verification, publication, entry execution, and resource storage. It does not supply an Aptos or Sui execution environment: framework packages, signer construction, transaction formats, gas schedules, and deployment tooling need separate validation.
For example, a module taking a BCS u64 can be invoked through the implemented entry interface. A function taking signer cannot receive an arbitrary 32-byte argument and treat it as authenticated authority. That distinction is central to this adapter's compatibility boundary.
Use the compatibility matrix to scope an adoption decision; this explanation traces the storage, authority and call boundaries behind those limits.
Storage preserves Move identities
Move addresses and chain public keys are 32 bytes, but a module or resource is stored at a derived key:
| Object | Account key | ||||
|---|---|---|---|---|---|
| Module | `SHA256("move:module:" | address | module_name)` | ||
| Resource | `SHA256("move:resource:" | address | bcs(StructTag))` | ||
| Module enumeration index | `SHA256("move:module_index:" | address)` | |||
| Resource enumeration index | `SHA256("move:resource_index:" | address)` |
Module accounts are executable; resource accounts are not. Both are owned by MOVE_PROGRAM_ID, whose bytes are 31 0xff bytes followed by 0x02. Full resource tags distinguish type parameters as well as address, module, and struct name. Storage mapping.
Reverse indexes make enumeration possible without reversing hashes or scanning every account. They are query aids: historical modules are not automatically backfilled into the module index, and an empty enumeration is not sufficient proof that no module exists.
The adapter reads its dirty map before the supplied state view and buffers writes/deletions until changeset extraction. A session can therefore read its own writes while the outer transaction still controls publication. Dropping an unsuccessful adapter discards those effects.
Publish only under an authenticated address
The native publication envelope is:
0xff || serialized Move module bytecodeThe consensus dispatch requires the module's self-address to be one of the transaction signers. The standalone MoveExecutor::deploy_module helper does not provide that outer authentication on its own. This prevents publishing a module under someone else's address through a conveniently sponsored transaction. Publish authorization.
Publication deserializes the module, precharges verification at five compute units per byte, verifies bytecode safety, and publishes through a VM session. Only a successful session finish is mapped into the adapter. The verifier also checks elapsed time against 500 ms after verification returns; this is not a preemptive timeout that interrupts the verifier. The direct native result currently reports fixed usage rather than an ecosystem-compatible Move gas charge. Publication implementation.
The convenience eto_deployMove RPC constructs a faucet-sponsored transaction when sponsorship is enabled. It cannot substitute that payer for an arbitrary module self-address signature. Use an appropriately signed native transaction and establish module dependencies first. A framework import is not fulfilled merely because the VM understands Move bytecode.
Choose the right call encoding
There are two materially different routes:
| Route | Encoding and capability | ||||||
|---|---|---|---|---|---|---|---|
| Direct native Move instruction | `address[32] | function_name_length[u8] | function_name | argument_bytes`; empty remainder means zero arguments, otherwise one BCS blob; no type arguments | |||
| Cross-VM/workflow Move call | Target is the 32-byte module address; selector is NUL-separated module\0function[\0type_tag]*; arguments are a counted array of length-prefixed BCS blobs |
The legacy direct route uses the function name as the module name too. It is therefore unsuitable for a general Module::different_function invocation. The workflow codec separates these names and carries multiple arguments and type arguments. Its integer framing is little-endian and rejects malformed/trailing data. Direct route, workflow route.
The RPC shape for the fuller route is:
eto_moveExecute([addressHex32, moduleName, funcName, tyArgs[], argsHex[]])Each argument is separately BCS encoded. For a single u64 value of seven, the argument is 0x0700000000000000; that example describes serialization only, not a deployed module or a completed live call. RPC sponsorship and deployment availability still apply. The RPC reference explains transport errors and submission results.
Raw bytes cannot manufacture capabilities
Before an entry session begins, the executor resolves the function signature and accepts only constant-safe value types: booleans, integers, addresses, and recursively safe vectors. It rejects raw signer, references, structs/resources, and generic value parameters until an authenticated construction ABI exists.
A correct BCS layout does not make an input authorized. In particular, bytes shaped like a one-field Coin must not manufacture a linear resource. A generic function may still work when its actual value parameters satisfy the permitted policy. Argument policy.
Successful entry execution is followed by session finish and changeset application. An ordinary abort or out-of-gas result returns a failure without applying the session's effects. Host invariant and state-read failures propagate as defects rather than being relabeled as a deterministic guest abort.
Native functions are explicit and direction-dependent
Top-level Move dispatch creates a fresh native table with the current dispatcher, signer set, height, depth, and gas ceiling. Its registered outbound call is:
native public fun call_evm(
target: vector<u8>,
calldata: vector<u8>,
value: u64,
): vector<u8>;The declaration belongs in dispatch at the 32-byte address consisting entirely of 0x0c. Publishing that declaration module remains necessary; a Rust native table is not an on-chain module.
The EVM target is 20 bytes. Caller identity is derived from the immediate calling Move module's low 20 address bytes; value authorization is separately tied to the outer signer set. A value-bearing call with no authorized signer aborts. The boundary reserves 1,000 internal gas units, then caps the child against the remaining Move budget. Consumed child work is reflected in the local meter while outer accounting avoids charging the same shared-context work twice. Native table and call.
The table also registers HILSA's read-only chain_head, interop_snapshot, and deposit_processed functions through the universal-read implementation. Bridge withdrawal/outbox helper code is not equivalent to registered callable natives. Look up the exact arguments and result encodings in HILSA VM interfaces.
Inbound cross-VM execution constructs a Move executor without this native table. Consequently, support for top-level Move → EVM does not imply support for arbitrary other-VM → Move → EVM recursion. Inbound construction. See composition for the common journal and depth boundary.
Views and events have different lifetimes
view_function bypasses entry visibility, returns separate BCS return blobs, and drops the session without finish/apply. Callers must know each return layout. Cross-VM successful returns are flattened into one byte output, so that interface does not preserve arbitrary multiple-return boundaries.
eto_moveSimulate accepts the same five-part parameter shape as a view and returns {success, gasUsed, returnValues, error}. Its adapter is discarded. However, the current RPC invokes execute_function, which can record events into the process-wide event store. This differs from the executor's event-suppressing simulation helper.
Treat eto_getMoveEvents as server-local telemetry that resets on restart, not a durable committed-event index. An event observed there alone is not proof of a committed Move transaction. The transaction-aware canonical path defers events in the shared journal so they can follow rollback. Simulation and events, session event handling.
Validate the boundary you need
State-adapter tests cover key separation, dirty reads, indexes, deletion, and guarded merge. Executor tests cover module round trips, BCS arguments, and rejection of forged signer/resource inputs. Native tests cover caller identity, authority, depth, child budget, and failure translation.
The ignored GPU fixture wasm_and_move_share_one_cuevm_journal_commit_and_rollback in composition_kernel_cuevm exercises shared state and rollback with CuEVM. These tests were inspected, not executed during this documentation run. Use validation strategy to select a focused existing artifact or test configuration.
For adoption, validate the module's imports, parameter types, native table, invocation route, and event requirements together. A successful bytecode verification alone establishes only one of those requirements.