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())), i.e. keccak256("maroo-testnet") on testnet. 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