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())) — 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
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
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 decodes with the IPrivacy ABI:PrivacyInvalidAuthorizationField(field)—effectiveSenderorexecutoris the zero address, ordeadlineis 0.RequesterIsNotMsgSender(msgSender, requester)—executoris not the caller.PrivacyAuthorizationExpired(deadline)—deadlinehas 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 thaneffectiveSender.PrivacyInvalidAuthorizationMagic(magic)— the ERC-1271 verifier returned something other than0x1626ba7e.
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
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.