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 (every vector in crates/cowboy-protocol-codec/fixtures/commonware-codec/ plus the quoted CBQS CreateStream wire vector).

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.

Version 0.6.0 ships codec/FFI 0.4.0 and rejects the retired EntitlementCreateRole, EntitlementAssignRole and EntitlementRevokeRole variants. This is a breaking, coordinated fresh-devnet release (COW-4964).

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_preimage(tx_json: str) -> bytes
cp.signing_hash(tx_json: str) -> bytes            # 32 bytes
cp.signing_hash_hex(tx_json: str) -> str          # lowercase hex, no 0x
cp.encode_transaction(tx_json: str) -> bytes      # full canonical tx encoding
cp.encode_submission(tx_json: str) -> bytes       # the POST /submit body
cp.sign(hash32: bytes, priv_key32: bytes) -> bytes            # 65-byte r||s||v
cp.sign_transaction(tx_json: str, priv_key32: bytes) -> str   # signed tx as canyon-JSON
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/commonware-codec/<name>/input.json).

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.

RAS + CBFS control-plane signing bytes

The canonical signing-byte contract (-codec's ras + cbfs_signing modules) is surfaced so a non-Rust signer — the python-sdk — derives the bytes it signs from here instead of re-implementing them (a byte-level fork is exactly the split that caused the 2026-07-11 outage, COW-2608):

# RAS consensus-auth (COW-2636)
cp.owner_authorization_signing_bytes(authorization_json: str) -> bytes  # RasOwnerAuthorization
cp.owner_delegation_signing_bytes(delegation_json: str) -> bytes        # RasOwnerDelegation
cp.relay_authorization_signing_bytes(authorization_json: str) -> bytes  # RasRelayAuthorization

# CBFS control-plane (COW-2649)
cp.delegation_cert_signing_bytes(cert_json: str) -> bytes            # DelegationCert
cp.delegation_cert_hash(cert_json: str) -> bytes                     # 32-byte keccak256(signing bytes)
cp.owner_cap_token_signing_bytes(token_json: str) -> bytes          # OwnerCapTokenV1
cp.delegated_request_signing_bytes(request_input_json: str) -> bytes  # DelegatedRequestInput
cp.relay_usage_signing_bytes(usage_input_json: str) -> bytes        # RelayUsageSigningInput

Each takes one canyon-JSON document — the serde shape of the named -codec struct, byte-for-byte the fixtures' input (fixed-width byte fields are JSON arrays of u8; Address is the 0x… string; Option<T> is the value or null). RasOwnerDelegation and DelegationCert require a typed service_class: the string MOUNT_CAPABLE_V1 or NON_MOUNT_SERVICE_V1, or one of the bound classes {"VOLUME_BOUND_SERVICE_V1": {"volume_id": [32 u8], "workload_id": [32 u8]}}, {"WORKLOAD_MOUNT_V1": {"volume_id": [32 u8], "workload_id": [32 u8]}} and {"SERVICE_RELEASE_V1": {"volume_id": [32 u8]}}; omission and unknown classes fail closed. Every byte comes from the same wasm the native golden gate pins: tests/test_golden.py drives these against crates/cowboy-protocol-codec/fixtures/ras/vectors.json, the same manifest the Rust wasmtime_golden gate pins, so python == native == wasm. See the per-function docstrings and the crate-level ABI doc in crates/cowboy-protocol-wasm-ffi/src/lib.rs for the exact per-struct JSON shape.

Example

import cowboy_protocol as cp

tx = """{
  "chain_id": 42, "nonce": 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": null, "signature": {"r": [0], "s": [0], "v": 0},
  "additional_signers": [], "origin_tx_hash": null,
  "origin_remaining_cycles": null, "origin_remaining_cells": null,
  "origin_remaining_access": null, "metadata": []
}"""

priv = bytes([1]) * 32
print(cp.address_from_private_key(priv))   # EIP-55 checksummed
h = cp.signing_hash(tx)                     # 32 bytes
sig = cp.sign(h, priv)                      # 65 bytes r||s||v
signed_tx_json = cp.sign_transaction(tx, priv)
body = cp.encode_submission(signed_tx_json) # bytes to POST /submit

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 this package for every transaction fixture vector and the quoted CBQS CreateStream vector, then asserts cp.* reproduces preimage.bin / signing_hash.hex / submission.bin byte-identically and reproduces the golden RFC-6979 signatures. It also replays the RAS/CBFS signing manifest, the CIP-10 workload transactions and the 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.

Metadata

Release files for cowboy-protocol-py 0.6.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.6.0
File Size Uploaded
cowboy_protocol_py-0.6.0.tar.gz 942.1 kB Details

Built distribution (wheel)

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

Total release size: 1.8 MB

Release files / cowboy_protocol_py-0.6.0.tar.gz

Download URL cowboy_protocol_py-0.6.0.tar.gz
Size 942.1 kB
Tags Source
SHA-256 checksum
How to use checksums
18c8cc4199bdc0a32d30bcff19109ed5ea17855ca0f44a1577cde79ae9a8f145
BLAKE2b-256 checksum
How to use checksums
dbfdc6dc8bab61e2fb0550df5b67d57ed6dbc77e61d181b28f2e8b1ecec9e294
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 16, 2026.

Transparency log

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

Download URL cowboy_protocol_py-0.6.0-py3-none-any.whl
Size 907.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bb32938ebdb84d6553b16c76e5225be213cafbdf55c6f51f0d483bc0060e0db8
BLAKE2b-256 checksum
How to use checksums
544f57e51ae5b5ce950c4e3f4afc023a14e43d7bef40018e9ca83971f7c69f16
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 16, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.6.0 This release

2 release files

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