Privacy Authorization EIP-712 도메인
릴레이어가 대리 실행하는 프라이버시 요청을 서명할 때 오프체인에서 재현해야 하는 EIP-712 도메인과 struct 타입, 다이제스트 입력을 정의합니다.
IPrivacy의 *WithAuthorization 진입점은 특정 executor가 특정 effective sender를 대신하여 프라이버시 작업을 실행하도록 허가하는 서명을 받습니다. 이 서명은 프리컴파일이 고정한 EIP-712 다이제스트를 대상으로 하며, 도메인은 EIP712Domain(name="Maroo Privacy Precompile", version="1", chainId=<EVM chainId>, verifyingContract=<IPrivacy 주소>)이고 primary type은 PrivacyActionAuthorization입니다. EVM chain ID(uint256)와 Cosmos chain ID(해시로 struct에 포함)가 모두 다이제스트에 묶이므로, 한 네트워크에서 만든 서명을 다른 네트워크에서 재사용할 수 없습니다. 오프체인 서명자는 동일한 도메인과 struct 필드를 정확히 재구성해야 하며, 그렇지 않으면 프리컴파일이 EOA/ERC-1271/EIP-7702 signer mismatch 문자열 revert로 호출을 거절합니다.
아키텍처
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;
Effective sender가 오프체인에서 EIP-712 PrivacyActionAuthorization에 서명하면, executor가 이를 프리컴파일에 제출하고 프리컴파일이 다이제스트를 재계산하여 복원된 서명자를 검증합니다.
EIP-712 도메인
name과 version은 하드코딩된 문자열이고, chainId는 EVM chain ID(테스트넷은 450815, 메인넷은 815 — 클라이언트는 런타임에 eth_chainId로 읽어야 합니다)이며, verifyingContract는 IPrivacy 프리컴파일 주소 0x100000000000000000000000000000000000000b입니다. 여기에 Cosmos chain ID를 넣는 실수를 자주 하지만, 그러면 domainSeparator가 달라져 서명이 실패합니다.// 클라이언트가 서명 대상으로 삼아야 하는 도메인입니다.
const domain = {
name: "Maroo Privacy Precompile",
version: "1",
chainId: 450815n, // EVM chain ID, eth_chainId로 조회
verifyingContract: "0x100000000000000000000000000000000000000b",
} as const; PrivacyActionAuthorization struct
authorizationEnvelopeSelector와 authorizationActionSelector는 각각 바깥쪽 *WithAuthorization 메서드와 안쪽 실제 액션 메서드(예: IPrivacy.transferWithAuthorization / IPrivacy.transfer)의 4바이트 selector입니다. cosmosChainIdHash는 keccak256(bytes(ctx.ChainID()))입니다. Cosmos 체인ID 문자열의 해시이며, EVM 숫자 체인ID가 아니고 컴파일 타임 상수도 아닙니다. 제네시스 시점에 운영자가 정합니다(marood testnet init-files --chain-id …, 이 저장소 Makefile은 maroo-testnet-1로 띄웁니다). 따라서 클라이언트는 하드코딩하지 말고 런타임에 읽어야 합니다. 문자열이 틀리면 서명은 그럴듯하게 만들어지지만 서명자 복원에서 실패합니다. 어디서 읽어야 하는가: 이 값을 Solidity로 노출하는 프리컴파일은 없습니다(IPrivacy의 함수 9개는 모두 상태 변경 함수이고, 다른 프리컴파일의 getParams도 이 값을 담지 않습니다). 오프체인에서는 노드에 묻습니다. https://api-testnet.maroo.io/cosmos/base/tendermint/v1beta1/node_info의 default_node_info.network이며, 마루 테스트넷에서는 maroo-testnet-1입니다. 값이 틀려도 두 경로 모두 원인을 알려 주지 않고 실패합니다. EVM의 withdraw는 어떤 chain-id를 기대했는지 알려 주지 않는 SDKInvalidRequest()로 revert되고, 인증 서명은 서명자 복원에서 실패합니다. 그러니 서명하기 전에 문자열을 확인합니다. requestHash는 안쪽 request 튜플의 ABI 인코딩에 대한 keccak256입니다(아래 참고). batchId와 batchItemIndex는 배치가 아닌 호출에서는 0이며, batchTransferWithAuthorization의 각 item에서만 값이 채워집니다.// 클라이언트가 viem / ethers에 등록해야 하는 EIP-712 타입 정의입니다.
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; 같은 이름, 다른 두 구조체
PrivacyActionAuthorization이라는 이름은 서로 다른 두 가지를 가리키며, 둘은 서로 바꿔 쓸 수 없습니다. 위의 EIP-712 메시지 타입은 멤버가 11개이고 실효 발신자가 서명하는 대상입니다. 이 이름은 typeHash의 일부이므로 변경할 수 없습니다. 같은 이름의 Solidity ABI 구조체는 멤버가 6개(effectiveSender, executor, nonce, deadline, authorizationKind, signature)이며 호출자가 ABI 인코딩하는 대상입니다. batchItemIndex, requestHash, cosmosChainIdHash와 두 selector는 서명된 메시지에만 나타납니다. 체인이 이 값들을 다시 유도하므로 클라이언트는 제출하는 튜플에서 되읽을 수 없습니다. batchId는 또 다릅니다. 튜플에도 없고 유도되지도 않으며, batchTransferWithAuthorization(bytes32 batchId, AuthorizedTransferItem[] items)의 별도 최상위 인자로 executor가 직접 넘기고 모든 서명자가 그 값에 커밋해야 합니다.requestHash 계산 방법
requestHash 필드는 서명을 특정 내부 페이로드에 고정하여, 서명된 authorization을 다른 요청에 재바인딩할 수 없게 합니다. 대부분의 메서드에서는 keccak256(abi.encode(request))이며, request는 내부 액션의 유일한 튜플 인자(예: PrivacyTransferRequest, PrivacyWithdrawRequest)입니다. 예외는 single-proof 배치 전송입니다. 이 경우 서로 다른 인코딩과 혼동되지 않도록 메서드 selector를 앞에 붙여 keccak256(methodID || abi.encode(request))로 계산합니다. methodID는 singleProofBatchTransfer의 4바이트 selector입니다.import { encodeAbiParameters, keccak256, toFunctionSelector } from "viem";
// PrivacyTransferRequest — 일반적인 경우입니다.
const transferRequestType = { /* PrivacyTransferRequest 필드 튜플 */ } as const;
const requestHash = keccak256(encodeAbiParameters([transferRequestType], [request]));
// PrivacySingleProofBatchTransferRequest — selector 접두어가 붙습니다.
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]))]),
); 프리컴파일이 수행하는 검증
decodeErrorResult에 IPrivacy ABI를 넘기면 디코드됩니다.PrivacyInvalidAuthorizationField(field)—effectiveSender나executor가 0 주소이거나deadline이 0입니다.RequesterIsNotMsgSender(msgSender, requester)—executor가 호출자가 아닙니다.PrivacyAuthorizationExpired(deadline)—deadline이 지났습니다.PrivacyAuthorizationNonceUsed(sender, nonce)— effective sender의 nonce가 이미 소모되었습니다.
이어서 지정한
authorizationKind로 서명을 확인합니다.PrivacyAuthorizationRejected(authorizationKind)— kind가 1, 2, 3이 아니거나, 서명에서 서명자를 복원할 수 없거나(EOA, EIP-7702), 계정의 코드가 kind와 맞지 않거나(컨트랙트에 EOA kind, 코드 없는 계정에 ERC-1271, delegation 없는 EIP-7702), ERC-1271 호출이 실패했습니다.PrivacyAuthorizationSignerMismatch(expected, got)— EOA나 EIP-7702 서명에서 복원한 주소가effectiveSender와 다릅니다.PrivacyInvalidAuthorizationMagic(magic)— ERC-1271 검증자가0x1626ba7e가 아닌 값을 반환했습니다.
프리컴파일 내부 진단 문자열("unsupported privacy authorization kind %d", "privacy EOA authorization signer mismatch" 등)은 revert 데이터에 포함되지 않습니다.
// 프리컴파일이 인식하는 authorizationKind 값입니다.
uint8 constant AUTH_KIND_EOA = 1; // effectiveSender에 대한 ecrecover
uint8 constant AUTH_KIND_ERC1271 = 2; // effectiveSender에 isValidSignature() 호출
uint8 constant AUTH_KIND_EIP7702_EOA = 3; // ecrecover 후 delegated code 요구
// 그 외 값(0 포함)은 다음 오류로 revert됩니다:
// PrivacyAuthorizationRejected(authorizationKind) 배치 필드 — batchId와 batchItemIndex
transferWithAuthorization, withdrawWithAuthorization, singleProofBatchTransferWithAuthorization)에서는 batchId가 zero hash이고 batchItemIndex는 0입니다. batchTransferWithAuthorization에서는 배치의 각 item이 공통 batchId와 자기 자신의 batchItemIndex를 담은 다이제스트에 서명합니다. 프로토콜은 multi-proof 배치를 MaxPrivacyMultiProofBatchItems = 20 item으로 제한합니다. 프리컴파일이 어떤 증명도 실행하기 전에 그 이상을 거절하므로, 클라이언트도 미리 동일한 상한을 강제해야 합니다.