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-1), 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). Off-chain, ask the node: default_node_info.network at https://api-testnet.maroo.io/cosmos/base/tendermint/v1beta1/node_info — on Maroo Testnet it is maroo-testnet-1. A wrong value fails quietly on both paths: an EVM withdraw reverts with a bare SDKInvalidRequest() that does not say which chain-id was expected, and an authorization signature fails signer recovery — so check the string before you sign. 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;

Two structs, one name

The name PrivacyActionAuthorization denotes two different things, and they are not interchangeable. The EIP-712 message type above has 11 members and is what the effective sender signs; the name is part of the typeHash, so it cannot be changed. The Solidity ABI struct of the same name has 6 members — effectiveSender, executor, nonce, deadline, authorizationKind, signature — and is what a caller ABI-encodes. batchItemIndex, requestHash, cosmosChainIdHash and the two selectors appear only in the signed message — the chain re-derives them, so a client cannot read them back off the tuple it submits. batchId is different again: it is neither in the tuple nor derived, but a separate top-level argument of batchTransferWithAuthorization(bytes32 batchId, AuthorizedTransferItem[] items), which the executor supplies and every signer must have committed to.

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 authorization and reverts with typed errors, which decodeErrorResult decodes with the IPrivacy ABI:

  • PrivacyInvalidAuthorizationField(field) — effectiveSender or executor is the zero address, or deadline is 0.
  • RequesterIsNotMsgSender(msgSender, requester) — executor is not the caller.
  • PrivacyAuthorizationExpired(deadline) — deadline has passed.
  • PrivacyAuthorizationNonceUsed(sender, nonce) — the effective sender's nonce was already consumed.


It then checks the signature for the stated authorizationKind:

  • PrivacyAuthorizationRejected(authorizationKind) — the kind is not 1, 2 or 3; the signature does not recover (EOA, EIP-7702); the account's code does not fit the kind (an EOA kind on a contract, ERC-1271 on an account without code, EIP-7702 without a delegation); or the ERC-1271 call failed.
  • PrivacyAuthorizationSignerMismatch(expected, got) — an EOA or EIP-7702 signature recovers to an address other than effectiveSender.
  • PrivacyInvalidAuthorizationMagic(magic) — the ERC-1271 verifier returned something other than 0x1626ba7e.


The precompile's internal diagnostics ("unsupported privacy authorization kind %d", "privacy EOA authorization signer mismatch", …) are not part of the revert data.
// 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 (0 included) reverts with
//   PrivacyAuthorizationRejected(authorizationKind).

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