Skip to main content

cowboy-protocol-py

The canonical Cowboy transaction codec for Python — a thin, pure-Python wrapper (import name cowboy_protocol) that loads the cowboy-protocol-wasm-ffi .wasm via the wasmtime pip package and exposes a Pythonic API.

It is the Python analog of @cowboyinc/protocol-wasm: every byte and every signature comes from the same cowboy-protocol-codec compiled to wasm, so python == native == wasm == ts — byte-identical, proven by the golden gate (the codec-owned crates/cowboy-protocol-codec/fixtures/tx-v3/ corpus, including CBQS transaction instructions).

This package lives in cowboyinc/core under protocol/bindings/python, co-located and versioned with the wasm and its flat ABI, so the ABI and its only Python consumer can never drift across repos. (Paths below are relative to protocol/.) The bundled .wasm is built from the wasm-ffi crate via crates/cowboy-protocol-wasm-ffi/build-ffi.sh and committed. A codec change makes the committed artifact stale; CI reports that as a warning and the release owner refreshes it before the next release, which refuses to publish a stale one. The package tests load that committed artifact and check golden bytes and retired-instruction rejection.

The EntitlementCreateRole, EntitlementAssignRole and EntitlementRevokeRole variants remain retired (COW-4964). Auth v3 uses ordinary transaction version 3, trusted full chain identities, and typed signature slots; v2 envelopes are rejected.

Why pure-Python

wasmtime ships its own platform wheels, so installing this package needs no Rust/C compiler — pip install just pulls the wasmtime wheel and the committed .wasm. The wasm has no JS / wasm-bindgen imports (it instantiates with an empty import set), so a plain wasmtime host runs it directly.

Install

pip install cowboy-protocol-py

To install an unreleased revision, pin it by commit:

pip install "cowboy-protocol-py @ git+https://github.com/cowboyinc/core.git@<rev>#subdirectory=protocol/bindings/python"

Replace <rev> with a full commit SHA (or tag) to pin. For local development against a checkout:

pip install -e bindings/python

API

import cowboy_protocol as cp

cp.signing_hash(tx_json: str, chain_identity_json: str) -> bytes  # native ID, 32 bytes
cp.eip712_signing_hash(tx_json: str, chain_identity_json: str) -> bytes
cp.eip712_typed_data(tx_json: str, chain_identity_json: str) -> str  # canonical JCS
cp.encode_transaction(tx_json: str) -> bytes   # ordinary v3 canonical encoding
cp.decode_transaction(raw: bytes) -> str       # strict v3 JSON, no trailing bytes
cp.attach_signature(unsigned_bytes: bytes, slot_index: int, signature_hex: str) -> bytes
cp.encode_submission(tx_json: str) -> bytes    # the POST /submit body
cp.encode_submission_batch(data: bytes, lengths: list[int]) -> bytes
cp.sign_transaction(tx_json: str, priv_key32: bytes, mode: int,
                    chain_identity_json: str) -> str  # signed primary slot
cp.sign(hash32: bytes, priv_key32: bytes) -> bytes  # low-level 65-byte r||s||v primitive
cp.address_from_private_key(priv_key32: bytes) -> str  # EIP-55 checksum

tx_json is the canyon-JSON transaction shape — byte-for-byte the same serde form node serializes, identical to the golden vectors' input.json (see crates/cowboy-protocol-codec/fixtures/tx-v3/<name>/input.json).

chain_identity_json is a closed object with a nonzero canonical decimal-string chain_id and a nonzero lowercase 0x-prefixed 32-byte chain_instance_id. Discover it from the intended RPC's closed /chain-info, independently of any prepared signing payload, and retain the same trusted identity for the operation. Mode 0 signs the native digest; mode 1 signs EIP-712. Transaction signatures are full {algorithm:1, mode:0|1, public_key:null, signature:"0x…"} slots.

The public hash, typed-data, and signing exports enforce the codec's checked producer boundary. They refuse deferred envelopes and invalid signer assemblies. Native Rust total identity hashes still exist for consensus/verification vectors; a total vector hash does not authorize a producer to sign it. The retired signing_preimage and signing_hash_hex exports are absent; use the native ID bytes' .hex() only when a hex display is needed.

On any wasm error (malformed JSON, bad private key, etc.) the wrapper reads the wasm's cpw_last_error and raises cowboy_protocol.CowboyProtocolError — it never returns garbage.

Native RAS, CBFS and workload object signing (CN-7)

These C object helpers use the codec's closed unsigned DTOs and require trusted chain_identity_json: str as their last argument. They share names and argument order with the WASM and flat-FFI exports. Python returns bytes for preimages and 32-byte hashes, and dict for successful verifier results.

# Native object preimages, hashes and core verifiers.
cp.ras_owner_delegation_signing_preimage(object_json: str, chain_identity_json: str) -> bytes
cp.ras_owner_delegation_signing_hash(object_json: str, chain_identity_json: str) -> bytes
cp.ras_owner_delegation_verify(signed_object_json: str, chain_identity_json: str) -> dict
cp.cbfs_delegation_cert_signing_preimage(object_json: str, chain_identity_json: str) -> bytes
cp.cbfs_delegation_cert_signing_hash(object_json: str, chain_identity_json: str) -> bytes
cp.cbfs_delegation_cert_verify(signed_object_json: str, chain_identity_json: str) -> dict
cp.ras_owner_authorization_signing_preimage(object_json: str, chain_identity_json: str) -> bytes
cp.ras_owner_authorization_verify(signed_object_json: str, chain_identity_json: str) -> dict
cp.ras_relay_authorization_signing_preimage(object_json: str, chain_identity_json: str) -> bytes
cp.ras_relay_authorization_verify(signed_object_json: str, expected_signing_public_key_json: str, chain_identity_json: str) -> dict
cp.workload_mount_policy_signing_preimage(object_json: str, chain_identity_json: str) -> bytes
cp.workload_mount_policy_signing_hash(object_json: str, chain_identity_json: str) -> bytes
cp.workload_mount_policy_verify(signed_object_json: str, chain_identity_json: str, owner_auth_json: str) -> dict

# Paid-read digests: the same unsigned DTO and full identity contract.
cp.cbfs_read_authorization_digest(object_json: str, chain_identity_json: str) -> bytes
cp.cbfs_read_completion_digest(object_json: str, chain_identity_json: str) -> bytes
cp.cbfs_read_channel_voucher_digest(object_json: str, chain_identity_json: str) -> bytes

ras_owner_authorization_verify checks signature self-consistency against the delegated_pubkey carried by the object. Success does not establish registration or authority. Compare the returned signing_public_key with an independently trusted registered delegation, then enforce its scope, expiry and revocation before accepting the authorization.

The identity JSON has exactly chain_id (a nonzero canonical decimal u64 string) and chain_instance_id (0x plus 64 lowercase hex digits, nonzero):

import json

chain_identity_json = json.dumps({
    "chain_id": "1337",
    "chain_instance_id": "0x" + "71" * 32,
})

Missing/malformed identity, extra fields and numeric chain IDs are errors. The signer obtains and checks both members against its trusted chain discovery result. A matching numeric chain ID alone does not identify a genesis instance. For objects carrying a chain_id, that field must equal the trusted ID.

Object DTOs are JSON objects with canonical decimal-string integers, lowercase 0x fixed bytes and tagged SigningPublicKey strings. For example, an Ed25519 key is the commonware-encoded string "0x0220d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a". Raw key arrays, uppercase key hex, missing key prefixes, unknown/duplicate fields and positional arrays are rejected. Delegation addresses accept lowercase or valid EIP-55 spelling. The native DTOs have no network member.

Hash/preimage inputs contain no signature member. A verifier takes that same DTO plus one required signature slot with all four members: {"algorithm":1,"mode":0,"public_key":null,"signature":"0x<130 lowercase hex>"} for wallet signatures. public_key must be present even when null. Wallet slots use secp256k1 native mode, low-S and recovery ID 0 or 1; owner/relay authorization slots use native Ed25519. The relay verifier's separate expected_signing_public_key_json is a JSON string encoding the independently trusted registered Ed25519 key, because its slot forbids an inline key and its body carries none. Successful verifiers return {"address":"0x…"} or {"signing_public_key":"0x…"}; invalid signatures raise CowboyProtocolError.

service_class is exactly "MOUNT_CAPABLE_V1" or "NON_MOUNT_SERVICE_V1". Object-wrapped unit classes and all three retired workload service classes (WORKLOAD_MOUNT_V1, VOLUME_BOUND_SERVICE_V1, SERVICE_RELEASE_V1) reject. Standing workload mount policies use the separate CN-19 policy API below. Owner authorization DTOs require volume_id and workload_effect_binding, using explicit null when absent. The complete schemas and delegation examples are in the WASM object API and shared DTO source.

Native preimages bind the domain, full identity and canonical object body. Ed25519 signs those returned bytes; secp256k1 signs the returned Keccak-256 hash. Python hash helpers return the 32 raw bytes, so use "0x" + digest.hex() when comparing with a WASM/CLI hash string. No helper adds a wallet personal-message prefix.

Existing Python names keep the same native DTO contract and final identity argument:

cp.owner_authorization_signing_bytes(object_json: str, chain_identity_json: str) -> bytes
cp.owner_delegation_signing_bytes(object_json: str, chain_identity_json: str) -> bytes
cp.relay_authorization_signing_bytes(object_json: str, chain_identity_json: str) -> bytes
cp.delegation_cert_signing_bytes(object_json: str, chain_identity_json: str) -> bytes
cp.delegation_cert_hash(object_json: str, chain_identity_json: str) -> bytes

cbfs auth delegation-prepare emits unsigned DTO objects cert and ras, chain_identity, and bare hex cert_hash / ras_hash. Serialize each DTO and the checked identity as JSON arguments, reconstruct the hashes here, then compare before signing. Completion's --cert-sig and --ras-sig accept the full native slot JSON. Transaction-building commands such as delegation-register-tx instead return the CN-2 handoff members unsigned_tx_hex, slot_index, mode, signing_hash, and chain_identity.

The CBFS data-plane helpers below retain their one-document native-serde input contracts, described by their function docstrings:

cp.owner_cap_token_signing_bytes(token_json: str) -> bytes
cp.delegated_request_signing_bytes(request_input_json: str) -> bytes
cp.relay_usage_signing_bytes(usage_input_json: str) -> bytes

tests/test_golden.py and the Rust wasmtime_golden gate pin these exports to the same codec vectors. The flat ABI is documented in crates/cowboy-protocol-wasm-ffi/src/lib.rs.

Example

import json
import cowboy_protocol as cp

# Example identity only. In a live operation discover it from the intended RPC.
identity = json.dumps({
    "chain_id": "42", "chain_instance_id": "0x" + "11" * 32,
})
tx = json.dumps({
    "chain_id": 42, "nonce": 0, "nonce_lane": 0,
    "instruction": {"System": {"CreateAccount": {
        "account": "0x0000000000000000000000000000000000001111",
    }}},
    "cycles_limit": 50000, "cells_limit": 50000, "access_limit": 512,
    "max_fee_per_cycle": 1, "max_fee_per_cell": 1, "max_fee_per_access": "1",
    "max_priority_fee_per_cycle": 0, "max_priority_fee_per_cell": 0,
    "max_priority_fee_per_access": "0",
    "from": "0x1a642f0E3c3aF545E7AcBD38b07251B3990914F1",
    "access_list": None, "metadata": [], "additional_signers": [],
    "origin_tx_hash": None, "origin_remaining_cycles": None,
    "origin_remaining_cells": None, "origin_remaining_access": None,
    "run_root": None, "delivery": 0, "sponsor": None,
    "signature": {"algorithm": 1, "mode": 0, "public_key": None,
                  "signature": "0x" + "00" * 65},
})

priv = bytes([1]) * 32
native_id = cp.signing_hash(tx, identity)
signed_tx_json = cp.sign_transaction(tx, priv, 0, identity)
body = cp.encode_submission(signed_tx_json)  # bytes to POST /submit
assert cp.signing_hash(signed_tx_json, identity) == native_id

Security — in-wasm signing threat model

sign, sign_transaction and address_from_private_key pass the raw 32-byte private key into wasm, where it lives in the host's linear memory for the duration of the call. This is the intentionally weaker threat model accepted for COW-2363; isolated-signer hardening is deferred. Do not feed high-value keys to these in-process signers in an untrusted host.

The golden gate (anti-drift)

tests/test_golden.py loads the v3 transaction corpus and asserts canonical transaction/submission bytes, checked native or EIP-712 digests, and golden RFC-6979 signatures. Deferred vectors retain native total-identity artifacts for the Rust gate, while the Python producer hash export must refuse them. test_v3_corpus.py also checks the native-generated tx-v3-bindings.json outcomes for invalid signer assemblies. The suite additionally replays the RAS/CBFS signing manifest, CIP-10 workload transactions and JobSpec vectors.

The vectors live in tests/fixtures: copies of the source fixtures under crates/, shipped in the sdist so the gate runs against an installed package with no checkout. tests/test_fixture_sync.py compares each copy with its source; refresh them with python bindings/python/tests/test_fixture_sync.py (it needs only Python).

CI (cowboyinc/core's .github/workflows/core.yml, the protocol-python job) runs this suite on the lowest and a current supported Python, twice:

  • Blocking: against the commit's own codec. In a temporary copy of the package, with the .wasm the wasm job just built and fixtures refreshed from source. A codec change that breaks the Python binding fails here.
  • Advisory: against what is committed. The committed .wasm and the committed fixture copies. If either is stale the job shows a warning and still passes. A codec change never has to touch this package.

The release owner refreshes both the .wasm and the fixture copies before tagging, and the release workflow refuses to publish until they are current.

A bit-exact committed-vs-fresh byte-match is intentionally not used: wasm builds aren't reproducible across toolchains (macOS dev vs CI ubuntu), so a byte-match would false-fail on benign build differences rather than real drift. Instead the wasm job in core.yml rebuilds the .wasm from source via build-ffi.sh and runs the wasmtime golden gate twice. The fresh build must equal the native codec: that blocks. The committed package .wasm is checked the same way, and against its build record, but only reports: a stale one shows a warning and does not fail the job. The release workflow runs those same two checks and blocks on them.

Refreshing the bundled wasm

This is the release owner's step before tagging a release. It is optional on an ordinary pull request: a codec change makes the committed wasm, and possibly the fixture copies, stale; CI says so in a warning, and nothing is blocked. publish-protocol-py.yml refuses to release until the wasm, its record and the fixture copies are current.

The shipped .wasm is the one CI builds on Linux with the pinned toolchain (rust-toolchain.toml, wasm32-unknown-unknown, --release --locked, no wasm-opt). It is never built on a developer machine. _provenance.json, shipped beside it and returned by cowboy_protocol.provenance(), records its sha256 and size, the JobSpec version, the toolchain, and a digest of what the wasm is compiled from, and only that:

  • src/, Cargo.toml and build.rs of the codec crate, cowboy-protocol-types and the wasm-ffi crate. Tests and fixtures are not inputs.
  • The Cargo.lock entries the wasm-ffi crate resolves for wasm32-unknown-unknown, from cargo tree. The rest of the monorepo's lockfile is not an input.
  • rust-toolchain.toml and the workspace's [profile.release] section.
  • crates/cowboy-protocol-wasm-ffi/build-ffi.sh, the build recipe. It refuses to run with RUSTFLAGS (or its CARGO_* equivalents) or a CARGO_PROFILE_RELEASE_* override set, so the script holds every flag.
  • The repository's .cargo/config.toml, which can set rustflags or [profile.release] overrides for the same build.

Releases are cut by .github/workflows/publish-protocol-py.yml; see docs/ci/protocol-py-releases.md in the repository root.

scripts/protocol-py-provenance.py check (it needs cargo and Python 3.11+) fails when the committed wasm or any of those inputs no longer matches the record, and refuses a record written with record --allow-host, for a non-Linux build host, or for a wasm whose producers section names another compiler than the record's rustc at the pinned toolchain. It is advisory in the wasm job of core.yml and blocking in the release workflow. To refresh, from the repository root:

# 1. Start from the commit to release: a branch at the tip of devnet.
# 2. Download the build core.yml's `wasm` job uploaded for that commit. Every
#    devnet push and pull request run uploads one, kept for 30 days:
gh run list --workflow core.yml --branch devnet --event push --limit 5
gh run download <run-id> --name protocol-py-wasm --dir /tmp/protocol-py-wasm
# 3. Install it and write the record. This refuses a wasm that does not match
#    its sha256 file, was not built by the pinned toolchain on Linux, or was
#    built from inputs other than the ones in this checkout. If it refuses,
#    the run built different inputs: use the run for the commit you are on.
python3 scripts/protocol-py-provenance.py record \
  --wasm /tmp/protocol-py-wasm/cowboy_protocol_wasm_ffi.wasm \
  --sha256 /tmp/protocol-py-wasm/cowboy_protocol_wasm_ffi.wasm.sha256 \
  --rustc-vv /tmp/protocol-py-wasm/rustc-vv.txt \
  --inputs /tmp/protocol-py-wasm/inputs.json
python3 scripts/protocol-py-provenance.py check
# 4. Commit the wasm and _provenance.json together, merge that pull request,
#    and tag the merged commit.

Refresh the fixture copies in the same change: run python protocol/bindings/python/tests/test_fixture_sync.py and commit whatever it changes under tests/fixtures.

source_revision in the record is the release commit. A commit cannot name itself, so it is null in the repository and written by the publish workflow before the archives are built.

Run locally:

pip install -e bindings/python
pip install pytest
python -m pytest bindings/python/tests -v

License

MIT OR Apache-2.0.

Signed object helpers

The Python package mirrors the commerce, Watchtower, room and anchor exports in protocol-wasm, including the same strict unsigned DTOs and JSON argument order. Hashes and preimages return bytes; verifiers and slot decoders return Python dictionaries. Each JSON argument is a separate flat-ABI pointer/length pair.

For example, payment_intent_signing_hash(unsigned_json, chain_identity_json) returns the exact 32 bytes to sign. Replace the unsigned policy_revision with the payer's account signature under signature (policy_revision plus [key_index, slot] entries, each slot with public_key: None), then call payment_intent_verify(signed_json, chain_identity_json, payer_auth_json) to invoke the core verifier, passing the payer's GET /accounts/{address}/auth body. purchase_authorization_verify takes the purchase authority's auth body the same way. Credit and purchase callers also compare the returned address with their independently trusted configured authority.

Watchtower verification takes three arguments: watchtower_message_verify(signed_json, expected_signing_public_key_json, chain_identity_json) (and the equivalent usage receipt function). The expected key is a JSON string containing canonical tagged Ed25519 key bytes from a trusted publisher/usage source. It is never obtained from the signed DTO.

room_authority_commitment(authority_json, chain_identity_json) requires a valid identity but returns a content-only SHA256 commitment, which is not instance-bound.

CN-19 planners return the codec's exact JSON text (str):

cp.workload_mount_policy_plan(record_json_bytes, volume_id, owner,
                              valid_until_epoch, revision, policy_revision,
                              chain_identity_json) -> str
cp.workload_mount_policy_instruction(record_json_bytes, volume_id, owner,
                                     valid_until_epoch, revision,
                                     signature_json, owner_auth_json,
                                     chain_identity_json) -> str

Epoch and revision arguments are canonical decimal strings. The mount policy is account-signed (CIP-49): hash/preimage take the closed ten-member policy DTO plus the owner's policy_revision; verify replaces policy_revision with an account signature (policy_revision and [key_index, slot] entries of secp256k1 native slots with public_key: null) and checks it against owner_auth_json, the owner's GET /accounts/{address}/auth body. Hash/preimage return bytes, and verify returns a dict. The full identity is required last and must match the policy's embedded pair. The requested volume must be declared; its mount principal may differ from the asserted owner under a mount grant. Publication independently proves current volume ownership.

Metadata

Release files for cowboy-protocol-py 0.8.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cowboy-protocol-py 0.8.0
File Size Uploaded Provenance Actions
cowboy_protocol_py-0.8.0.tar.gz 1.6 MB With provenance Details for cowboy_protocol_py-0.8.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for cowboy-protocol-py 0.8.0
File Interpreter ABI Platform Actions
cowboy_protocol_py-0.8.0-py3-none-any.whl Python 3 none any Details for cowboy_protocol_py-0.8.0-py3-none-any.whl

Total release size: 3.1 MB

Release files / cowboy_protocol_py-0.8.0.tar.gz

Download URL cowboy_protocol_py-0.8.0.tar.gz
Size 1.6 MB
Tags Source
SHA-256 checksum
How to use checksums
3ec2e0d7513d4e80c937c439c4a49a85ae7e9b8f370345d2b0b3dfba2eac907b
BLAKE2b-256 checksum
How to use checksums
cb81f81370c3eea7fa266d2941d1439c56c0938200eb15188eb5da722ac0d745
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.

Transparency log

Release files / cowboy_protocol_py-0.8.0-py3-none-any.whl

Download URL cowboy_protocol_py-0.8.0-py3-none-any.whl
Size 1.5 MB
Tags Python 3
SHA-256 checksum
How to use checksums
205ec2bb61deac0a9cbe00595257e0b8262d0d1e7e6a95a771d086f7de99ccdc
BLAKE2b-256 checksum
How to use checksums
6eb94c799cd61141b10d145b57558d141eb197719223b13d8bd4134053dd3c5d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.

Transparency log

Security for cowboy-protocol-py 0.8.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for cowboy_protocol_py-0.8.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page