Status: Draft
Type: Standards Track
Category: SDK
Created: 2025-10-20
Requires: CIP-1 (Actor Message Scheduler), CIP-2 (Off-Chain Compute), CIP-3 (Fee Model), CIP-5 (Timers)
Type: Standards Track
Category: SDK
Created: 2025-10-20
Requires: CIP-1 (Actor Message Scheduler), CIP-2 (Off-Chain Compute), CIP-3 (Fee Model), CIP-5 (Timers)
1. Abstract
This CIP specifiescowboy_sdk, the Python SDK embedded in the Cowboy PVM and imported by actor code executing on-chain. It is normative for the PVM’s actor model, call primitives, handler permissions, continuation semantics, type system, verification builder, error hierarchy, ownership runtime, and the cowboy CLI surface.
There are two Python-facing layers for Cowboy development. This CIP is normative for cowboy_sdk only. The standalone installable package (cowboy-sdk, imported as cowboy) is described in §2 and is non-normative for this CIP.
Any Cowboy node that runs Python actors MUST expose the module layout and syscalls defined here. Any CLI that names itself cowboy SHOULD implement the subcommands in §16 with compatible flag semantics.
Key properties:
- Deterministic by construction. Non-deterministic Python primitives (wall-clock time, hardware FPU, unseeded randomness, filesystem, network,
set(),pickle) are trapped or replaced by SDK equivalents. - Two-dimensional metering. Every syscall is metered in Cycles (compute) or Cells (state IO) per CIP-3.
- Handler-mode purity. Every actor method declares whether it may issue async side effects (
@purevs@deferred); the runtime enforces this distinction. - Deny-by-default access control. Handlers are unreachable from external callers unless explicitly decorated with
@publicor@callable_by(...). - Continuations compiled at decoration time.
asyncmethods decorated with@runner.continuationor@actor.continuationare lowered to a finite-state machine at class-load time, not dynamically at runtime. - Entitlements gate all external-effect syscalls. Entitlements are declared in a deploy-time manifest and enforced at the Host API boundary.
2. SDK Layers
There are two Python-facing packages for Cowboy development. They are distinct and serve different purposes.2.1 cowboy_sdk — In-PVM Actor SDK (this CIP)
Actor code that will run on-chain imports from
cowboy_sdk. This is the package this CIP specifies.
2.2 cowboy — Standalone Developer SDK
The standalone package provides:
- Actor authoring helpers —
@actor,@public,@callable_by,@pure,@deferred,@init_handler,OWNER,SELF, localruntimestubs for unit testing - Source generation —
actor_instance.to_source()emits deployable Python source that imports fromcowboy_sdk - Chain client —
CowboyClient,AsyncCowboyClientfor RPC queries and transactions - Deployment —
client.deploy_actor(code, salt, init_handler, ...),client.execute_actor(...) - Wallets and signing —
Wallet,generate_private_key,sign_payload - Jobs —
JobSpec,client.submit_job() - CBSS — secrets management helpers
cowboy local SDK mirrors enough of the cowboy_sdk actor authoring API to write and unit-test actors locally. It does not implement continuations, ActorRef, runner, Verify, CowboyModel, SoftFloat, ordered_set, BlockHeight, or storage raw/guard helpers. Actors that use those features must be tested against a live PVM sandbox.
2.3 Handler Signature Difference
The most important practical difference for actor authors:to_source() generates module-level wrappers with the payload: bytes signature. Actors deployed via to_source() therefore receive the payload bytes verbatim on-chain — canonical CBOR when the caller used the default codec — and decoding is the author’s responsibility. The payload codec is a wire-format concern for call() and send() — see §9.5 and §11.5.
2.4 Compatibility Matrix
3. Motivation
Python is the surface that actor authors touch most often. For the protocol to be interoperable across implementations and for actors to remain portable across node versions, the SDK contract must be stable and normatively specified — not left as implementation-defined. A prior revision of this CIP was drafted before significant SDK consolidation landed (handler-mode purity, runtime module, error-code hierarchy, CREATE2 address derivation, entitlement manifest format, permissions system). This revision rebuilds against the shipping code and clarifies the two-package architecture so that the spec tracks implementation reality. Subsequent revisions will extend the SDK for CIP-9 volume mounts (currently runner-side only) and CIP-7 / CIP-17 streaming helpers. Both are noted as gaps in §19.4. Definitions
- Actor: A Python class whose public methods are invocable handlers. Identified by a 20-byte address.
- Handler: A public method of an Actor. Accepts the payload the host delivers; returns
bytes, or a value its declared codec (raw/cbor/json, §9.5) can encode, or raises. - Handler mode: A per-method property, either
pure(default) ordeferred. Pure handlers MUST NOT issue async side effects. - Permission: A per-method access control tag (
@public,@callable_by, or absent). Absent means deny-by-default from external callers. - Syscall: A call from the PVM into the Host API (implemented in Rust) to perform a privileged operation. The SDK wraps syscalls.
- Continuation: A compiled FSM representation of an
async defhandler thatawaits a runner job or an async actor call. State survives block boundaries. - Manifest: A JSON document declaring the entitlements a deployed actor requests. Attached to the deploy transaction.
- PVM: The Python Virtual Machine executing actor bytecode under determinism constraints (CIP-3 §4).
5. SDK Module Layout
The canonical package iscowboy_sdk, shipped with the PVM at node/pvm/Lib/cowboy_sdk/. Version 0.1.1 targets Python ≥ 3.11.
Required actor-facing API — every symbol below MUST be importable from the cowboy_sdk package root. An implementation MUST NOT remove, rename, or change the semantics of any of these:
Currently exported helpers and extension surfaces — present in the shipping implementation but not mandated by this CIP. An implementation MAY omit or rename these; actor authors SHOULD prefer the required API above:
Private submodules (
cowboy_sdk.codec, cowboy_sdk.pvm_sys, cowboy_sdk.pvm_time, cowboy_sdk.pvm_random) are implementation details and SHOULD NOT be imported by actor code. They MAY be imported by SDK extensions that understand the PVM boundary.
6. Actor Model
6.1 Declaration
An Actor is a Python class decorated with@actor. The examples below use cowboy_sdk import style (on-chain). The standalone cowboy package uses the same decorator names but requires (self, payload: bytes) handler signatures — see §2.3.
@actor decorator:
- Injects
self.address(read-onlyAddress, the actor’s own 20-byte address).cowboy_sdkonly — the standalonecowboySDK does not injectself.address. - Injects
self.storage(a dict-like proxy over actor private state — see §6.5). - Wraps every public method in permission enforcement (deny-by-default; see §7) and handler-mode enforcement (see §8).
- Scans for
@runner.continuationand@actor.continuationmethods and installs the corresponding__resumecallbacks.cowboy_sdkonly. - Registers the class as the handler entry point for the deployed actor.
Actor is accepted without the decorator depends on the PVM loader’s module-scanning logic, which is non-normative at this layer; actor authors SHOULD always use @actor.
6.2 Address Derivation
Actor addresses are derived via Ethereum-style CREATE2:deployer_address: 20 bytes, the sender of the deploy transaction.salt: caller-supplied bytes, keccak256-reduced to 32 bytes for CREATE2 —derive_actor_addresshashes the salt unconditionally; it is not zero-padded (keccak256(salt) ≠ zero_pad(salt)for any non-trivial salt).code_hash: 32-byte hash of the actor source. The CLI’scowboy actor addressis authoritative for the exact hash algorithm used.- Output: 20-byte
Address.
derive_actor_address(deployer, salt, code_hash) computes this locally. The CLI helper cowboy actor address computes it without submitting a transaction (§16).
6.3 Deployment
Wire fields (DeployActor transaction):
CLI defaults (applied by
cowboy actor deploy before building the transaction):
Atomic initialization:
- By default the CLI calls
"init"atomically in the same transaction as the deploy, withsenderset to the deployer’s address. - Pass
--no-initto deploy without an init call (only safe for actors that do not rely oninitfor ownership bootstrapping). - Pass
--init-handler <name>to name a different handler. - Pass
--init-payload <json-string | @file>to supply a custom payload.
DeployActor transaction reverts if init_handler raises.
The deployment receipt carries the derived actor address.
Note on __init__: Previous revisions of this CIP described deployment as invoking __init__ with constructor args. This is incorrect. __init__ may be used for local Python class initialization (without args), but the deploy-time initializer is always a handler named via init_handler, not the Python constructor.
6.4 Message Handler Dispatch
A message to an actor carries amethod_name (UTF-8 string) and payload (raw bytes). Dispatch:
- The PVM loads the actor class and state.
method_nameis looked up; unknown methods raiseAttributeError— converted toActorCallError.- The handler runs under its declared permission (§7) and handler mode (§8).
- Return value is encoded with the handler’s declared codec — passed through unchanged under
raw, the default (§9.5) — and returned to the caller (forcall()) or discarded (forsend()).
to_source() receive payload as undecoded bytes in each module-level wrapper — canonical CBOR when the caller used the default codec. The author is responsible for decoding (e.g. cbor2.loads(payload)).
The reserved handler on_timer(msg) receives timer fires (CIP-5). Additional reserved handlers MAY be introduced by future CIPs.
6.5 Storage
Actor state is a private key-value store scoped to the actor’s address. Access is exclusively throughself.storage:
cowboy_sdk only):
cowboy_sdk only):
self.storage[key] MUST be CBOR-encodable per §11.5.
Storage writes and reads are metered in Cells per CIP-3. Cross-actor storage access is prohibited; actor B cannot read actor A’s storage directly — it must call a method on A.
A small set of dunder-style keys (__OWNER__, __INITIALIZED__) is reserved by the SDK. All access methods raise ValueError for those keys; use the sanctioned runtime APIs instead (see §12.10).
7. Handler Permissions
Access control is deny-by-default: a non-underscore-prefixed handler with no permission decorator is unreachable from external callers. This applies in bothcowboy_sdk and the standalone cowboy package.
7.1 Decorators
Both decorators also take
decode= to select the handler’s payload codec — "raw", "cbor", or "json", as in @public(decode="cbor") or @callable_by(OWNER, decode="json"). Declare it on every exposed handler: omitting it takes a default that is scheduled to change, and §9.5 explains what that costs.
7.2 Internal Helpers
Convention: name internal helpers with a leading underscore. The@actor wrap pass skips underscore-prefixed names unless they carry an explicit @public / @callable_by tag, so self._helper(payload) runs without a permission check. A non-underscore method without a permission decorator is still treated as a handler and will raise PermissionDeniedError when reached from an external caller.
7.3 @callable_by(SELF) Semantics
SELF resolves via caller == self.address — i.e., the host has actively set sender to the actor’s own address. This fires for timer self-fires and explicit self-submission flows. It does not fire for ordinary self.method() calls inside a handler, because runtime.get_sender() returns the outermost caller address (the EOA or upstream actor), not the executing actor’s address.
7.4 init Bootstrap Window
A handler named init is implicitly callable by anyone while the actor has not yet recorded successful initialization (the __INITIALIZED__ reserved slot is unset). The SDK sets this flag automatically after init returns without raising. After that, init follows the same deny-by-default rules as every other handler.
The deploy machinery runs init in the same transaction as the deploy with sender = deployer, eliminating the front-run window.
8. Handler Modes
Every handler runs under one of two modes, declared via decorator:
A pure handler MUST NOT issue any operation that produces an async effect:
send(), runtime.schedule_timer(), runtime.submit_job(), emitting callbacks. Synchronous call() and local computation are allowed. Any prohibited operation inside a pure handler raises PurityViolationError.
A deferred handler MAY issue any SDK operation subject to entitlement gates.
8.1 Read-Only Execution Flag
Independent of the handler-mode distinction in §8 above, the runtime carries a per-callread_only flag on the execution context — a Boolean that the validator sets when a handler is invoked through a non-state-mutating entry point (notably the validator’s POST /actor/read RPC and analogous external query paths). The flag is part of the normative runtime surface and MUST be honored uniformly across implementations.
Semantics. When read_only is set:
- Every state-mutating host syscall MUST return
HostError::Forbiddeninstead of recording the mutation. The mutating syscalls — exhaustive list — are:state_set,state_delete,emit_event,send_message,schedule_timerandcancel_timer(all variants),submit_job,create_deferred_tx,upgrade_self, everytoken_*mutation,fork(CIP-27), and any future host call that writes durable state. - The flag MUST persist across
call()sub-calls: a read-only top-level call cannot launder a mutation through a callee. The callee inheritsread_only = truefor the duration of the cross-call. - Read syscalls (
state_get,state_scan_prefix, etc.) and pure computation are unaffected. send_messageis on the deny list because, although fire-and-forget, it is a durable effect (CIP-1).
@pure / @deferred. The handler-mode decorator declares the author’s intent for whether a handler issues async effects; read_only is the runtime decision about whether to permit them on a given call. They are independent:
@pure+read_only=false: ordinary external view-style call. Async effects would already be rejected by@pureenforcement.@pure+read_only=true: typical RPC query path. Belt-and-braces.@deferred+read_only=false: ordinary mutating call.@deferred+read_only=true: the handler could issue async effects, but the runtime is rejecting them at the host boundary. This is the case other CIPs (e.g. CIP-27 §3.1) need to name when they require their syscalls to be inert under read-only.
9. Call Primitives
Three primitives for inter-actor and off-chain interaction:
Standalone
cowboy SDK note: Top-level call() and send() are not exported by the standalone package. For local tests use runtime.call_actor(target, method, payload, cycles_limit) and runtime.send_message(target, payload) respectively.
Related primitives that move state rather than invoke handlers: runtime.transfer_balance (native CBY, §12.12) and runtime.token_transfer (CIP-20 tokens, §12.4) are ledger-level effects, not handler invocations. They share call()’s same-transaction rollback semantics but invoke no recipient code. Use them when funds need to move and the receiving address is not expected to react.
9.1 call() — Synchronous Cross-Actor Call
target: 20-byte address, asAddress, hex string, or raw bytes.method: UTF-8 handler name on the target.args: the arguments, encoded with the call’scodec— a dict of keyword arguments or a list of positional arguments under an encoded codec (cbor/json), orbytessent verbatim underraw.cycles_limit: explicit cycle budget for the callee. Defaults to100_000if omitted; passing it explicitly is recommended for precise metering.codec: the payload codec for both the arguments and the return —raw,cbor, orjson, defaulting tocbor. It must match thedecode=the target handler declares — see §9.5.
- Executes in the same transaction. Shared read-write set with the caller.
- Callee exceptions propagate — uncaught, they roll back the entire transaction.
- Call depth is capped at 32. On-chain, the host rejects an overflow as
Forbidden(E1205) andcall()surfaces it asActorCallError, indistinguishable from a permission or re-entrancy reject.CallDepthExceededis raised only when an in-process (local/checkpoint-mode) chain overflows Python’s recursion limit. Below the call-error shape activation height the legacy classification applies, under which an on-chain overflow does raiseCallDepthExceeded. - Return value is decoded with the call’s codec and returned to the caller.
ActorRef wrapper provides syntactic sugar, and takes the same selector:
9.2 send() — Fire-and-Forget Message
- Enqueues a message for delivery at the start of the next block.
- No return value;
send()returns immediately. - Irrevocable: once a transaction commits, its sent messages are delivered even if later logic would want to cancel them.
- Allowed only from
@deferredhandlers. payloadis encoded with the send’scodec—raw,cbor, orjson, defaulting tocbor(§9.5).send(raw=True)is the original spelling ofcodec="raw"and stays equivalent to it; naming any other codec alongsideraw=TrueraisesValueErrorrather than letting one silently win.
send(). Raising after a send() does not unsend the message.
9.3 await runner.<op> — Runner Continuation
Decorated async def methods can await off-chain operations. cowboy_sdk (PVM) only — not available in the standalone cowboy package.
runner:
9.3.1 runner.http response status and headers
By default the data of an HTTP job is the response body only: the parsed JSON value for an application/json response, {"raw": <base64>, "content_type": <type>} otherwise, or {"result": <value>} when the job sets an extraction selector. The response status and headers do not reach the actor.
An actor that needs them sets the request header X-Cowboy-Return-Headers to a comma-separated list of response header names, matched case-insensitively. The Runner does not send this header to the destination, and data becomes:
- The inner
datais exactly whatdatais without the control header. headersholds only the requested headers that the response carried, keyed in lowercase. Each is an array of its values in the order the server sent them. A value is a string when its bytes are valid UTF-8, and{"base64": "..."}otherwise.- The serialized
headersobject counts towardoutput_tokensand is capped at 16 KiB; a response over the cap fails the job. - The control header is accepted only for jobs whose result is not compared across runners: at most one runner, or verification mode
NoneorEconomicBond. Actorrunner.httpjobs useEconomicBondwith one runner. Any other job fails before the request is sent, because headers such asDatediffer between requests. - A control header that names no header (empty, blanks or commas only) is ignored.
RunnerResult.textdoes not change:extract_textfollows thedatakey to the body.
9.4 await ActorRef.async_* — Actor Continuation
Actor-to-actor async calls use the @actor.continuation decorator. cowboy_sdk (PVM) only.
- Each
await ActorRef.async_<method>(...)is lowered tosend(target, {...})plus a state save. - The target’s handler is invoked in a future block; when it completes, it delivers a callback message that resumes the caller’s continuation.
- Requires both caller and callee to be
@deferred.
9.5 Payload Codecs — decode= and codec=
At the host boundary a handler’s payload and return are byte strings. The codec that turns them into native values is declared on the handler and selected by the caller:
cbor is the default for call() and send(). On the handler side the default is raw today but is scheduled to become cbor at an activation height (permissions._DECODE_DEFAULT_ACTIVATION_HEIGHT, a dormant u64::MAX sentinel until governance sets it), so a handler that declares nothing has its payload type change under it on a flag day — bytes before, a decoded value after. An explicit decode= is immune to that switch, which is why every actor in this repo declares one. Both ends validate against a single tuple, payload_codec.CODECS, so a handler cannot declare a codec that no caller is able to select — which is why decode="json" was unavailable until the caller side could name it.
Two handler kinds can only ever be raw, and declaring otherwise fails at class creation rather than at call time: a continuation, whose generated resume has its own payload path, and on_fork, which the host invokes with a bare 20-byte parent address. init and on_fork are also exempt from the gated default — the host builds those payloads, so no caller negotiated a codec for them — but declaring raw explicitly is still clearer than relying on the exemption.
cbor and json are the two encoded codecs; raw is the byte-in/byte-out hatch. Everything below that says “encoded codec” applies to both.
ExecuteActor payload is opaque bytes, so nothing on the callee side can detect or correct a caller that picked the wrong codec. A mismatch usually raises — a CodecError from the callee’s decode, or a decode failure on the caller’s return — but small scalars can misdecode silently. Treat a handler’s declared decode= as part of its signature.
Empty payload. Under either encoded codec, None sends no payload (empty bytes), which is distinct from an encoded empty container — [] is 0x80 under cbor and [] under json. An empty inbound payload decodes back to None. Under raw, pass b"".
Pre-serialized returns. A bytes return from an encoded handler passes through unencoded, so a handler can hand back an already-serialized payload. ok(value) forces encoding instead — under cbor, ok(b"\x00\x01") yields a CBOR byte string rather than those two bytes verbatim. JSON has no byte-string type, so ok(b"...") under json raises CodecError; wrap a string or a container instead. ok() is meaningful only under an encoded codec — reaching the permission wrapper from a raw handler raises TypeError rather than leaking out unencoded.
Failure taxonomy is deliberately asymmetric:
The handler side normalizes every codec fault to
CodecError so a failure surfaces as a codec slug rather than a host-internal error. On the caller side cbor deliberately does not re-classify: encode_args runs in the caller’s own frame, where the exception is the caller’s to catch. json raises CodecError there because the strict profile classifies its own refusals before the stdlib can leak one, and a CowboyError is no harder for a caller to catch.
The strict JSON profile
json is not json.dumps with defaults. json_codec narrows it so that identical values produce identical bytes and nothing is emitted that a serde_json consumer would refuse:
- Sorted keys, no separator padding, ASCII-escaped.
{"b": 1, "a": 2}serializes to{"a":2,"b":1}, and{"k": "café"}to{"k":"caf\u00e9"}— the bytes never depend on dict insertion order, and are always 7-bit. - No non-finite numbers.
NaN/Infinity/-Infinityare refused on encode, and on decode in both spellings: the bare tokensjson.loadsaccepts by default, and a number literal that is valid RFC 8259 syntax but overflows to infinity (1e400). - No surrogate code points, on encode or decode, paired or not. A supplementary character is a single code point, so it survives as a well-formed pair escape —
"😀"encodes to"\ud83d\ude00"— and nothing legitimate is lost. - UTF-8 only on decode, so a UTF-16/32 BOM cannot reinterpret the payload under an encoding this profile never emits.
- No byte-string type, and object keys are strings. A
bytespayload is refused, and integer keys stringify —{1: "a"}round-trips as{"1": "a"}, wherecborreturns{1: "a"}. Choosecborwhen key fidelity or binary data matters.
CodecError (E1213) — and for json, on the caller side as well as the handler side.
10. Continuations
All continuation features in this section arecowboy_sdk (PVM) only.
10.1 FSM Compilation
Both@runner.continuation and @actor.continuation compile at class-load time into a pair of functions:
<name>: the initial handler; starts the FSM, persists state 0, issues the first async effect, returns.<name>__resume: the resume handler; called by the runtime on callback delivery, loads state, advances the FSM, issues the next async effect (or returns final value).
10.2 capture() — Explicit Local State
Local variables that must survive an await MUST be attached to a capture() object:
CaptureTypeError at the await point.
Continuation state is stored at key __continuation:<correlation_id> within the actor’s own storage. Limits:
Exceeding either limit raises
ContinuationSizeLimitError or ContinuationCountLimitError.
10.3 Guards
Continuations can assert that specific storage keys were not modified during the async wait: Method A — decorator-level:GuardedValue:
keccak256(cbor(value_at_snapshot)). The fingerprint is stored in the continuation state and re-checked on resume.
10.4 Bounded Loops
await inside a Python loop requires an explicit upper bound:
bounded_loop with max_iterations = N generates N FSM states at compile time. Exceeding N at runtime raises LoopBoundExceeded. Unbounded iteration with await is rejected at class-load time.
10.5 Sequential Await Limit
A single continuation function MAY contain at most 8 sequential await points (not counting awaits insidebounded_loop). This is a compile-time limit; violations are rejected at class-load. Authors needing more should split into multiple continuations.
10.6 Continuation Persistence Format
The@runner.continuation / @actor.continuation machinery has two orthogonal layers:
- Authoring layer (compile-time, §10.1–10.5). The decorator rewrites an
async defwithawaitpoints into straight-line code (the FSM transform), andcapture()/ guards / bounded loops constrain what may cross a suspension. This is developer ergonomics — it shapes the source, not the persisted bytes. - Persistence layer (runtime, this section). When a continuation suspends at an
await, the runtime serializes the suspended interpreter state and commits it to actor storage; the resuming dispatch restores it. This is the format that is part of the consensus boundary.
ContinuationMode::Checkpoint): the persisted artifact is a versioned snapshot of the suspended VM, not an SDK-authored field record.
Note. The SDK also shipsStorage location. A suspended continuation is written to the actor-state keycontinuation.save_contrecord helpers (a small typed map ofstate/ctx/handler/ timeout / guards). Those belong to the FSM execution mode, which production does not currently enable, and they exist in two not-yet-reconciled variants. The normative on-chain format is the checkpoint snapshot defined below. Reconciling the record helpers and thefsm-vs-checkpointterminology is tracked in COW-2340.
__continuation:<cid>, where <cid> is the hex encoding of the resuming message id. The value flows into the state root, so it is consensus-committed. Because a snapshot can legitimately exceed the 64 KiB inline-blob cap (§6.5), __continuation:-prefixed keys are exempt from that cap (capping them would abort actor resumes).
Snapshot envelope. The snapshot is encoded as a single canonically-ordered CBOR map with a leading version (current SNAPSHOT_VERSION = 3). The top-level fields are:
Encoding & determinism. Maps use length-first key ordering (compare encoded key bytes by length, then bytewise) — the same ordering as §11.5, applied recursively. Integers use shortest-form; strings/bytes are length-prefixed; floats are IEEE-754 big-endian. The serialization performs no hash-ordered iteration and invokes no non-deterministic source, so two validators suspending the same execution produce byte-identical snapshots. (As elsewhere, authors must avoid native
float in consensus paths per §11.3; the encoder is deterministic, but IEEE-754 arithmetic across heterogeneous FPUs is not.)
Resume validation. On resume the runtime decodes the snapshot, rejects an unknown version or a source_path mismatch, validates that frame lasti values and all object indices are in range, and restores the frame stack before continuing execution at the saved instruction pointer.
Versioning. Version 3 is current. The decoder also accepts the legacy single-frame layout (v1/v2 — a flat lasti/code with an implicit single module-level frame) and upgrades it in memory; new snapshots are always written at the current version.
11. Type System
The SDK replaces or constrains Python built-ins whose semantics are non-deterministic or ambiguous across platforms. All types in this section arecowboy_sdk (PVM) only unless otherwise noted. The standalone cowboy package exports Address as two separate forms (a Pydantic hex-string model for chain interactions, and a 20-byte stub for local actor testing) but does not export BlockHeight, SoftFloat, ordered_set, or CowboyModel.
11.1 Address
20-byte Ethereum-compatible address.
11.2 BlockHeight
Semantic int wrapper for block heights. Identity-equal to int at the bytecode level; useful for type annotations.
11.3 SoftFloat, ordered_set
SoftFloatis a software-floating-point type. Native Pythonfloatdepends on the hardware FPU and is non-deterministic across platforms; authors MUST useSoftFloatinstead. The SDK may aliasSoftFloat = floatwhen running under a PVM that enforces softfloat at the instruction layer; authors SHOULD still annotate withSoftFloatfor clarity.ordered_setis a dict-backed set with deterministic insertion-order iteration. Python’s built-inset()is prohibited in actor code.
11.4 CowboyModel
A dataclass-like base class for structured data. Feature set:
- Deterministic serialization via
to_cbor()andfrom_cbor(bytes). - JSON Schema export via
schema()(for use withVerify). - Field validation on construction.
- Forbids
set/frozensetfields (non-deterministic iteration). - Forbids
floatfields (requiresSoftFloat).
11.5 CBOR Codec
The SDK uses Canonical CBOR (RFC 8949 §4.2) everywhere encoding crosses a determinism boundary. Storage values and continuation state are unconditional. Call arguments,send() payloads, and handler return values are Canonical CBOR under the default cbor codec, the strict JSON profile under json, and uncoded bytes under raw — the codec is per call and per handler (§9.5), so it is the pair that decides, not this section. Both encoded codecs are deterministic; only their byte formats differ. Requirements:
- Map keys sorted by length-first ordering (RFC 8949 §4.2.3): compare the encoded key bytes by length first, then bytewise (lexicographically) within equal lengths.
- No duplicate keys.
- Floats encoded as IEEE 754 double (when unavoidable).
- Integers encoded in the shortest form.
cowboy_sdk.codec.encode(v) -> bytes and .decode(b) -> value are the canonical entry points.
Ordering note. This is RFC 8949 §4.2.3 (“length-first”) map-key ordering, not the §4.2.1 (“Core Deterministic Encoding”) pure byte-lexicographic ordering. Length-first is what the encoders in this section’s scope actually produce — the pure-Python codec (cowboy_sdk.codec, the encoder used on-chain since there is no host-sidecbor_encode) and the VM’s continuation/snapshot encoder (cbor_key_cmp) both sort(len(key), key). The two implementations are byte-for-byte consistent; the choice is fixed by the deployed encoders. (Note: the transaction /PayloadSignenvelope is a separate determinism boundary — it is keccak256-signed and serialized withciboriumin struct-field-declaration order, not length-first, so a tx signer must mirror that field order rather than length-first-sort its keys. That envelope is outside §11.5’s scope.) Migrating the boundary to §4.2.1 would change the canonical bytes of all on-chain CBOR (state values, payloads, continuation state) and is therefore a coordinated consensus change, tracked separately — not implied by this section.
12. Runtime Module
cowboy_sdk.runtime exposes the Host API. Actor code SHOULD prefer the higher-level primitives in cowboy_sdk (call, send, storage proxy); runtime is for cases where fine-grained control is needed (e.g., system actors, upgrade flows).
The standalone cowboy package provides a partial local stub of runtime. It covers context access (get_sender, get_actor_address, get_block_height, get_timestamp_ms), events, send_message / call_actor, low-level state no-ops, ownership helpers, and test utilities (configure, reset, get_captured_events, get_captured_messages). It does not implement token operations, timers, upgrade_self, submit_job, keccak256, randomness, charge_gas, or scan_state_prefix — those functions exist only in the on-chain cowboy_sdk.runtime.
12.1 Context
These are the ONLY authorized sources of time and identity.
time.time(), datetime.now(), and similar are trapped.
12.2 State
Lower-level state access parallelingself.storage:
set_state and delete_state refuse writes to reserved keys (__OWNER__, __INITIALIZED__). See §12.10.
12.3 Events
EmitResult dictionary with emit_id, sync_outcomes, and deferred_subs. The optional 32-byte schema identifier follows CIP-29 §2.1. Event payload and schema declaration Cells are metered per CIP-3 §2.2.2.
12.4 Tokens (CIP-20)
The standard CIP-20 interface is exposed as runtime operations:token.create, token.transfer, etc.).
12.5 Timers (CIP-5)
fire_at_block, the scheduler delivers payload to the actor’s on_timer handler. Requires timer.schedule entitlement. schedule_timer_ex allows overriding fee payer, gas limit, and expiry per CIP-5 §4.1.
12.6 Jobs (CIP-2)
@runner.continuation; actor authors SHOULD prefer the continuation form.
12.7 Upgrades
sys.upgrade entitlement.
12.8 Crypto
randomness() produces deterministic per-block VRF output keyed by domain. This is the only authorized source of randomness; random.random() and secrets are trapped.
12.9 Metering
Superseded for application actors (2026-07-28). The paragraph above records the original CIP-6 position and is retained as historical specification text. Current guidance: application actors MUST NOT call runtime.charge_gas(). The function remains implemented and still consumes Cycles, so a manual call charges gas in addition to the automatic syscall-boundary metering described above — it inflates cost rather than front-loading it. Manual calls were removed from the bundled node examples and developer guides in node#921, and from the remaining actors, examples, and guides. This note changes recommended practice only; it does not alter the metering semantics specified in this section.
12.10 Ownership
The ownership model tracks a single privileged address in the reserved__OWNER__ storage slot. User code cannot read or write this slot via self.storage[] — the proxy raises ValueError for reserved keys. Use the runtime APIs:
- If no owner is set, any caller may set it (bootstrap window). Safe only when ownership is claimed inside
init, because the CLI runsinitatomically in the deploy transaction withsender = deployer, closing the window. Deploying with--no-init(or deferring the owner claim to a later transaction) leaves an unguarded front-run window — the protocol enforces no deployer-binding on the firstassume_ownership_if_unowned()call, so a third party can claim ownership between deploy and the owner-claim tx. Actors that rely oninitfor ownership MUST NOT be deployed with--no-init. - Once set, only the current owner or an intra-actor call may transfer.
- Passing the zero address to
transfer_ownershipraisesValueError; userenounce_ownership()explicitly.
@callable_by(OWNER) handlers become permanently unreachable after renounce_ownership().
Reserved storage keys:
The standalone
cowboy package provides stub implementations of all ownership functions backed by module-level state. Call runtime.reset() between tests to clear ownership and initialization state.
12.11 State Prefix Scan
limit (key, value) pairs whose user-key starts with prefix, in ascending key order. Only verbatim-mode keys (≤ 32 bytes) are visible. limit is clamped to 1000 by the host.
12.12 Native CBY Balance
amount wei (CBY has 9 decimals; 1 CBY = 10^9 wei) from this actor’s native CBY balance and credits the same amount to to. Synchronous, no return value, no callback into to.
transfer_balance is the only host-level primitive that moves native CBY between addresses. It is the counterpart, for native CBY, of runtime.token_transfer for CIP-20 tokens. The two ledgers are independent: token_transfer does not touch CBY balance and transfer_balance does not touch any token ledger.
Arguments:
to: 20-byte recipient address. The system-reserved address band (per CIP-1 §5) is rejected withHostError::InvalidInput. The zero address is rejected.amount: non-negative integer in wei.
- Atomic with the calling transaction. The debit and credit are staged in the transaction’s write-set; if the enclosing transaction reverts, both reverse together. Implementations MUST route the balance writes through the same staged-writeset machinery used by
call()andset_state— direct ledger mutations bypassing the writeset are non-conformant and leak balance on revert. - Insufficient balance raises
HostError::InsufficientFundsand does not mutate either account. amount == 0is a no-op (no event, no metering beyond the syscall base cost).- Self-transfer (
to == get_actor_address()) is permitted and is a no-op on net balance, but still consumes the syscall base cost. - Allowed from any
@deferredhandler.@pureand read-only contexts MUST rejecttransfer_balancewithHostError::Forbidden. - Requires the
econ.transferentitlement on the calling actor’s manifest. Legacy actors with no manifest MAY transfer freely, consistent with the rest of the runtime.
fork() (CIP-27 §3.3) by either:
-
Atomic seed via
fork(endowment=N)— birth the child already funded, with a single atomic debit/credit folded into the fork itself. Use when the parent must guarantee the child has CBY before any post-fork code runs (e.g. a child whoseon_forkschedules a self-timer at defaultfee_payer = self). -
Post-fork transfer — fork the child, then
transfer_balance(child, N)from the parent. Both calls are in the same transaction and share rollback, but the child is briefly zero-balance between the two syscalls. Use when the funding amount depends on state the parent doesn’t know pre-fork.
call():
CBY’s balance ledger is part of the chain’s account model, not actor storage. There is no recipient handler to invoke and nothing CBOR-encodable to pass — the operation is a pure debit/credit on two ledger entries. Modeling it as a call() would require every receiving actor to expose a handler whose only behavior is “accept funds,” which forces handler-existence on every actor that might ever be funded (including EOAs, which have no handlers). transfer_balance mirrors EVM’s value field on a plain ether transfer: a runtime-level effect, not a contract method.
Standalone cowboy SDK note: The standalone package provides a local in-memory stub recording (to, amount) pairs for test inspection (runtime.get_transfer_log()). No supply checks run.
13. Verification Builder
Verify produces CIP-2 verification configurations using a fluent chain. cowboy_sdk (PVM) only.
VerifyBuilder.mode() validates against the following set and raises ValueError on unknown values:
Checks are appended in the order
.check() is called and passed to the Rust result-verifier in that order.
Built-in checkers:
Additional supplemental checkers (
not_empty, contains, length_limit, http_status, response_time, signature_valid, tee_attestation, deterministic_output, field_exists, format_check) are exported by the implementation but are not mandated by this CIP.
14. Entitlements
14.1 Manifest Format
A manifest accompanies every deploy and upgrade. JSON shape:idvalues MUST appear in the Entitlement Registry (types/src/registry.rs).- The
entitlementsarray MUST be lexicographically sorted byid; a chain MUST reject deploy transactions with an unsorted manifest. paramscontents are entitlement-specific and validated at deploy time against the registry’sParamSchema.- An upgrade’s manifest MUST be a subset (in both ids and param-bound strength) of the prior manifest.
14.2 Runtime Enforcement
Entitlements are enforced at the Host API boundary, not in SDK Python:- Actor invokes an SDK function.
- SDK issues the corresponding syscall.
- Host checks the actor’s manifest for the required entitlement.
- If absent: Host returns
HostError::MissingEntitlement→ SDK raises the corresponding error. - If present but quota-exceeded or param-restricted: Host returns the appropriate error.
14.3 Entitlement Registry (Informative Summary)
CIP-2 §7 is the normative source.
Note — token entitlements are not discrete string IDs (COW-1507). Unlike the other rows, thetoken.*capabilities are not registered as string IDs in the entitlement registry. The implementation models them with a typedScope::Token([u8; 32])(the token id) plus the matchingActionvariant (TokenTransfer/TokenMint/TokenBurn/TokenFreeze/ …) innode/types/src/entitlement.rs— so a grant is naturally scoped to a specific token rather than a globaltoken.transferstring.token.createis the exception: it has noActionvariant and is not entitlement-gated — creating a token is gas-metered only, and the creator (tx.from) becomes the token’s owner and mint authority. This row is an informative summary of those capabilities; the typedScope::Token+Actionmodel is authoritative.
15. Error Hierarchy
All SDK exceptions derive fromcowboy_sdk.CowboyError. Each exception exposes:
HOST_ERROR_CODE: int— Host API error code (1–8)ERROR_SLUG: str— short identifier, formatE1xxx.why: str— human explanation.fix: str— suggested remediation
Exceptions are CBOR-encoded into the transaction receipt; clients use
ERROR_SLUG for programmatic handling.
The standalone cowboy package also exports client-side exceptions (TransactionFailed, RpcError, AccountNotFound, NonceMismatch, etc.) that are not part of this CIP.
16. CLI
The canonical CLI iscowboy, implemented in node/cli/. Commands relevant to actor development:
16.1 Project bootstrap
.cowboy/config.json (RPC URL, sender key, nonce cache).
16.2 Actor lifecycle
actor):
16.3 Ecosystem
16.4 Conformance
Any implementation MAY extend the CLI with additional commands. Renaming any command in this section is a breaking change and requires a CIP revision.17. PVM Determinism Rules
Actor code MUST conform to the following rules. Violations are detected at class-load time where statically decidable; otherwise they raiseDeterminismError at runtime.
18. Reference Implementation
Canonical implementation:- In-PVM SDK (
cowboy_sdk):node/pvm/Lib/cowboy_sdk/(version 0.1.1) - Host API:
node/execution/src/pvm_host.rs - CLI:
node/cli/(binarycowboy) - Standalone developer SDK (
cowboy):python-sdk/(PyPI:cowboy-sdk, version 0.1.0) - Examples:
cowboy/examples/core/05-tokens-and-balances,cowboy/examples/core/09-runner-llm,cowboy/examples/gallery/audio-transcription,cowboy/examples/gallery/image-generation,cowboy/examples/gallery/dao-copilot,cowboy/examples/gallery/compliance,cowboy/examples/gallery/advanced-messaging-ring
19. Open Questions / Gaps
- CIP-9 volume mounts from the SDK. Today, CBFS volumes are a runner-side concern. An actor-side API for
mount(volume_id, access_mode)is not yet defined. Needed for first-class storage-attached actor workflows. - CIP-7 / CIP-17 stream helpers.
cowboy watchtowerexists at the CLI level and as a system-actor pattern, but there is no actor-side@on_stream(stream_id)decorator orstream_publish()helper. Authors currently manage this via rawcall()/send(). - Checkpoint vs FSM mode.
runtime.is_checkpoint_mode()reports the persistence layer. Checkpoint mode (ContinuationMode::Checkpoint) is the current production on-chain format (§9.5); the FSM execution mode is not yet enabled in production. Terminology reconciliation is tracked in COW-2340. - Reentrancy semantics on
call()cycles.@reentrancy_guardis defined, but its key derivation (keccak256(method_name ‖ caller_addr)) may over-collide in contract-factory patterns. A CIP-6.1 revision may add an explicitkeyargument. - Address scheme forward compatibility.
Addressis fixed at 20 bytes (Ethereum-compatible). A future migration to a larger scheme would break the type; no migration path is specified here. - Static loop-bound detection. The compiler rejects unbounded
await-in-loop at class-load, but edge cases (awaits inside comprehensions, awaits in helper functions called from loops) are under-specified. A future revision should enumerate accepted and rejected shapes. - Entitlement manifest upgrade semantics. “Subset” is defined informally; the registry needs an
is_tighter_thanoperator per entitlement, currently implemented ad-hoc. - Standalone
cowboySDK coverage gaps. The standalone package does not yet mirrorrunner,capture,ActorRef,Verify,CowboyModel,SoftFloat,ordered_set,BlockHeight, orStorage.get_raw()/set_raw()/guard(). Actors that use those features must be tested against a live PVM sandbox.

