Privacy Authorization EIP-712 Domain
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
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
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
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
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
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.