Status: Draft
Type: Standards Track
Category: Core
Created: 2026-10-25
Requires: CIP-40 auth v3 (transaction v3, signature slots §7.3, signed objects §7.6, tagged signing keys §7.7, wallet identity §12.1), CIP-3 (transaction charging, §2.5 signature metering), CIP-4 (state prefixes), CIP-43 (signed-transaction envelope accesses)
Related: CIP-37 (protocol
Type: Standards Track
Category: Core
Created: 2026-10-25
Requires: CIP-40 auth v3 (transaction v3, signature slots §7.3, signed objects §7.6, tagged signing keys §7.7, wallet identity §12.1), CIP-3 (transaction charging, §2.5 signature metering), CIP-4 (state prefixes), CIP-43 (signed-transaction envelope accesses)
Related: CIP-37 (protocol
ActorOwner), CIP-41 (multi-signer authorization)Cowboy Cloud and Cowboy Chain. This CIP specifies the Cowboy protocol. Cowboy Cloud runs on this protocol today. Cowboy Chain, a public network on the same protocol, is under consideration for a future release. Where the two differ, this CIP says so.
1. Background
A Cowboy account address is the last 20 bytes of the keccak256 hash of its secp256k1 public key, EVM-style. Without an authorization policy, transaction verification recovers the public key from each signature slot and requires the derived address to equal the slot’s declared address. The key is the account. There is no way to replace it, and no way for an ordinary account to require more than one key. Losing the key therefore loses the account and everything it controls: balances, tokens, CBFS volumes, delegations, and every actor whose protocolActorOwner (CIP-37 §7.1.1) is that account. ActorOwner cannot be transferred and its website sharing cannot be changed without a signature from the owning account. A leaked key is equally final: the owner can move assets out, but the address, and anything bound to it by other accounts or actors, stays compromised.
Off-chain wallet backup (encrypted key backup and CBSS-based restore in the Dashboard) covers a lost device. It does not cover a key that is gone everywhere, and it cannot help when the key itself must be replaced because it leaked.
2. Goal
An account address is a stable identity that outlives any one key.- The account is controlled by an authorization policy: one key, or a threshold of a set of keys. Ordinary accounts get native multisig.
- The current policy can replace itself (
SetAuthPolicy). - An account can name a recovery account that can replace the policy after a delay, during which the current policy can cancel. A recovery account with a k-of-n policy is guardian recovery.
- Accounts that never set a policy behave exactly as today and pay nothing extra beyond one state read per signer.
- Everything sits on the CIP-40 transaction v3 model: the same signature slots, the same signing-digest construction, the same tagged key encoding.
2.1 CIP-40 dependency
This CIP builds on the CIP-40 auth v3 text. It uses: theSignatureSlot and slot-policy constants including SECP256K1_OBJECT_POLICY and TX_SLOT_POLICY (§7.6); object_signing_preimage framing (§7.6); the signed-object table and its row numbers (§7.6: rows 1 RasOwnerDelegation, 7 workload DelegationCert, 9 ReadTransferReceipt authorization and completion, 19 session vouchers, 22 PaymentIntent, 24 account Intent, 26 TokenPermit); SigningPublicKey, canonical_signing_preimage, storage_key_fragment() and KEY_VALIDATION_CYCLES (§7.7).
3. Non-goals
- New signature algorithms. At genesis
TX_SLOT_POLICYadmits secp256k1 only, so policy keys are secp256k1. If the transaction policy later admits another algorithm (for example P-256 passkeys), a policy can include it with no change here. - Undoing account actions. A policy change alters which keys authenticate the account. State that earlier transactions created (grants, transfers, approvals recorded on chain) stays as it is; those were the account’s own actions.
- User-actor signature checks. Actor code that compares an
ecrecoverresult to an account address is not policy-aware, and user actors get no policy read. Actors authorize from the transaction sender and CIP-41 co-signers. - Address-authorized objects. Some wallet-signed objects still verify against the key the address derives from, and ignore the account policy: CBFS read-channel vouchers and requests (CIP-40 §7.6 rows 8 and 30, which keep address authorization), workload mount-policy owner signatures, purchase authority (row 23) and the CIP-34 auction grant originator (row 25). A policy change does not revoke a leaked original key for these objects, and an account on a k-of-n policy cannot sign them. Moving them to
AccountSignatureis separate work (§9).
4. Definitions
- Authorization policy.
AuthPolicy { threshold, keys }(§5.1): the account authorizes a signature whenthresholddistinct keys fromkeyssign. AddressDerived. The policy of an account with noAccountAuthrow: one signature by the key whose derived address equals the account address. The key itself is not stored or known until it signs.AccountSignature. An account’s signature on a transaction or signed object: a list of key-indexed signature slots (§5.2).AccountAuth. The per-account consensus row defined in §5.1.- Recovery account. An account, named by address, whose signed transactions may start and complete recovery of another account. It is an ordinary account with its own policy, rotation and recovery.
- Auth revision.
AccountAuth.revision, a counter incremented by every change to the row. It guards instructions against stale approvals. An absent row has revision zero. - Policy revision.
AccountAuth.policy_revision, a monotonic counter incremented only when the policy changes (SetAuthPolicy,CompleteRecovery). Every account signature names it. An absent row has policy revision zero.
5. Specification
5.1 State
A new state prefixAccountAuth (0x26, CIP-4 §4.2) uses the key prefix ‖ address[20] ‖ zeros in the same fixed-length layout as WalletMark. The value is a new canonical record:
1 <= threshold <= keys.len() <= MAX_POLICY_KEYS, every key uses an algorithm in TX_SLOT_POLICY (secp256k1 at genesis), and keys is strictly ascending by storage_key_fragment() (CIP-40 §7.7), which also makes the keys distinct. A single-key account is the policy { threshold: 1, keys: [k] }; there is no separate single-key form.
Only the instructions in §5.3 write this row. Actor code, deferred transactions and genesis configuration cannot write it. A row, once created, is never deleted. A malformed row is a storage error and never reads as absent.
A row is created by the first SetAuthPolicy or SetRecovery on an account. SetRecovery on an AddressDerived account, including SetRecovery(None), stores { threshold: 1, keys: [k] } with auth revision 1 and policy revision zero, where k is the key recovered from the transaction’s own primary signature, so a row always holds an explicit policy. This is the same authorization made explicit, so the policy revision does not change.
The row’s wire form starts with the version byte 1; any other version is a decode error.
5.2 Policy verification
The address rule for creating an account does not change: a new account’s address is derived from its initial key. From then on the address is frozen. It never changes when the policy changes.AddressDerived carries no key: the public key of an account that has never set a policy may never have been revealed. Its policy revision is zero, and a verifier recovers the key from the supplied signature and checks that it derives to A.
An account’s signature is an AccountSignature:
policy_revision is part of the signed message. For transactions, each signer entry’s policy_revision and key_index list are bound into both digests (§5.10). For native account-signed objects (item 2 below), every slot signs object_signing_preimage(domain, identity, body ‖ account[20] ‖ u64be(policy_revision)), the row’s ordinary body followed by the signing account and the policy revision. Row 19 is EIP-712 and takes no native framing; it binds the same two fields inside its typed struct (§5.2.1).
On the wire, policy_revision is a varint followed by the entry list; an empty list is a decode error. It is structurally valid iff entries has between 1 and MAX_POLICY_KEYS items, key_index is strictly ascending, every slot passes its surface’s slot policy, and, when policy_revision is zero, there is exactly one entry at index 0. Revision zero can only ever be satisfied by the key the account address derives from: either AddressDerived, or the stored 1-of-1 that SetRecovery makes explicit from that same key. A verifier can therefore check a revision-zero signature without reading state. It satisfies account_policy(A) iff:
policy_revisionequals the account’s current policy revision;- for
AddressDerived: exactly one entry, at index 0, whose recovered key derives toA; - for
Stored: exactlythresholdentries, everykey_index < keys.len(), and the key recovered from each slot equalskeys[key_index].
- Transactions. The primary signer and every additional signer (CIP-41) carry an
AccountSignature. The sponsor field is reserved at genesis and keeps a singleSignatureSlot; when CIP-40 enables sponsorship, the sponsor signature becomes anAccountSignatureunder the same rules. A transaction carries at mostMAX_TX_SIGNATURE_SLOTSslots in total, checked in the structural check before any recovery. Transaction signature verification is stateless: it checks structure, recovers every slot and returns each signer’s address, policy revision and(key_index, recovered key)list, keys in the §7.7 33-byte compressed form. It also rejects a revision-zero signer whose single key does not derive the declared address. Satisfaction againstaccount_policyis part of the stateful admission checks that CIP-40 §7.3 and §12.1 already run against the current execution view (wallet/actor role, nonce, balance), and fails withAccountPolicyUnsatisfied. A signer that isAddressDerivedat revision zero needs no further work. For any other signer, a transaction that was pre-verified at block proposal is recovered again during this check, so pre-verification never skips the policy check. RPC ingress runs the same check against committed state before accepting a transaction. - Account-signed objects. CIP-40 §7.6 marks each row whose signer is a wallet account (among them row 1
RasOwnerDelegation, row 7 workloadDelegationCert, row 9ReadTransferReceiptauthorization and completion, row 19 session vouchers, row 22PaymentIntent, row 24 accountIntent, row 26TokenPermit). Each marked row carries anAccountSignaturein place of its single slot, and its body MUST name the signing account, so the verifier knows whose policy to resolve and an object cannot be replayed against a second account that shares keys. Each slot signs the same digest. Rows signed by non-accounts (CBSS proxies, runner anchors, the credit authority) are unaffected. - Where objects are verified. Node verifiers read
account_policyfrom the current execution view. The Session actor and Trading Post verify row 19 in native execution code and read the policy from the same store; user actors get no policy read. Off-chain verifiers (CBFS relays, runners, gateways) resolve the policy through the finalized-state proof in §5.7, verified against a trusted checkpoint, and fail closed when they cannot.
5.2.1 EIP-712 session rows
Row 19 (session vouchers, per-call receipts and route session reports) is the only EIP-712 row in the CIP-40 §7.6 inventory. Its account-signed objects are exempt from the native framing above. Its domain is unchanged:EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt) with name Cowboy MPP Session, version 1, the real chain ID, salt = chain_instance_id, and verifyingContract set to the Session actor for vouchers or the Trading Post for per-call receipts. That keeps the chain, genesis-instance and surface separation exactly as CIP-40 defines it.
Row 19 has a single typed struct. All three objects are a CIP-8 SessionVoucher hashed under the one Voucher type; they differ only in verifyingContract and in who signs:
For a session voucher,
account is session.signer and the voucher is checked against that account’s policy. The payer only funds, closes and receives the refund; its policy plays no part in voucher verification unless it is also the signer. A route session report is signed by the route-metering key bound into the session, so CIP-49 does not change it; that key’s lifecycle belongs to the session binding.
For the two account-signed objects the Voucher type appends address account and uint64 policy_revision after its existing members:
verifyingContract keeps them apart, as today. Because route session reports share the struct, they use the same extended type with account set to the zero address and policy_revision zero; their verifier requires account = 0, policy_revision = 0 and a single entry at index 0, then checks the metering key as before. There is no other row-19 type string to extend. account is the signing account and MUST equal the account the verifier attributes the object to; policy_revision MUST equal the AccountSignature’s. Every slot is a SECP256K1_EIP712_OBJECT_POLICY slot over the same final digest keccak256(0x19 ‖ 0x01 ‖ domainSeparator ‖ hashStruct(struct)), verified unchanged as an EIP-712 digest. A voucher signed under an earlier policy revision therefore fails even when its keys remain in the current policy.
The declared address in every signer entry, and Signer.signer in the EIP-712 projection, remain the account address. The digests bind every signer’s key_index list and slot descriptors and exclude signature values, as CIP-40 already does for descriptors, so entries cannot be added, removed or re-indexed after signing.
A key may appear in the policies of several accounts, including the account at its own derived address. Each is a separate account with its own nonce, balance and row.
5.3 Instructions
All five are system instructions carried by an ordinary caller-signed transaction v3. They MUST be the transaction’s top-level instruction; actors cannot invoke them. Deferred transactions cannot carry them. Every one is authorized by the sender’sAccountSignature satisfying the sender’s current policy, like any transaction. Each except CancelRecovery takes expected_revision and rejects with AuthRevisionMismatch unless it equals the current auth revision of the affected account; each successful instruction increments the revision by one.
Opcodes 231 and 232 are CIP-37’s
SetWebAccess and TransferActorOwnership.
Key proofs. key_proofs has exactly one entry per key of the proposed policy, in key order. Entry i is a CIP-40 §7.6 signed object under SECP256K1_OBJECT_POLICY, domain cowboy/account/key-proof/v1, body account[20] ‖ u64be(expected_revision) ‖ canonical_signing_preimage(keys[i]), and MUST recover to keys[i]. The proofs show every key exists and is held, so a typo or a foreign public key cannot brick the account. The trusted chain identity in the object frame, the account and the revision prevent replay onto another chain, account or revision. The handler validates the policy shape first, then charges KEY_VALIDATION_CYCLES[algorithm] and validates each key before verifying its proof (CIP-40 §7.7).
SetAuthPolicy. Replaces policy and increments policy_revision. If a recovery is pending, clears it and emits RecoveryCanceled { reason: PolicyChanged } before AuthPolicyChanged { cause: Set }.
SetRecovery. Some(config) requires config.account != account, config.account present in the transaction’s additional_signers with an AccountSignature satisfying its own policy (CIP-41), and MIN_RECOVERY_DELAY_SECS <= delay_secs <= MAX_RECOVERY_DELAY_SECS. The co-signature proves the recovery account exists and is held, and CIP-40 §12.1 admission already rejects actor and system addresses as signers and marks the recovery account as a wallet. None removes the recovery account. Either form clears a pending recovery (emitting RecoveryCanceled { reason: Reconfigured }) and takes effect immediately. Emits RecoveryConfigured.
InitiateRecovery. Checks, in order: expected_revision, a configured recovery account equal to from (NotRecoveryAccount; an account with no row has none), no pending recovery (RecoveryPending), then the policy and key proofs. Writes pending with the proposed policy and executable_at_ms = the executing block’s timestamp + delay_secs × 1000, using the delay configured at initiation. Emits RecoveryInitiated.
CancelRecovery. Requires a pending recovery (NoPendingRecovery). Clears it. The current policy may cancel at any time until CompleteRecovery executes, including after executable_at_ms. Emits RecoveryCanceled { reason: Owner }.
CompleteRecovery. Requires from to equal the configured recovery account, a pending recovery, and the executing block timestamp >= executable_at_ms (RecoveryNotReady). Sets policy = pending.new_policy, increments policy_revision, clears pending, keeps recovery unchanged. Emits AuthPolicyChanged { cause: Recovery }.
Errors and their structured codes:
A failed instruction follows the normal CIP-3 failure path: nonce and fees are consumed and the row is unchanged.
5.4 Guardian recovery
Guardian recovery is a recovery account whose own policy is k-of-n over the guardians’ keys.InitiateRecovery and CompleteRecovery from that account need k guardian signatures in its AccountSignature, exactly as any transaction from a k-of-n account does; guardians assemble them with the CIP-41 §4.4 co-signing flow. Nothing in this CIP is guardian-specific. Changing the guardian set is a SetAuthPolicy on the recovery account, authorized by its current quorum.
5.5 Nonces and replay
The five instructions use the sender’s ordinary account nonce and CIP-40’s chain-identity-bound transaction digest. No separate nonce exists. Initiation and completion consume the recovery account’s nonce and are paid by it. The recovered account’s nonce and balance are untouched. The account nonce continues across policy changes and recovery. Every account signature names the policy revision it was made under (§5.2), so a transaction signed before a policy change can never be admitted afterwards: if it was included its nonce is consumed, and if it was withheld its policy revision is stale. This holds even when the new policy keeps some of the old keys. Recovery therefore does not need to advance the nonce; the policy revision does that job.expected_revision makes every auth change except cancellation compare-and-set, so a wallet approval prepared against one state cannot apply to another; an InitiateRecovery prepared before a SetRecovery cannot apply after it. CancelRecovery carries no revision: at most one recovery is pending, and canceling whichever one is pending is always what the owner wants.
5.6 In-flight transactions and objects
A transaction whose signatures no longer satisfy the account’s policy fails the stateful check in §5.2. It is an admission rejection, handled exactly like a signature that recovers to the wrong address: no nonce, fee or wallet marker is consumed. The same holds for a transaction signed under a new policy before the change that installs it has executed. Ordering inside a block follows the execution view: a policy change at position i invalidates that account’s transactions signed under the old policy at positions after i. Block producers MUST evaluate the check against the speculative state of the block being built. RPC ingress checks against committed state. Mempools MUST re-check pending transactions of an account (as primary or co-signer) when itsAccountAuth row changes, and evict those that no longer satisfy it.
Signed objects (§5.2 item 2) are checked against account_policy every time they are verified. A policy change, by SetAuthPolicy or by recovery, advances the policy revision and so revokes every outstanding object signed under an earlier revision, including objects signed by keys that remain in the new policy and standing objects re-verified on each use such as RAS owner delegations and CBFS DelegationCerts. The owner re-signs the ones still wanted under the new policy; wallets SHOULD re-issue the account’s live delegations as part of a routine change.
5.7 Events and reads
Events are emitted under the affected account’s address so a watcher filters by its own account.RecoveryConfigured carries delay_secs = 0 when the recovery account is removed. RecoveryInitiated and the AuthPolicyChanged from completion are emitted under the recovered account, even though the recovery account sends them.
RPC exposes two reads:
GET /accounts/{address}/authreturns the committed height and the row in one flat object:{ "height": h, "kind": "AddressDerived" }(no row; revision and policy revision zero, no key) or{ "height": h, "kind": "Stored", "revision", "policy_revision", "policy", "recovery", "pending" }. Policy keys are 33-byte compressed hex. This read is unproven and suits wallets and display.POST /proof/finalized-stateacceptsaccount_auth: [address, ...](sorted, unique) alongside actor-storage claims in a version 2 bundle request. The response proves each row present or absent at a finalized block. Off-chain verifiers that authorize anything from an account signature use this read, verify it against a trusted checkpoint, and fail closed when they cannot.
pending is set they MUST warn the user with the time remaining and offer a one-step cancel; they SHOULD also alert on any AuthPolicyChanged the user did not initiate. Account history beyond the current row is the explorer’s concern.
Wallets display the canonical approval facts for each instruction before signing: account, current and expected revision, the proposed policy (threshold, each key and its derived address), recovery account, delay and, for SetAuthPolicy and SetRecovery, whether a pending recovery will be canceled. The client-signing allowlist and inspect_unsigned_transaction cover all five instructions. A wallet that derives an account’s address from its key MUST track the account address separately once that account has changed its policy.
5.8 Fees
Each instruction is charged as an ordinary transaction under CIP-3, with no surcharge and no deposit:- intrinsic and calldata charges as for any system instruction;
- the CIP-3 §2.5 per-slot verification charge for every slot of every
AccountSignature, including a k-of-n sender’s k slots and the recovery account’s co-signature onSetRecovery; - for
SetAuthPolicyandInitiateRecovery,KEY_VALIDATION_CYCLES[algorithm]and one object-signature verification per proposed key; - the system instruction’s base cycles and Cells, and a flat
storage_write_cellscharge perAccountAuthwrite, independent of the row’s size; - one
AccountAuthread and one write in the instruction’s access accounting.
AccountAuth read per signer account (primary and each co-signer) to the CIP-43 signed-transaction envelope, alongside the existing Actor read and WalletMark write per signer. An absent row is a cold read like any other.
5.9 Interaction with ActorOwner
ActorOwner.owner (CIP-37 §7.1.1) is an account address. Because policy changes and recovery keep the address, they restore control of every actor that account owns: TransferActorOwnership and SetWebAccess verify the new policy through §5.2 with no change to the ActorOwner record, its revision or website access. An actor owned by a k-of-n account needs k signatures for those operations. Recovery is not an ownership transfer and does not reset sharing. An owner recovering from a leaked key should review sharing and re-lock sites the attacker may have published.
5.10 Auth v3 structures
This CIP uses these auth v3 structures:- The primary
signatureand eachadditional_signersentry areAccountSignatures (the reserved sponsor field follows when sponsorship is enabled), with each signer’spolicy_revisionand key-index list bound into both digests. In EIP-712 modeSignerbecomesSigner(address signer,uint64 policy_revision,SignerSlot[] slots)withSignerSlot(uint8 key_index,uint8 algorithm,uint8 mode,bool has_public_key,bytes public_key). - Transaction signature verification returns each signer’s address, policy revision and
(key_index, recovered key)list in compressed form. The declared-address comparison is replaced by the stateful policy check of §5.2, run at RPC ingress and in execution. The structural check bounds the slot count byMAX_TX_SIGNATURE_SLOTSbefore recovery. - The §7.6 table marks account-signed rows (rows 1, 7, 9, 19, 22, 24 and 26 listed in §2.1); each marked row carries an
AccountSignature; native rows signbody ‖ account ‖ u64be(policy_revision)under their existing domain, row 19’s typed structs gainaddress account,uint64 policy_revision(§5.2.1), and each verifier takes a resolvedaccount_policy. - A §7.6 table row for
cowboy/account/key-proof/v1underSECP256K1_OBJECT_POLICY. SigningPublicKey(§7.7) as the stored key type, withKEY_VALIDATION_CYCLEScharged before validation.- One
AccountAuthread per signer account in the envelope, and the new state prefix and opcodes 233–237 in the codec.
co_signers still lists accounts; an account appears in it when its AccountSignature satisfies its policy, so actor quorum logic counts accounts as before.
5.11 Parameters
The protocol requires an explicit
delay_secs. Wallets pre-fill 7 days (604 800 seconds).
6. Rationale
Threshold policies. Storing a threshold over a key set costs almost nothing over a single key, and it gives every ordinary account native multisig without deploying an actor. A single key is the 1-of-1 case. Guardian recovery then needs no mechanism of its own: it is a recovery account with a k-of-n policy. Exactly threshold signatures, indexed. Key indices make the distinctness check a stateless ascending-order check and let verification compare each recovered key to one stored key. Requiring exactlythreshold entries keeps metering predictable and leaves one valid shape per signer set.
Tagged stored keys. Storing tagged SigningPublicKeys reuses CIP-40 §7.7 and lets a future non-recoverable algorithm (Ed25519, P-256) join a policy without a second format.
No row for the common case. An AddressDerived account keeps the address-derived rule, so the default path costs one absent read and no stored keys.
Recovery account instead of a bare recovery key. Making the recovery party an account gives it a nonce, a balance to pay with, wallet support, its own policy and its own recovery, with no new replay machinery. A bare key would need its own nonce space and a fee payer.
Explicit completion. Completing by transaction writes the new policy and emits an event at a known height. Resolving the policy lazily from a pending timestamp would make every signature check time-dependent and leave no record of when control changed.
Cancel until completion. The owner can cancel after the delay has elapsed, up to the moment completion executes. A completion window that locks the owner out would favor the recovery party with no benefit to an owner who still holds the account.
Revoke on policy change. Every account signature names the policy revision it was made under and is valid only at that revision. A policy change therefore revokes everything signed under the old policy, held-back transactions included, even when the new policy keeps some old keys. Checking keys alone would let a retained or restored key keep old authorizations alive.
Key proofs. A policy change is the one operation that can strand an account by mistake. A possession proof per key removes the typo and wrong-key cases, and threshold <= keys.len() with every key proven means the new policy is satisfiable.
Fixed delay bounds. One day leaves an absent owner a real chance to notice; thirty days keeps recovery useful. Governance adjustment buys nothing at this stage.
7. Security Considerations
Recovery-key theft. A thief who can satisfy the recovery account’s policy can initiate. The owner has the full delay to cancel, and wallets warn loudly while recovery is pending. The owner should then replace the recovery account (SetRecovery also cancels the pending attempt). Residual risk, accepted: an owner who does not look at any wallet for longer than the delay loses the account. A k-of-n recovery account raises the bar to k compromised guardians. Recovery keys should not share a backup with the account’s own keys.
Account-key theft. The owner changes the policy; the first change to execute wins. An attacker who satisfies the current policy and changes it first also controls CancelRecovery and SetRecovery, so recovery cannot defeat an active attacker who holds a quorum of current keys. This follows from the current policy’s power to cancel. Recovery is for lost keys; a policy change is for leaked ones. A k-of-n account tolerates up to k−1 leaked keys and up to n−k lost keys without recovery.
Griefing by repeated initiation. Only the configured recovery account can initiate, at most one recovery is pending, and each attempt costs the initiator a transaction. A recovery account that initiates repeatedly is hostile or compromised; the owner removes it with one SetRecovery. An initiation landing first bumps the revision and makes a concurrent SetRecovery fail; one retry converges, because no new initiation is possible while that one is pending, and CancelRecovery needs no revision.
Signature-count abuse. Slot counts are bounded per account and per transaction and checked before any recovery; every slot is metered under CIP-3 §2.5. Duplicate or reordered indices fail structure checks. Extra entries beyond the threshold fail the policy check.
Stale approvals. Every instruction except cancellation is compare-and-set on the auth revision (§5.5).
Bricking. Key proofs and the threshold bound guarantee the new policy is satisfiable when installed. Losing keys afterwards is the case recovery covers.
Old-policy artifacts. Signatures and account-signed objects (§5.2 item 2) from a replaced policy stop verifying, including standing delegations and signatures by keys kept in the new policy (§5.6). On-chain state created by the account’s earlier transactions stays in force (§3). After a leak, wallets SHOULD walk the owner through reviewing grants and approvals the attacker may have made.
Determinism. The delay uses the executing block timestamp, which validators already agree on. Policy resolution reads the current speculative view, so a change earlier in the block is observed by every later check.
Off-chain verifiers. RAS, CBFS relays and gateways that accept wallet-signed objects must resolve the policy from finalized state. A verifier that still compares recovered addresses would keep accepting a replaced key; the conformance suite covers each such verifier.
8. Test Vectors
8.1 Computed vectors
vectors/cip49-account-auth-v1.json is produced by vectors/gen_cip49_vectors.py with eth-account 0.13.4 (eth-abi, eth-keys). The script recovers every signature before writing it and cross-checks every hand-built EIP-712 digest against eth-account’s encode_typed_data; its output is deterministic. Keys are keccak256("cip49-vector-key-" ‖ label); the chain identity is illustrative (chain_id 26909, chain_instance_id = keccak256("cip49-vector-chain-instance")). Signatures are CIP-40 slots: r ‖ s ‖ v, v ∈ {0,1}, low-S.
Every case starts from a reachable state. The account’s address derives from key K0, so at policy revision 0 it is
AddressDerived, and each SetAuthPolicy increments the policy revision:
- retained_key: rev 0
AddressDerived(K0) → rev 1 1-of-[A,B] → rev 2 1-of-[A,C]. - aba: rev 0
AddressDerived(K0) → rev 1 1-of-[A] → rev 2 1-of-[B] → rev 3 1-of-[A]. - multisig: rev 0
AddressDerived(K0) → rev 1 2-of-[A,B,C] (canonical order B, C, A).
hashStruct are in the JSON):
8.2 Additional cases
The cases below are not in the §8.1 file yet. Wire goldens are computed with the protocol codec; behavioral cases run against a node. Where a case below is already computed in §8.1, it reuses those digests.- Encoding goldens.
AuthPolicy1-of-1, 2-of-3 and 16-of-16;AccountAuthwith no recovery, with recovery, and with a pending recovery;AccountSignature; each of the five instructions in commonware wire bytes and JSON; the EIP-712Signer/SignerSlottyped data and digest for a 2-of-3 signer. - Policy validity. Threshold 0, threshold above
keys.len(), 17 keys, unsorted keys, duplicate keys, unsupported algorithm. - Key proofs. Preimage and hash for a fixed account, revision, key and chain identity; a valid proof set; rejections for a missing proof, proofs out of order, wrong account, wrong revision, wrong chain instance and a proof recovering to a different key.
- Policy revision. 1-of-[A,B] changed to 1-of-[A,C]: a transaction and a
TokenPermitsigned by A at the old revision reject, and the same signed at the new revision accept. A→B→A: signatures from the first A period reject. A transaction withheld beforeCompleteRecovery, with a fresh nonce, rejects after it. ASetRecoverydoes not change the policy revision and does not revoke signatures. - Session vouchers. (Computed in §8.1.) The revised
Vouchertype string, typehash and final digest for a fixed domain. A voucher signed bysession.signerwhere the signer differs from the payer accepts, and the payer’s signature on the same session rejects. 1-of-[A,B] changed to 1-of-[A,C]: a voucher signed by retained key A at the old revision rejects at settlement, and A’s voucher at the new revision accepts. A→B→A: a voucher from the first A period rejects. A voucher whoseaccountdiffers from the attributed account rejects. The same voucher digest under the Trading PostverifyingContractdoes not verify as a Session voucher. - Per-call receipts. The extended
Voucherdigest under the Trading PostverifyingContractfor a fixed hire; the hirer’s 2-of-3 receipt with two slots accepts and with one rejects; a receipt signed by retained key A at the old policy revision rejects after 1-of-[A,B] changes to 1-of-[A,C]. - Route session reports. A report with
account = 0andpolicy_revision = 0signed by the bound metering key accepts, and the payer’s policy change has no effect on it. - Satisfaction. For an
AddressDerivedaccount and for 1-of-1 and 2-of-3 policies, in both signature modes: exactly threshold valid entries accept; fewer or more entries reject; an out-of-range index, a repeated or descending index, and a slot recovering to a key other thankeys[key_index]reject; slots signed by the address-derived key of an account that changed policy reject; a transaction overMAX_TX_SIGNATURE_SLOTSrejects before recovery. - Same-block ordering.
SetAuthPolicythen a transaction signed under the old policy in one block: the second is an admission rejection with no nonce, fee or marker effect. - Recovery lifecycle. Configure, initiate, complete at exactly
executable_at_ms(accept) and 1 ms before (reject); cancel before and afterexecutable_at_ms; initiation while pending; initiation from a non-recovery account;SetAuthPolicyandSetRecoveryeach canceling a pending recovery; the recovery account changing its own policy mid-delay and then completing. - Guardians. A 2-of-3 recovery account initiates and completes with two guardian signatures and is rejected with one.
- Bounds and roles. Delays at
MIN - 1,MIN,MAX,MAX + 1; recovery account equal to self or missing fromadditional_signers. - Revision. Every revision-carrying instruction with a stale
expected_revision;CancelRecoveryracing a fresh initiation. - Signed objects. A
DelegationCertand aTokenPermitsigned before a policy change reject after it; the same objects re-signed under the new policy accept; a 2-of-3TokenPermitwith two slots accepts and with one rejects; an object naming account A does not verify for account B that shares a key. - Mempool. A transaction declaring an
AddressDerivedaccount but signed by another key is refused at ingress. - RPC. An account with no row returns
AddressDerivedwith no key; afterSetRecoveryit returnsStoredwith the key recovered from that transaction. - ActorOwner. After recovery, the new policy signs
TransferActorOwnershipandSetWebAccessfor an actor owned by the account; the old key cannot.
9. Status
Implemented ondevnet, including off-chain resolution through the finalized-state proof for CBFS owner authorization and runner storage grants.
Not built yet:
- Mempool re-check and eviction when an
AccountAuthrow changes (§5.6). AccountSignaturefor the sponsor, which waits on CIP-40 sponsorship.- Proof-based policy resolution in every off-chain verifier. Gateway payments and CBFS and runner paid reads use the unproven
GETread; the runner’s off-chain voucher check accepts onlyAddressDerivedsigners and fails closed for any other policy. The CIP-37 gateway wallet login, which resolves through the finalized-state proof, is not ondevnetyet. AccountSignaturefor the address-authorized objects listed in §3.- Reuse of already-recovered keys when
SetRecoverymakes anAddressDerivedrow explicit, and in the admission check for pre-verified transactions; both currently recover the signatures again without a metering charge, bounded byMAX_TX_SIGNATURE_SLOTS. - CIP-49 instruction builders in the wasm and SDK packages.
- The wallet and Dashboard pending-recovery warning, cancel flow, delegation re-issue and key-backup refresh.
10. References
- CIP-40 §7.3, §7.6, §7.7, §12.1, auth v3 (transaction verification, signed objects, tagged keys, wallet identity)
- CIP-37 §7.1.1 (protocol
ActorOwner) - CIP-41 (multi-signer authorization, co-signing flow)
- CIP-3 §2.5 (signature metering)
- CIP-43 (signed-transaction envelope accesses)
- CIP-4 §4.2 (state prefixes)