Overview
This page is the reference card for what the Python Virtual Machine (PVM) accepts and enforces: the static checks applied to actor code, which modules an actor may import, the runtime determinism guards, and the limits the continuation compiler imposes. For the why, see Determinism & Sandbox; for gas limits and pricing, see Resource Limits and the fee model.
Static validation
Actor code is validated before execution. A violation rejects the code with a validation error:
| # | Rule | Detail |
|---|
| 1 | Valid UTF-8 | Code bytes must decode as UTF-8 |
| 2 | No unbounded asyncio concurrency | asyncio.gather(, asyncio.wait(, asyncio.wait_for(, asyncio.as_completed( are rejected as source patterns |
| 3 | Integer digit runs | No run of more than 1,234 consecutive decimal digits anywhere in the source — including inside strings and comments (the source-level form of the 4096-bit integer cap; hex literals are not covered by this scan) |
Module imports
The PVM enforces imports with a whitelist, with a blacklist veto checked first. Importing anything outside the allowlist raises NonDeterministicError: module not allowed (older conceptual pages describe this as an ImportError; the implementation raises the dedicated error type).
Hard-banned (blacklist veto):
pickle datetime os ctypes _ctypes cffi _cffi_backend
Redirected: import time and import random never resolve to the stdlib modules — the import guard aliases them to the SDK’s deterministic replacements before the deny check. Don’t rely on the redirect: CIP-6 §17 forbids import time / import random in actor code; use the supported surface instead — runtime.get_block_height() / runtime.get_timestamp_ms() for time, and runtime.randomness(domain) for randomness.
Commonly used stdlib modules that are importable:
| Domain | Modules |
|---|
| Data & encoding | json, struct, base64, binascii, codecs, unicodedata |
| Hashing | hashlib, hmac |
| Text | re, string |
| Collections & functional | collections, collections.abc, heapq, bisect, functools, itertools, operator |
| Typing & classes | typing, abc, enum, dataclasses, types |
| Math | math (executes on deterministic software floats — see below) |
The authoritative allowlist lives in the PVM’s import guard; if a module isn’t importable, treat that as the protocol’s answer rather than working around it. The SDK itself (cowboy_sdk) and the host module (pvm_host) are always importable.
Runtime guards
These are enforced during execution regardless of what static validation saw:
| Guard | Behavior |
|---|
| Integer size | builtins.int is replaced with a guarded subclass: arithmetic on guarded values (+ - * ** << and their reflected forms) producing a result wider than 4,096 bits raises OverflowError. isinstance(x, int) still works normally. Known gap: expressions over bare literals (e.g. 2 ** 10000) bypass the Python-level guard — the source-level digit-run check above catches the common case, and full VM-level enforcement is tracked as a separate work item |
| File I/O | open() (and io.open) is replaced and raises — actors cannot touch a filesystem |
| Floating point | All float arithmetic runs on a software IEEE-754 implementation (SoftFloat) — bit-identical results on every architecture; no native FPU, no JIT |
| Hash seed | PYTHONHASHSEED is pinned to 0, so dict/set hashing is reproducible across validators |
| Locale | Locale is pinned; setlocale accepts only C.UTF-8, C, or POSIX and raises on anything else |
CIP-6 §17 requires cowboy_sdk.ordered_set in place of set() / frozenset(). The VM does not currently block the set builtin, but treat the spec rule as binding — hash-based iteration order is exactly the kind of implicit nondeterminism these rules exist to keep out of actor logic.
Continuation limits
Functions decorated with @runner.continuation (or @actor.continuation) are compiled to state machines at import time. The compiler statically rejects:
| Limit | Error |
|---|
More than 8 sequential await points per function | ContinuationLimitError |
await inside a nested function definition | ContinuationLimitError |
yield / yield from (generators are not serializable) | DeterminismError |
await inside a for/while loop without @bounded_loop | ContinuationLimitError |
Recursive await of the continuation itself | ContinuationLimitError |
@bounded_loop(max_iterations=N) (default 1,000) establishes a per-call iteration budget; the loop body must count against it by iterating with BoundedRange / check_iteration(), and exceeding the budget raises LoopBoundExceeded — a plain for loop under the decorator is not counted automatically. Variables needed after an await must be declared via capture() (CIP-6 §10).
Execution caps
- Dual gas meters — every execution is metered in Cycles (compute) and Cells (data); exhausting either aborts with out-of-gas. See Resource Limits.
- Sub-limits — certain invocations run under caps below the transaction budget; for example the current implementation holds CIP-20 transfer hooks to 50,000 Cycles / 50,000 Cells per call.
- Storage reads — reads are tracked during execution and settled to the meters afterward; a read-heavy handler can run out of gas at settlement.
- Synchronous call depth — cross-actor
call() chains are capped at 32.
Where the rules live
| Layer | Source (monorepo) |
|---|
| Static validation | node/execution/src/pvm_executor.rs |
| Import guard & runtime guards | node/pvm/crates/pvm-runtime/ |
| Continuation compiler | node/pvm/Lib/cowboy_sdk/_compiler.py |
| Normative spec | CIP-6 §10, §17 and CIP-3 |