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
.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.
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.9.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 | Provenance | Actions |
|---|---|---|---|---|
| cowboy_protocol_py-0.9.0.tar.gz | 1.7 MB | With provenance | Details for cowboy_protocol_py-0.9.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| cowboy_protocol_py-0.9.0-py3-none-any.whl | Python 3 | none | any | Details for cowboy_protocol_py-0.9.0-py3-none-any.whl |
Total release size: 3.4 MB
Release files / cowboy_protocol_py-0.9.0.tar.gz
| Download URL | cowboy_protocol_py-0.9.0.tar.gz |
|---|---|
| Size | 1.7 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3f3a9b353f34f964defe99f691c246bd682333995087ce32f6b4f3d731465561
|
|
BLAKE2b-256 checksum How to use checksums |
e2a000a84281e7510e7bf51e33dbb6ff0a65e2ab42870ae70b42b8abc02a84c4
|
| 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 Nov 1, 2026.
Transparency logRelease files / cowboy_protocol_py-0.9.0-py3-none-any.whl
| Download URL | cowboy_protocol_py-0.9.0-py3-none-any.whl |
|---|---|
| Size | 1.6 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
43f1344d87a7df05e5ed4866b62031dd7b6ebc4e1a4fbc60e4fa9d8235039743
|
|
BLAKE2b-256 checksum How to use checksums |
427c26c639b0aa6e97fee0ee3b650b80ea3a2244d93c7a4b96a098793115907b
|
| 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 Nov 1, 2026.
Transparency logSecurity for cowboy-protocol-py 0.9.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.
Attestation for cowboy_protocol_py-0.9.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 Nov 1, 2026.
Transparency logA 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.