Privacy Authorization EIP-712 Domain

mechanism privacy

The exact EIP-712 domain, struct type, and digest inputs a relayer-authorized privacy signer must reproduce off-chain.

The *WithAuthorization entry points on IPrivacy accept a signature that authorizes a specific executor to run a specific privacy action on behalf of an effective sender. That signature covers an EIP-712 digest whose domain and struct type are fixed by the precompile — the domain is EIP712Domain(name="Maroo Privacy Precompile", version="1", chainId=<EVM chainId>, verifyingContract=<IPrivacy address>), and the primary type is PrivacyActionAuthorization. Both the EVM chain ID (as a uint256) and the Cosmos chain ID (hashed into the struct) are bound into the digest, so a signature produced against one network cannot be replayed on another. Off-chain signers must reconstruct the same domain and struct fields exactly, or the precompile rejects the call with an EOA/ERC-1271/EIP-7702 signer-mismatch string revert.

Architecture

flowchart LR
  Signer[Effective sender wallet]:::evm
  Relayer[Executor / relayer]:::evm
  Precompile[IPrivacy precompile\n0x...000b]:::precompile
  Signer -->|EIP-712 sign\nPrivacyActionAuthorization| Relayer
  Relayer -->|"transferWithAuthorization(request, auth)"| Precompile
  Precompile -->|recompute digest\nverify signer| Precompile
  classDef evm fill:#0096AA,stroke:#0096AA,color:#fff;
  classDef precompile fill:#FF8C50,stroke:#FF8C50,color:#fff;

The effective sender signs an EIP-712 PrivacyActionAuthorization off-chain; the executor submits it to the precompile, which recomputes the digest and verifies the recovered signer.

The EIP-712 domain

The domain is fixed by the precompile. name and version are hardcoded strings, chainId is the EVM chain ID (450815 on testnet, 815 on mainnet — clients should read it from eth_chainId at runtime), and verifyingContract is the IPrivacy precompile address 0x100000000000000000000000000000000000000b. Using the Cosmos chain ID here — a common mistake — produces a different domainSeparator and the signature fails.
// Domain a client must sign against.
const domain = {
  name: "Maroo Privacy Precompile",
  version: "1",
  chainId: 450815n,                                        // EVM chain ID, from eth_chainId
  verifyingContract: "0x100000000000000000000000000000000000000b",
} as const;

The PrivacyActionAuthorization struct

The primary type has eleven fields. authorizationEnvelopeSelector and authorizationActionSelector are the 4-byte selectors of the outer *WithAuthorization method and the inner action method (for example IPrivacy.transferWithAuthorization / IPrivacy.transfer). cosmosChainIdHash is keccak256(bytes(ctx.ChainID())) — the hash of the Cosmos chain-id string, which is not the EVM numeric chain ID and not a compile-time constant. It is fixed at genesis by the operator (marood testnet init-files --chain-id …; the repo's Makefile provisions maroo-testnet), so a client must read it at runtime rather than hardcode it — a wrong string produces a valid-looking signature that fails signer recovery. Where to read it from: no precompile exposes it to Solidity (IPrivacy declares nine functions, all mutating, and the other precompiles' getParams views do not carry it), so it comes from your deployment configuration, the same place your RPC URL does. If you have it wrong, the two paths fail differently: a withdraw reverts with withdraw chain id mismatch: expected <chain>, got <yours>, which NAMES the value the chain wants, while an authorization signature simply fails signer recovery with nothing to compare against — so check it against a withdraw first. requestHash is the keccak256 of the ABI encoding of the inner request tuple (see below). batchId and batchItemIndex are zero for non-batch calls and populated only for batchTransferWithAuthorization items.
// EIP-712 type layout the client must register with viem / ethers.
const types = {
  PrivacyActionAuthorization: [
    { name: "authorizationEnvelopeSelector", type: "bytes4" },
    { name: "authorizationActionSelector",   type: "bytes4" },
    { name: "effectiveSender",               type: "address" },
    { name: "executor",                      type: "address" },
    { name: "nonce",                         type: "uint256" },
    { name: "deadline",                      type: "uint64" },
    { name: "cosmosChainIdHash",             type: "bytes32" },
    { name: "requestHash",                   type: "bytes32" },
    { name: "batchId",                       type: "bytes32" },
    { name: "batchItemIndex",                type: "uint64" },
    { name: "authorizationKind",             type: "uint8" },
  ],
} as const;

How requestHash is computed

The requestHash field pins the signature to one specific inner payload so a signed authorization cannot be rebound to a different request. For most methods it is keccak256(abi.encode(request)) where request is the single tuple argument of the inner action (e.g. PrivacyTransferRequest, PrivacyWithdrawRequest). Single-proof batch transfers are the one exception — their request hash also prefixes the method selector so it cannot be mistaken for an unrelated encoding: keccak256(methodID || abi.encode(request)) where methodID is the 4-byte selector of singleProofBatchTransfer.
import { encodeAbiParameters, keccak256, toFunctionSelector } from "viem";

// PrivacyTransferRequest — normal case.
const transferRequestType = { /* tuple of PrivacyTransferRequest fields */ } as const;
const requestHash = keccak256(encodeAbiParameters([transferRequestType], [request]));

// PrivacySingleProofBatchTransferRequest — selector-prefixed.
const selector = toFunctionSelector("singleProofBatchTransfer((bytes,bytes,bytes[],(bytes,bytes,bytes,uint32,uint8,bytes,bytes,bytes,bytes,bytes,bytes)[],string,uint64,bytes,uint64))");
const batchRequestHash = keccak256(
  new Uint8Array([...hexToBytes(selector), ...hexToBytes(encodeAbiParameters([batchRequestType], [batchRequest]))]),
);

Validation the precompile applies

Before it checks the signature, the precompile validates the digest inputs and reverts with plain string reasons — these are not typed custom errors, so decodeErrorResult does not decode them. The strings are stable: EVM chain ID must be a uint256, verifying contract is required, authorization selectors must be 4 bytes, authorization effective sender and executor are required, authorization nonce must be a uint256, authorization deadline is required, and unsupported privacy authorization kind %d. If the digest itself is well-formed but the recovered signer does not match, the revert is one of privacy EOA authorization signer mismatch, privacy ERC-1271 authorization rejected: %w, or privacy EIP-7702 authorization signer mismatch depending on authorizationKind.
// authorizationKind values understood by the precompile.
uint8 constant AUTH_KIND_EOA        = 1; // ecrecover against effectiveSender
uint8 constant AUTH_KIND_ERC1271    = 2; // isValidSignature() on effectiveSender
uint8 constant AUTH_KIND_EIP7702_EOA = 3; // ecrecover then require delegated code

// Any other value reverts with:
//   "unsupported privacy authorization kind %d"

Batch fields — batchId and batchItemIndex

For single-request calls (transferWithAuthorization, withdrawWithAuthorization, singleProofBatchTransferWithAuthorization), batchId is the zero hash and batchItemIndex is 0. For batchTransferWithAuthorization, each item in the batch signs a digest carrying the shared batchId and its own batchItemIndex. The protocol caps a multi-proof batch at MaxPrivacyMultiProofBatchItems = 20 items — the precompile rejects a larger input before running any proofs, so clients must enforce the same limit up front.
ESC
Type to search