Skip to main content

Overview

cowboy_sdk.runtime is the supported, developer-facing wrapper over the low-level pvm_host bindings. Actors should call runtime.* (and the higher-level self.storage, call(), send() helpers) rather than pvm_host directly. All listed signatures are from the shipping SDK.
from cowboy_sdk import runtime

height = runtime.get_block_height()
runtime.emit_event("my.event", {"height": height})

Execution context

FunctionReturns
context()Full context dict: block_height, block_hash, tx_hash, sender, actor_addr, timestamp_ms, nonce, msg_id
get_sender()Transaction sender (20-byte address)
get_actor_address()This actor’s own address (20 bytes)
get_tx_hash()Current transaction hash (32 bytes)
get_block_height()Current block height (int)
get_timestamp_ms()Block timestamp in milliseconds
get_chain_id()Chain ID (falls back to 0 where the host context doesn’t supply one — the production context currently doesn’t)
get_pvm_version()PVM version string (falls back to "unknown" under the production host)
Mode helpers: mode() returns "fsm" (production), "checkpoint" (debug-only), or "local"; is_production() / is_development() / is_checkpoint_mode() are the boolean forms; require_fsm() raises unless running in FSM mode.

State

Prefer self.storage[...] (the @actor proxy, with automatic CBOR encoding). The raw layer underneath:
FunctionSemantics
get_state(key: bytes) -> bytes | NoneRead raw bytes
set_state(key: bytes, value: bytes)Write raw bytes; refuses the reserved keys __OWNER__ / __INITIALIZED__
delete_state(key: bytes)Delete; same reserved-key guard
scan_state_prefix(prefix: bytes, limit: int = 100) -> list[tuple[bytes, bytes]]Up to limit key/value pairs from prefix, ascending; only verbatim (≤ 32-byte) keys are visible. Paginate by appending b"\x00" to the last key

Events and gas

FunctionSemantics
emit_event(name: str, payload: bytes | str | dict)Emit an event; str is UTF-8-encoded, dict is JSON-encoded
charge_gas(amount: int)Report extra gas consumption to the meter

Ownership and lifecycle

FunctionSemantics
get_owner() -> bytesOwner address, or b"" if unset
assume_ownership_if_unowned() -> bytesBootstrap helper: if unowned, the current sender becomes owner (idempotent). Call it from init only — init runs atomically with deploy, which is what closes the front-running window
transfer_ownership(new_owner: bytes)First setter wins while unowned; afterwards only the owner may transfer. Zero address rejected
renounce_ownership()Permanently sets the owner to the zero address; @callable_by(OWNER) handlers become unreachable
is_initialized() -> boolTrue once init has completed successfully

Messaging and jobs

FunctionSemantics
call_actor(target: bytes, method: str, payload: bytes, cycles_limit: int) -> bytesSynchronous cross-actor call (prefer the cowboy_sdk.call() wrapper, which handles CBOR and errors)
send_message(target: bytes, payload: bytes)Fire-and-forget async message — allowed in @deferred handlers only
submit_job(payload: bytes)Submit a pre-encoded Runner job — @deferred handlers only. Prefer the high-level runner.llm/http/mcp continuations (CIP-2)

Timers (CIP-5)

FunctionSemantics
schedule_timer(height: int, payload: bytes) -> bytesFire payload back at this actor at height; protocol defaults for fees/TTL; returns a timer_id
schedule_timer_ex(height, payload, fee_payer=None, gas_limit=None, expires_at=None) -> bytesExtended form: choose the fee payer — which must be the executing actor or the current transaction sender; any other address is rejected (CIP-5 §4.2) — plus a per-fire gas limit and an absolute expiry (≤ current height + max TTL)
extend_timer(timer_id: bytes, new_expires_at: int)Renew a timer you own; a missing timer is a silent no-op
cancel_timer(timer_id: bytes)Cancel by id

Event subscriptions (CIP-29)

FunctionSemantics
subscribe_event(emitter: bytes, topic: bytes, handler: str, gas_prepaid: int, bid: int = 0) -> bytesSubscribe to (emitter, topic); registration fee and bid are burned, gas_prepaid is held and refundable; returns a 33-byte sub_id
unsubscribe_event(sub_id: bytes) -> intSubscriber-only; refunds remaining prepaid gas
force_unsubscribe_event(sub_id: bytes)Emitter-only; remaining gas refunds to the subscriber
update_bid(sub_id: bytes, additional_bid: int) -> intRaise the subscription’s priority bid (burned)
topup_subscription(sub_id: bytes, additional_gas: int) -> intAdd prepaid gas; any caller may pay

Randomness and crypto

FunctionSemantics
randomness(domain: bytes) -> bytesDeterministic protocol randomness, domain-separated
keccak256(data: bytes) -> bytesKeccak-256 digest

Code upgrade

FunctionSemantics
upgrade_self(new_code: bytes, new_manifest: bytes | None = None)Replace this actor’s code (requires the sys.upgrade entitlement). A new manifest must be a subset of the current one — entitlements can narrow, never broaden. State, balance, and mailbox are preserved; new code takes effect on the next invocation

Tokens (CIP-20)

The token surface (token_create, token_balance_of, token_total_supply, token_transfer, token_approve, token_allowance, token_transfer_from, token_mint, token_burn) is covered with examples in the CIP-20 spec; amounts are int base units (u128), addresses 20 bytes, token_id 32 bytes.

Behavior under the mock host

Unit tests run against cowboy_sdk.mock_host (usually via cowboy_sdk.testing.SimulatedChain). Context, state, ownership, timers, subscriptions, and call_actor (with set_call_handler) all work in-memory. emit_event works under SimulatedChain (which captures events for chain.events()) but not under the bare mock host; the mock implements no gas meter, so guard charge_gas calls behind runtime.is_production() in code you unit-test. keccak256 falls back to SHA-256, and randomness and the token_* operations require a real chain.

Further reading

  • CIP-6 §12 — Runtime Module — normative spec for the core of this surface (mode helpers, CIP-29 subscriptions, and some context getters are shipping-SDK additions beyond §12)
  • SDK Overview — the high-level @actor / continuation / model layer
  • PVM Reference — validation rules and determinism guards