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
.wasmthewasmjob just built and fixtures refreshed from source. A codec change that breaks the Python binding fails here. - Advisory: against what is committed. The committed
.wasmand 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.tomlandbuild.rsof the codec crate,cowboy-protocol-typesand the wasm-ffi crate. Tests and fixtures are not inputs.- The
Cargo.lockentries the wasm-ffi crate resolves forwasm32-unknown-unknown, fromcargo tree. The rest of the monorepo's lockfile is not an input. rust-toolchain.tomland the workspace's[profile.release]section.crates/cowboy-protocol-wasm-ffi/build-ffi.sh, the build recipe. It refuses to run withRUSTFLAGS(or itsCARGO_*equivalents) or aCARGO_PROFILE_RELEASE_*override set, so the script holds every flag.- The repository's
.cargo/config.toml, which can setrustflagsor[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)
| File | Size | Uploaded | |
|---|---|---|---|
| cowboy_protocol_py-0.6.0.tar.gz | 942.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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