Skip to main content
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

  1. 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.
  2. 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.
  3. Ecosystem leverage. Signing via the eip155 namespace 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.
  4. 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_mode does, 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-codec and 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 eip155 WalletConnect sessions and eth_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 a cowboy namespace adds no capability over eip155 + 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 Transfer type) are a candidate follow-up once the generic mode is proven; see §15.
  • Mixed-mode multisig. signature_mode applies 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 u8 discriminant 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_id names the network. chain_instance_id names 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), with salt = chain_instance_id. No verifyingContract is 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

The submission path, mempool, consensus, and execution are identical for both modes; only the digest recomputed at verification differs.

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). chainId and salt = chain_instance_id come from one closed GET /chain-info response and are treated as one identity. The domain version is the decimal transaction version (§7.2).
  • bytes fields hash as keccak256(contents) per EIP-712; arrays hash as keccak256 of the concatenated encoded elements.
  • Signatures are 65-byte r ‖ s ‖ v with v ∈ {0, 1} on the wire. EVM wallets emit v ∈ {27, 28} from eth_signTypedData_v4; normalizing to {0, 1} is the connector’s job (§11.2), and verifiers MUST reject any wire v ∉ {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 framing u8 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:
The pair is required: 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:
Both methods reject unless 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:
The 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):
Field mapping:
  • kind — the shell’s kind byte (§7.1), leading the struct as it leads the wire body. Always 0 for 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’s Instruction, byte-identical to what Transaction::write emits 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 wire Option<Address>. None maps to false and the zero address. Some(address) maps to true and that address, including zero. The boolean binds the Option tag, so None and Some(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_id and chain_instance_id appear in the domain, not the struct.
  • All other fields map one-to-one to the transaction fields of the same name.
The typed struct deliberately omits 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_hash present) are structurally unsigned. They require signature_mode == 0, fee_payer_override == None, a zero primary signature, and an empty additional_signers list. 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_list is None;
    • origin_tx_hash, origin_remaining_cycles, and origin_remaining_cells are all None.
    These invariants close the fields the typed struct does not carry: with them, every byte of the canonical encoding is either bound in the EIP-712 digest, fixed by invariant, or a signature slot — so a mode-1 signature determines the full transaction and its 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: primary signature must recover to from; 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).
For every primary and additional signature, 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:
  • from MUST NOT appear in additional_signers. Previously unchecked: one key could fill both slots with valid signatures and pass verification, letting a single signer satisfy any consumer that counts 1 + additional_signers.len() as a quorum of two (the Council’s HashSet accumulation 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: Rust Err(TransactionSigningError::DuplicatePrimary); Python DuplicatePrimarySignerError; CLI build-unsigned and assemble tested separately, each exiting non-zero with no artifact written; every roster-taking wasm export throwing JsError; every mirrored FFI export returning its packed-ABI sentinel (out_ptr == 0) and setting cpw_last_error(). The sign_transaction post-derived-from collision — parsed JSON clean, derived from colliding with a listed co-signer — is tested on both the wasm and FFI routes. Fixtures: a dedicated invalid corpus at cowboy-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, and cycles_limit ≥ TX_BASE_CYCLES + k × COSIGNER_VERIFY_CYCLES so 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, empty additional_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 — legible nonce, 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 no signRouteAuth, 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 no signPayment, and makes no payment-admission or settlement readiness claim.

10. Ethereum Facade

A wallet adding Cowboy as a network needs a JSON-RPC URL whose eth_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 uses nativeCurrency.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 the eip155 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 active eth_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.
The WalletConnect session can succeed only when the wallet already supports or has provisioned the Cowboy chain; the facade cannot generically add an unknown chain before session approval. A wallet-specific out-of-session onboarding flow MAY provision it, after which the connector creates a fresh Cowboy session. Before every signature, the approved CAIP account chain, provider active chain, EIP-712 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:
  1. Validates every configured facade mirror and the release-pinned activation-manifest digest, then freezes the resulting ChainIdentity for the operation.
  2. Builds canonical canyon JSON without routing any u64 through JavaScript number.
  3. Calls the canonical wasm export with that exact ChainIdentity to produce the full typed object, then checks the returned domain identity and bound account.
  4. Requests exactly eth_signTypedData_v4 for the approved account.
  5. Accepts exactly 65 bytes, leaves v=0/1 unchanged, maps 27/28 to 0/1, rejects every other value, attaches mode 1, re-verifies locally through TransactionVerificationContext, 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 in cowboy-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_override binds the wire Option tag, preventing None from sharing a digest with Some(ZERO).
  • Signature canonicality. Verifiers reject high-S signatures and every wire recovery id outside {0,1}; connectors normalize only provider 27/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-info mirror 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.json and chain_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, and submission.bin;
  • for mode 1, exact typed_data.json, domain_separator.hex, typehash.hex, struct_hash.hex, digest.hex, and signature.hex.
Every .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 instruction over 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 extra bytes32 tx_extra field. 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.
  • eip155 over a cowboy CAIP 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 immutable Cip40ActivationV1 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:
The final manifest is the same object plus exactly two lowercase bytes32 strings, 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.
Node startup recomputes the payload hash, chain instance, genesis parent, final-manifest bytes, and external manifest digest and refuses a mismatch. The deployed /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:
  1. 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;
  2. 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;
  3. 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.
Route-authentication, payment, governance, storage, session, banking admission, and actor-fork redesigns are not activation dependencies or readiness claims of this release. CIP-40 owns only the outer transaction wire’s 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.