Status: Draft
Type: Standards Track
Category: Core
Created: 2026-08-05
Requires: CIP-28 (fee-payer wire surface)
Relates-to: CIP-3 (fee preflight runs before either mode’s digest or recovery), CIP-33 (co-owner of the Transaction-v2 decoder; kind 1 remains decode-only and consensus-disabled here), CIP-41 (multi-signer authorization — relies on the §7.3 duplicate-primary rule)
1. Abstract
Cowboy already uses Ethereum’s cryptography end to end: secp256k1 ECDSA, keccak256, and Ethereum-identical address derivation. Any Ethereum keypair is a valid Cowboy account. What an Ethereum wallet cannot do today is produce a valid Cowboy signature: transactions are signed over a raw keccak256 of the canonical codec encoding, and stock wallets only expose prefixed signing surfaces (personal_sign, eth_signTypedData_v4).
This proposal closes that gap with an EIP-712 typed-data signature mode accepted alongside the native mode for caller-signed transactions. Transaction gains a signature_mode discriminant; mode eip712 verifies an EIP-712 digest of the transaction’s typed representation. Both native and EIP-712 transaction identity bind an immutable ChainIdentity, while signature bytes remain excluded from digest(chain_identity).
With mode eip712 in place, a compatible EVM wallet can become a Cowboy signer through an injected provider or the standard WalletConnect eip155 namespace and eth_signTypedData_v4. Cowboy’s own wallets ride the same rails and recognize the Cowboy transaction domain to render native, instruction-aware approval UI instead of raw typed-data fields.
The proposal also specifies the Ethereum facade: a deliberately minimal JSON-RPC endpoint (POST /eth) implementing just enough of the eth_* surface for wallet_addEthereumChain, balance display, and nonce lookup. It is not EVM compatibility and does not accept Ethereum transactions.
2. Motivation
- Wallet reach without custody. Today the only way to sign a Cowboy transaction is to hold the raw private key in the signing context — the browser extension, the wasm SDK, or a server key. The wasm SDK explicitly documents this as a weakened threat model. EIP-712 mode moves signing into hardware wallets and audited wallet software that never reveal the key.
- One testable core. Transaction v2, explicit chain identity, the connector, and the facade form one implementation slice that can be exercised end to end without activating route-authentication, payment-admission, governance, or storage redesigns.
- Ecosystem leverage. Signing via the
eip155namespace lets dapps reuse wagmi/viem/AppKit and compatible wallets use an existing typed-data surface. A custom WalletConnect namespace would initially reach only Cowboy-specific wallets. - Standard wallet surface. EIP-712 gives the transaction a domain-separated typed-data digest that provider and WalletConnect APIs can request without exporting the user’s key.
3. Design Goals
- The signature determines the transaction. In both modes, every field of the executed object is bound under the signature, either directly in the digest or by a structural invariant enforced at verification. No field is a free variable.
- One digest identity.
tx.digest(chain_identity)— the BMT leaf, receipt key, and dedup identity — is the tagged, chain-bound construction over canonical transaction bytes with signatures zeroed. Signature bytes never affect identity;signature_modedoes, so otherwise-identical native and EIP-712 authorizations are distinct transactions. - One construction. The transaction domain shape, encoding rules, field names, and verification context are defined once and consumed everywhere a transaction is signed or verified.
- Single implementation. Transaction EIP-712 machinery lives in
cowboy-protocol-codecand is consumed by node and the wasm/FFI/SDK bindings. No hand-copied digest code. - Fail closed. Unknown signature modes, unknown typed-data versions, and malformed signatures are rejected at decode or verification, at every admission point.
- Wallet-agnostic dapp surface. Dapps integrate via standard
eip155WalletConnect sessions andeth_signTypedData_v4. Cowboy-specific logic (building the typed data, encoding the submission) lives in a connector library, not in wallets. - No display spoofing. Every field in a typed struct is verified against the object it authorizes. No derived, display-only fields.
4. Non-goals
- EVM execution or general
eth_*compatibility. The Ethereum facade (§10) serves wallet onboarding only.eth_call,eth_sendTransaction,eth_sendRawTransaction,eth_getLogs,eth_estimateGas, and EVM semantics are permanently out of scope for it. - A custom
cowboy:WalletConnect namespace. CAIP-2 registration of acowboynamespace adds no capability overeip155+ typed data and would fragment wallet support. Not pursued. - Per-instruction expanded typed structs. The typed representation carries the instruction as canonical bytes (§7.2). Expanded, human-readable per-instruction structs (e.g. a dedicated
Transfertype) are a candidate follow-up once the generic mode is proven; see §15. - Mixed-mode multisig.
signature_modeapplies to a transaction as a whole; the primary signer and all additional signers use the same mode. - Route-authentication and payment signing. Those designs remain separate drafts owned by their respective CIPs. This release adds no route/payment version gate, connector method, server readiness claim, or activation dependency.
5. Definitions
- Native mode. The non-EIP signature scheme: 65-byte recoverable secp256k1 over the tagged, chain-identity-bound §7.1 signing preimage with all signatures zeroed.
- EIP-712 mode. The scheme defined here: the same 65-byte recoverable secp256k1, but over an EIP-712 typed-data digest (
keccak256(0x19 ‖ 0x01 ‖ domainSeparator ‖ structHash)). - Signature mode. A
u8discriminant selecting the verification scheme:0= native,1= eip712. All other values are invalid. - Chain identity.
ChainIdentity { chain_id: u64, chain_instance_id: [u8; 32] }.chain_idnames the network.chain_instance_idnames one exact release-pinned genesis and is derived in §16 independently of block-header digest versions. - Cowboy transaction domain.
EIP712Domain(string name,string version,uint256 chainId,bytes32 salt), withsalt = chain_instance_id. NoverifyingContractis used because no contract is the transaction verification authority. - Connector. The dapp-side library (
@cowboyinc/connect) that builds typed data, requests signatures over WalletConnect or an injected provider, assembles signed transactions, and submits them to the REST RPC. - Ethereum facade. The minimal JSON-RPC endpoint of §10.
6. Architecture
6.1 Signature schemes touched by this CIP
The native transaction digest is separated from EIP-712 by the
0x19 0x01 prefix; the EIP-712 transaction is additionally separated by domain name, version, chain ID, and salt. The same logical transaction on two chain instances therefore produces different signing digests.
Other authorization schemes remain owned by their respective CIPs and are neither changed nor declared release-ready here. To keep native/EIP separation checkable as the protocol grows, this CIP establishes the rule: no native signing preimage may begin with the byte 0x19. ASCII domain tags satisfy the rule naturally; schemes led by a version byte avoid the value 0x19.
6.2 Signing-flow comparison
6.3 EIP-712 encoding conventions
All Cowboy typed structs follow EIP-712 encoding exactly as specified in the standard, with these conventions for the schemes defined here:- Field names are snake_case, matching the canonical Rust field names. Type strings in this document are normative byte-for-byte; a wallet or SDK that re-derives a typehash from differently-cased names produces a different digest and fails verification.
- The transaction domain is the four-field form
EIP712Domain(string name,string version,uint256 chainId,bytes32 salt).chainIdandsalt = chain_instance_idcome from one closedGET /chain-inforesponse and are treated as one identity. The domainversionis the decimal transaction version (§7.2). bytesfields hash askeccak256(contents)per EIP-712; arrays hash askeccak256of the concatenated encoded elements.- Signatures are 65-byte
r ‖ s ‖ vwithv ∈ {0, 1}on the wire. EVM wallets emitv ∈ {27, 28}frometh_signTypedData_v4; normalizing to{0, 1}is the connector’s job (§11.2), and verifiers MUST reject any wirev ∉ {0, 1}. Verifiers MUST reject high-S signatures. Together these give each authorization exactly one valid wire encoding.
7. Transaction EIP-712 Mode
7.1 Wire change
Transaction v2 has the closed framingu8 version=2 ‖ u8 kind ‖ body. The shared standalone decoder accepts exactly two kinds. kind=0 is the ordinary caller-signed transaction, with no inner version byte and this complete canonical field order:
signature_mode is one byte and decoders accept only 0 or 1. fee_payer_override is the commonware Option<Address> encoding: tag 0x00, or tag 0x01 followed by 20 address bytes.
kind=1 is the CIP-33 TradingPostSpawn body. It carries no signature fields. The standalone shared decoder accepts it only for exact codec/corpus reproduction. Every external RPC/mempool submission and every consensus block-validation, import, replay, and execution entry point MUST reject a decoded kind=1 before lane selection, digest use, recovery, state access, event, or execution; a block containing it is invalid even when proposed directly by a Byzantine validator. This release provides no kind-1 producer or capability advertisement and does not activate the CIP-33 spawn path. Every unknown kind and every outer version other than 2 rejects at the outer discriminant.
Ownership is explicit: CIP-40 owns the outer shell, kind-0 body, ChainIdentity, transaction digest,
and disabled-kind policy. CIP-33 owns the exact kind-1 body and any future activation that changes
the disabled policy. CIP-33 §2.6.4 mirrors this contract and pins its fixed-width body; kind 1 does
not inherit kind 0’s minimal-UInt scalar encoding.
Digest and native signing preimage, exact for kind 0:
chain_id names the network and chain_instance_id names the exact genesis. Zeroing covers signature bytes only—signature_mode, the fee_payer_override Option tag/value, and signer addresses remain—so identity is signature-independent but authorization-mode-dependent.
The codec exposes no context-free v2 digest API:
self.chain_id == identity.chain_id. The same immutable ChainIdentity flows through block construction, block verification/replay, mempool admission, execution, RPC, CLI, wasm, FFI, and every transaction producer. The CIP-3 preflight still runs before either mode’s digest computation. Version 2 is the sole transaction encoding specified by this release, and all native/EIP-712 vectors are re-issued over it.
7.2 Typed representation
Domain:version string MUST equal the decimal rendering of CURRENT_TX_VERSION. Bumping the transaction version therefore changes the mode-1 domain separator, and no mode-1 signature is valid across a version boundary. Per §7.1 the mode-1 digest additionally binds the shell’s kind as a struct field (kind=0 is the only signable kind — kind=1 is structurally unsigned); its slot is pinned in the type string below.
Primary type (normative type string, no whitespace deviations):
kind— the shell’s kind byte (§7.1), leading the struct as it leads the wire body. Always0for a signable transaction; binding it makes the §7.1 cross-kind replay rejection derivable from the typehash itself.instruction— the canonical codec encoding of the transaction’sInstruction, byte-identical to whatTransaction::writeemits for that field. This binds the full instruction payload (including nested actor calls, deploy code, amounts) without replicating the enum in EIP-712’s type system.has_fee_payer_override/fee_payer_override— the exact typed projection of CIP-28’s wireOption<Address>.Nonemaps tofalseand the zero address.Some(address)maps totrueand that address, including zero. The boolean binds the Option tag, soNoneandSome(ZERO)cannot share a digest.additional_signers— the transaction’s additional-signer addresses, in the transaction’s canonical (strictly ascending) order. Binding the signer set into the digest preserves the native mode’s property that no signer can be added or removed after signing.chain_idandchain_instance_idappear in the domain, not the struct.- All other fields map one-to-one to the transaction fields of the same name.
access_list and the deferred-execution fields (origin_tx_hash, origin_remaining_cycles, origin_remaining_cells). They are not left unbound: §7.3 forbids them outright in mode 1. When access lists activate, the type string gains an access_list field — a typehash change that cleanly partitions old and new signatures.
7.3 Digest and verification
Transaction::verify(context) is the single verification entry point called from RPC admission, the mempool listener, consensus block verification, execution, and transport dispatch. It branches on signature_mode only after validating the trusted context and shared structural rules.
-
Deferred transactions (
origin_tx_hashpresent) are structurally unsigned. They requiresignature_mode == 0,fee_payer_override == None, a zero primary signature, and an emptyadditional_signerslist. These checks run before fee-payer resolution or state access. -
mode 0: verify against
signing_hash(&context.chain_identity), the §7.1 tagged construction. -
mode 1: the following structural invariants are checked before any recovery, and violation is rejection:
access_listisNone;origin_tx_hash,origin_remaining_cycles, andorigin_remaining_cellsare allNone.
digest(), exactly as a native signature does. Verification then computes the EIP-712 digest above from the transaction’s own fields and runs the identical recovery path: primarysignaturemust recover tofrom; each additional signer’s signature must recover to its stored address, all against the same digest (matching the native mode’s shared-digest multisig semantics).
Transaction::verify first rejects a wire recovery id
outside {0,1}, before any recovery call, and then uses EthSignature::recover_address, whose
high-S rejection remains authoritative. The {0,1} precheck is new; the current helper’s acceptance
of recovery ids 2 and 3 is not relied upon. Structural checks shared by both modes
(MAX_ADDITIONAL_SIGNERS, strict signer ordering) are unchanged, and both modes gain one:
-
fromMUST NOT appear inadditional_signers. Previously unchecked: one key could fill both slots with valid signatures and pass verification, letting a single signer satisfy any consumer that counts1 + additional_signers.len()as a quorum of two (the Council’sHashSetaccumulation is immune only by accident of implementation). The check runs with the other structural invariants, before any recovery in either mode — after the CIP-3 §2.5 common preflight, before the mode digest is computed. Every transaction producer ships the matching contract and rejects a duplicate primary with an error rather than silently de-duplicating — a caller who listed the primary has a confused roster, and silent repair would mask it. The producer contract, per surface, with its concrete error API:Every table row carries one executable rejection test asserting its exact error: RustErr(TransactionSigningError::DuplicatePrimary); PythonDuplicatePrimarySignerError; CLIbuild-unsignedandassembletested separately, each exiting non-zero with no artifact written; every roster-taking wasm export throwingJsError; every mirrored FFI export returning its packed-ABI sentinel (out_ptr == 0) and settingcpw_last_error(). Thesign_transactionpost-derived-fromcollision — parsed JSON clean, derivedfromcolliding with a listed co-signer — is tested on both the wasm and FFI routes. Fixtures: a dedicated invalid corpus atcowboy-protocol/crates/cowboy-protocol-codec/fixtures/commonware-codec-invalid/, layout<case>/input.json+<case>/expected_error.txt— deliberately not the four-file shape of the success corpus at…/fixtures/commonware-codec/, whose harness (…/tests/golden_vectors.rs) assumes successful encodings; the invalid corpus is driven by a dedicated harness alongside it (…/tests/invalid_vectors.rs). Each duplicate-primary vector is otherwise valid — canonical ordering, valid mode structure, andcycles_limit ≥ TX_BASE_CYCLES + k × COSIGNER_VERIFY_CYCLESso the CIP-3 §2.5 minimum is not what rejects it; duplicate-primary is the sole violation, in mode 0 and mode 1, with an instrumented assertion that zero recovery calls occur. The valid deferred case (mode 0, emptyadditional_signers) remains accepted.
digest(chain_identity) — transaction identity — is the §7.1 tagged construction over domain tag, chain identity, and the signature-zeroed v2 encoding. Two transactions identical except for signature_mode have different hashes: they are different authorizations, both well-defined.
7.4 What a wallet displays
A stock EVM wallet renders the domain name (“Cowboy Transaction”), the chain id, and the struct fields — legiblenonce, fee fields, and from, but hex blobs for instruction and metadata. Since the instruction carries the recipient, amount, or deploy payload, mode 1 in a wallet without Cowboy support is blind signing with a domain tag: the user proves which bytes they signed but cannot read what those bytes do. The anti-spoofing property holds — nothing displayed is unbound — but instruction legibility requires a Cowboy-aware signer.
Cowboy’s own wallets recognize the domain, decode instruction with the canonical codec, and render a native approval screen (transfer amount and recipient, actor and handler, deploy summary). Connectors SHOULD render the decoded instruction alongside the wallet prompt, and documentation SHOULD steer high-value signing toward Cowboy-aware wallets until expanded per-instruction typed structs land (§15).
8. Deferred Route-Authentication Signing
Route-authentication EIP-712 forms are not activated or specified by this release. They remain companion work owned by the route-authentication CIP. CIP-40 adds no route-auth version to its activation payload, exposes nosignRouteAuth, and makes no Gateway/Runner/Workload readiness
claim.
9. Deferred Payment Signing
Payment-intent EIP-712 forms are not activated or specified by this release. They remain companion work owned by the payments CIP. CIP-40 adds no payment-auth version to its activation payload, exposes nosignPayment, and makes no payment-admission or settlement readiness claim.
10. Ethereum Facade
A wallet adding Cowboy as a network needs a JSON-RPC URL whoseeth_chainId agrees with the requested chain. The facade provides that narrow onboarding and display surface; it is not an EVM endpoint.
GET /chain-info returns exactly this closed JSON object with Cache-Control: no-store:
chain_id is a canonical unsigned decimal u64 string and never passes through a JavaScript number; the values shown are illustrative, not an allocation. At startup the node independently recomputes and stores the complete §16 release identity, including the external manifest digest; this handler reads that trusted local result and MUST NOT echo request data or an unchecked configured digest. /chain-info is configuration discovery, not a self-authenticating state proof. The connector’s trust anchor is separately release-pinned metadata obtained with the application/release, and HTTPS; it compares every configured mirror to that expected identity and to every other mirror. Missing, extra, duplicate, reordered mode, wrong-type, wrong-case, or unequal identity/version fields make the connector refuse to sign.
GET /chain-info and POST /eth are deliberately public, read-only browser endpoints. Successful and error responses on those exact paths include Access-Control-Allow-Origin: * and never include Access-Control-Allow-Credentials. Connector requests use credentials: "omit". An OPTIONS request to /chain-info returns 204 with Access-Control-Allow-Origin: *, Access-Control-Allow-Methods: GET, OPTIONS, and Access-Control-Allow-Headers: content-type; /eth returns the same headers with POST, OPTIONS. No neighboring authenticated RPC endpoint inherits this CORS policy.
10.1 Endpoint
POST /eth on the existing REST RPC server accepts a UTF-8 application/json JSON-RPC 2.0
body. The root is one request object or a nonempty array of at most 16 request objects. Each request
has exactly jsonrpc:"2.0", a method string, optional params, and optional id; an id is a JSON
string or I-JSON integer and is echoed unchanged. An absent id is a notification and produces no
response entry. No-parameter methods accept absent params or exactly []; every other method
requires the exact positional array below. Duplicate keys, extra request members, non-I-JSON
numbers, or trailing JSON reject as an invalid request.
Every response object has exactly jsonrpc:"2.0", the request id, and one of result or
error. An error is exactly {"code":<integer>,"message":<string>} with no data: parse error
(-32700,"Parse error"), invalid request (-32600,"Invalid Request"), method not found
(-32601,"Method not found"), invalid params (-32602,"Invalid params"), or internal error
(-32603,"Internal error"). JSON-RPC success and error bodies use HTTP 200. A malformed root,
empty batch, or batch larger than 16 returns one invalid-request object with id:null and executes
nothing. Batch responses preserve request order while omitting notifications; an all-notification
batch returns HTTP 204 with an empty body.
QUANTITY is lowercase 0x hexadecimal with no leading zero (0x0 is zero). DATA<n> is 0x
plus exactly 2n lowercase hex digits. Input addresses are 0x plus 40 case-insensitive hex
digits. FINALIZED_TAG is exactly one of "latest", "safe", or "finalized"; all three resolve
to the same locally finalized Cowboy head. Every other tag, including "pending", "earliest"
and numeric block tags, is invalid params.
The compatibility block result has exactly these members.
block_hash, parent, tx_root,
state_root, receipt_root, height, and timestamp come from the same finalized Cowboy block.
ZERO8, ZERO20, ZERO32, and ZERO256 are zero-filled DATA<n> values. This is a typed wallet
liveness object, not an Ethereum block or an EVM commitment.
eth_getBlockByNumber returning baseFeePerGas: null signals a pre-EIP-1559 network, so wallets fall back to legacy fee display and never attempt fee-market calls in earnest. The facade is read-only and stateless. It MUST NOT implement eth_sendTransaction, eth_sendRawTransaction, eth_call, or eth_estimateGas — a wallet or tool that submits an Ethereum-format transaction gets a clean method-not-found, not a silent misinterpretation.
10.2 Balance scaling
CBY has 9 protocol decimals (1 CBY = 10⁹ Cowboy wei). The supported-wallet metadata usesnativeCurrency.decimals = 18, so eth_getBalance returns checked(balance_wei9 × 10⁹) as a JSON-RPC quantity. The multiplication uses at least u128 or arbitrary-precision arithmetic and never narrows through u64. This is a facade display convention only.
10.3 Chain id uniqueness
In theeip155 namespace, chain ID 1 is Ethereum mainnet. A Cowboy network exposing the facade MUST use one noncolliding ID allocated in the release-pinned docs/cips/registries/cip-40-chain-ids-v1.json. The registry is the closed object {"allocations":[...],"format_version":1}. Each allocation is the closed object {"allocation_pr":N,"chain_id":"D","network":"S","public":B}: N is a JSON integer in 1..=2^32-1 naming the allocating cowboyinc/cowboy pull request; D is a nonzero canonical unsigned decimal u64 string; S matches [a-z][a-z0-9-]{0,31}; and B is a JSON boolean. Rows are strictly ascending by the numeric u64 value of D; both D and S are globally unique, and existing rows are immutable. The file bytes are RFC-8785 JCS of that object followed by exactly one LF; the LF is file framing and is excluded from any JCS digest or equality check. This revision is intentionally empty, so the illustrative 26909 cannot be used for a production release until a separately reviewed row is added and collision-checked against a pinned ethereum-lists/chains checkout.
Wallet display metadata is a separate release-pinned object containing the allocated chain ID, chain name, 18-decimal CBY metadata, HTTPS /eth RPC URLs, and optional explorer URLs. Before asking a provider to add or switch networks, the connector validates every listed RPC’s eth_chainId and /chain-info response against that metadata, the expected activation-manifest digest, and each other. One failed or divergent mirror rejects the operation.
11. WalletConnect Integration
11.1 Provider state machines
Injected and WalletConnect providers have different onboarding state machines. For an injected EIP-1193 provider, the connector first validates the release-pinned network metadata and every direct facade mirror, reads the provider’s activeeth_chainId, and requests the EIP-3326 method wallet_switchEthereumChain when it differs. Only its exact unknown-chain ProviderRpcError.code = 4902 permits a subsequent EIP-3085 wallet_addEthereumChain; 4001 and every unmapped error fail without an add request. After adding, the connector requests the switch again and re-reads eth_chainId before signing.
For WalletConnect, dapps request a standard eip155 session:
- Chains:
eip155:<cowboy_chain_id>per network the dapp supports. - Methods:
eth_signTypedData_v4(required). No transaction-sending methods are requested. - Events:
accountsChanged,chainChanged.
domain.chainId, /chain-info.chain_id, domain salt, and connector chain_instance_id MUST agree. Account/chain/session changes invalidate pending typed data.
11.2 Connector
@cowboyinc/connect is the dapp-side integration surface. This release exposes connect, getAccount, and signTransaction over injected and WalletConnect transports; it adds no arbitrary-message, route-authentication, or payment-signing method. For each signature operation it:
- Validates every configured facade mirror and the release-pinned activation-manifest digest, then freezes the resulting
ChainIdentityfor the operation. - Builds canonical canyon JSON without routing any
u64through JavaScriptnumber. - Calls the canonical wasm export with that exact
ChainIdentityto produce the full typed object, then checks the returned domain identity and bound account. - Requests exactly
eth_signTypedData_v4for the approved account. - Accepts exactly 65 bytes, leaves
v=0/1unchanged, maps27/28to0/1, rejects every other value, attaches mode 1, re-verifies locally throughTransactionVerificationContext, encodes, and submits.
11.3 Cowboy wallets
The iOS wallet integrates Reown WalletKit. A request is recognized as Cowboy only when domain name/version/chain/salt,primaryType, exact type/member sets, and bound account match the pinned schema; malformed near-misses using a reserved Cowboy domain hard-reject. For a recognized transaction, the wallet decodes and re-encodes the exact signed instruction bytes before rendering them. Stock-wallet mode remains blind signing with a domain tag (§7.4).
11.4 SDK surface
New canonical exports incowboy-protocol-wasm (mirrored in the C-ABI FFI crate, as the export surfaces are kept in lockstep):
Rust, wasm, C ABI, Swift FFI, Python, CLI, and JS transaction bindings accept the same explicit
ChainIdentity; every verification binding accepts TransactionVerificationContext. Typed-data JSON is generated only by the canonical implementation—connectors and wallets MUST NOT hand-assemble it.
12. Security Considerations
- Domain and instance separation. Native v2 and EIP-712 use structurally distinct prefixes, and
both bind the same immutable
ChainIdentity. Same-chain-ID/different-instance and cross-mode mutations therefore fail. - Complete transaction binding. The typed struct plus §7.3 invariants cover every canonical
field. Structural checks run in
verify(context)before recovery at every admission point. - Version and kind policy. The version/kind bytes are inside the native preimage; mode 1 binds version through its domain and kind through its message. Kind 1 may decode in the standalone codec but is invalid in every external and consensus path in this release.
- Option-tag binding.
has_fee_payer_overridebinds the wire Option tag, preventingNonefrom sharing a digest withSome(ZERO). - Signature canonicality. Verifiers reject high-S signatures and every wire recovery id outside
{0,1}; connectors normalize only provider27/28. - Signed bytes are the authorization. A stock wallet cannot interpret the opaque instruction. This release provides cryptographic interoperability, not a human-readable stock-wallet consent claim. Cowboy-native rendering is trusted only after exact decode→re-encode.
- Facade misuse. The facade refuses transaction-sending and EVM-execution methods. An Ethereum-format transaction is never interpreted as a Cowboy submission.
- Release identity. Connectors compare every configured
/chain-infomirror to one pinned activation-manifest digest before building typed data. Chain/provider/session changes invalidate the operation.
13. Protocol Constants
14. Release Corpus
This document freezes the corpus schema and minimum matrix. It does not publish placeholder v2 bytes or hashes. Before release, implementation-generated vectors are independently recomputed and pinned in every consumer. A successful kind-0 vector contains:input.jsonandchain_identity.json; the latter is exactly the closed object{"chain_id":"<canonical u64 decimal>","chain_instance_id":"0x<64 lowercase hex>"};- exact
preimage.bin,signing_hash.hex, andsubmission.bin; - for mode 1, exact
typed_data.json,domain_separator.hex,typehash.hex,struct_hash.hex,digest.hex, andsignature.hex.
.bin file is raw bytes. Every .hex file is lowercase hexadecimal without 0x and with
exactly one trailing LF; hashes contain 64 digits and signatures 130. JSON files are UTF-8, closed
schema, duplicate-key-free, and end in one LF. typed_data.json is the exact object passed to
eth_signTypedData_v4, not a hand-built equivalent.
A decode-only kind-1 fixture instead contains exactly input.json, submission.bin, and
expected_rejection.txt. It has no signing preimage, signing hash, typed-data object, or
signature. The shared codec must decode and re-encode it byte-for-byte; RPC, mempool, directly
proposed block, block import, replay, and execution fixtures must all reject it before lane/state
use. Unknown kinds reject at the outer discriminant before body decode.
The kind-1 expected_rejection.txt contains exactly TxKindDisabled(1)\n; every boundary harness
asserts that semantic rejection after successful decode. A raw decode-failure fixture contains
exactly submission.bin and expected_decode_error.txt; the text file contains one stable codec
error token plus LF. Decode failures are not mislabeled as disabled-kind failures.
The kind-0 matrix covers native and EIP-712 transfer, actor execution, multisig, all three
fee-override encodings (None, Some(ZERO), Some(nonzero)), maximum-width lossless integer
JSON, provider v normalization, duplicate-primary rejection, wrong chain/instance/domain,
version/kind/mode mutation, malformed Option tags, high-S, invalid recovery id, deferred-mode
invariants, and signature-byte-independent identity. An independent EIP-712 implementation consumes
the canonical wasm-emitted object.
The release matrix exercises protocol codec, node RPC/mempool/block verification/replay/execution,
CLI, wasm, FFI, Python/JS SDKs, connector, wallet, indexer, and explorer. It also pins the §16
JCS/identity fixture; the closed /chain-info schema; one-divergent-mirror rejection; injected
provider switch/add behavior including 4902 and 4001; WalletConnect session invalidation; and
the facade’s exact request/result/error schemas, batch and notification behavior, quantities,
finalized-tag aliases, complete compatibility-block object, balance scaling, method refusal,
endpoint-scoped CORS, credential omission, and preflights. Tests also prove that an adjacent
authenticated RPC route receives none of these permissive CORS headers and that a configured but
non-recomputed manifest digest cannot be served as local chain identity. Registry vectors cover
empty, one-row, numeric sorting ("9" before "10"), duplicate ID/network, extra member, and
missing/excess final-LF cases.
15. Rationale
- Generic
bytes instructionover per-instruction typed structs. Expanded structs would make stock-wallet prompts legible, but each adds another reconstruction path for a large instruction enum. The generic form gives one verifier and full cryptographic binding. Stock-wallet blind signing remains an explicit product/security acceptance gate; a connector-rendered summary is not part of the signed authorization. - Structural invariants over a catch-all hash field. The alternative to forbidding
access_list/origin_*in mode 1 was binding them via an extrabytes32 tx_extrafield. The invariant approach was chosen because the fields are semantically meaningless in a caller-signed, non-deferred transaction today — forbidding them costs nothing, keeps the typed struct legible, and the typehash-change mechanism handles their future activation. eip155over acowboyCAIP namespace. A native namespace would require wallet-specific support.eip155+ typed data reuses provider machinery in wallets that support the Cowboy chain; §11 does not claim universal compatibility. The facade answers only the pinned read-only subset and refuses transactional methods.- Mode as a per-transaction field, not per-signer. Per-signer modes would let a MetaMask co-signer join a natively-signed multisig, at the cost of a mode vector in the encoding and a per-signer digest branch in verification. Deferred until a concrete need exists; the typed struct already binds the signer set, so adding per-signer modes later is a version bump, not a redesign.
16. Clean-Genesis Release Contract
This release starts from one clean genesis and defines no state-conversion procedure. The immutableCip40ActivationV1 payload is a closed JSON object with exactly these six members:
genesis_config_sha256 hashes the exact separately pinned genesis-input bytes. Those bytes
completely determine validator/consensus configuration and height-zero application state except for
the payload-derived genesis parent. make_genesis_v2 rejects a mismatch before construction.
Duplicate keys, non-I-JSON input, missing/extra members, or values outside the grammars reject before
RFC 8785 canonicalization. Derivations consume raw digest bytes:
activation_payload_hash and chain_instance_id.
activation_manifest_digest = SHA-256(RFC 8785 JCS(final_manifest)) is an external release pin,
not a manifest member. Keccak-256 is the Ethereum function, not FIPS SHA3-256.
The following fixture is normative for JCS and hash construction. Its genesis-input bytes are the 31
ASCII bytes {"accounts":[],"validators":[]} with no newline. 26909 is illustrative and
unallocated, so it reproduces the bytes but fails the separate production allocation gate.
/chain-info implementation is not
yet this contract: its current two-member response and numeric chain_id must be replaced by the
closed six-member §10 object before connector release.
The release set is deliberately narrow:
- land CIP-40, CIP-28’s reciprocal field/type mapping, CIP-33’s reciprocal decoder policy, the Technical Whitepaper §2/Appendix A mirror, the changelog, and the chain-ID registry artifact;
- allocate a production
(network, chain_id)row separately, derive one clean genesis, and start validators only after every node agrees on chain identity, manifest digest, and v2 codec; - deploy protocol codec, node, CLI, wasm/FFI, SDKs, connector, wallet, indexer, and explorer only after the §14 corpus passes at each real sign/verify/import boundary.
fee_payer_override encoding; CIP-28 retains all Bank behavior. Kind 1
remains decoder-only and consensus-invalid until a later CIP-33 release explicitly changes that
policy.
