ERC-6538 스텔스 메타주소 레지스트리 (Stealth Meta-Address Registry)
Final · 계정·지갑 · 2023-01-24 작성
스텔스 메타주소를 온체인에 등록하고 누구나 조회할 수 있게 하는 체인당 하나뿐인 공용 레지스트리입니다. 이를 통해 받는 사람의 실제 주소를 드러내지 않고 자산을 보낼 수 있습니다.
왜 필요한가
ERC-5564가 정의한 스텔스 주소로 자산을 받으려면 보내는 쪽이 먼저 받는 쪽의 스텔스 메타주소를 알아야 합니다. 그런데 이 메타주소를 어디서 찾을지 정해진 장소가 없으면 당사자끼리 따로 주고받거나 오프체인 서비스에 의존해야 합니다. 오프체인에만 있는 정보는 스마트 컨트랙트가 읽을 수 없어서 ENS나 그노시스 세이프(Gnosis Safe) 같은 서비스가 스텔스 주소 기능을 자동으로 붙이기 어렵습니다. 프로젝트마다 자기만의 저장소를 만들면 같은 사용자의 메타주소가 여러 곳에 흩어져 표준화도 어렵습니다. 이 표준은 체인마다 하나만 배포되는 공용 레지스트리를 정해서 누구나 같은 곳에서 메타주소를 등록하고 조회하게 만듭니다. 레지스트리에 등록하는 것 자체가 스텔스 거래에 참여할 준비가 되었다는 신호 역할도 합니다.
구현해야 하는 것
function registerKeys(uint256 schemeId, bytes calldata stealthMetaAddress) external;: 호출자 본인의 스텔스 메타주소를 주어진 schemeId에 등록합니다.function incrementNonce() external;: 호출자의 논스를 1 올려 이미 만들어 둔 대리 등록 서명을 모두 무효화합니다.function DOMAIN_SEPARATOR() external view returns (bytes32);: 이 컨트랙트가 쓰는 EIP-712 도메인 구분자를 돌려줍니다.function stealthMetaAddressOf(address registrant, uint256 schemeId) external view returns (bytes memory);: 주어진 등록자와 schemeId에 저장된 스텔스 메타주소를 돌려줍니다.function ERC6538REGISTRY_ENTRY_TYPE_HASH() external view returns (bytes32);: registerKeysOnBehalf에 쓰이는 EIP-712 타입 해시를 돌려줍니다.function nonceOf(address registrant) external view returns (uint256);: 주어진 등록자의 현재 논스를 돌려줍니다.event NonceIncremented(address indexed registrant, uint256 newNonce);: 등록자가 논스를 올렸을 때 새 논스 값과 함께 발생합니다.error ERC6538Registry__InvalidSignature();: registerKeysOnBehalf에 유효하지 않은 서명이 들어왔을 때 발생하는 오류입니다.
심층 설명
체인마다 하나뿐인 싱글톤 레지스트리
ERC6538Registry는 체인당 인스턴스가 하나만 있는 싱글톤 컨트랙트여야 합니다(MUST). 배포 주소는 0x6538E6bf4B0eBd30A8Ea093027Ac2422ce5d6538이며 결정적 배포자(deterministic deployer) 0x4e59b44847b379578588920ca78fbf26c0b4956c를 통해 CREATE2로 배포합니다. 이때 솔트는 0x7cac4e512b1768c627c9e711c7a013f1ad0766ef5125c59fb7161dade58da078을 씁니다. 등록 주체는 주소, ENS 이름, 그 밖의 식별자로 식별될 수 있습니다.
저장 구조와 schemeId
메타주소는 stealthMetaAddressOf[registrant][schemeId] 형태의 이중 매핑에 bytes로 저장합니다. schemeId는 스텔스 주소 방식을 구분하는 1바이트 정수이고 이 표준은 schemeId 1을 ERC-5564에 명시된 뷰 태그(view tag) 방식의 SECP256k1 곡선 스킴으로 정합니다. ERC-5564의 메타주소 표기는 st:<shortName>:0x<spendingPubKey>:<viewingPubKey>인데, 어느 체인인지는 레지스트리가 배포된 체인으로 이미 정해지므로 저장 값은 압축된 spendingPubKey와 viewingPubKey를 이어 붙인 바이트열입니다.
registerKeys로 직접 등록
registerKeys(schemeId, stealthMetaAddress)는 호출자(msg.sender)의 해당 schemeId 항목에 메타주소를 저장하고 StealthMetaAddressSet(registrant, schemeId, stealthMetaAddress) 이벤트를 냅니다. 호출자 본인 항목만 바꾸므로 별도의 서명 검증이 없고 다시 호출하면 값을 덮어써 메타주소를 갱신할 수 있습니다.
registerKeysOnBehalf와 서명 검증
제3자가 등록자를 대신해 등록하는 함수입니다. 서명 대상은 EIP-712 타입 해시 Erc6538RegistryEntry(uint256 schemeId,bytes stealthMetaAddress,uint256 nonce)로 만든 구조화 데이터이며 현재 nonceOf[registrant] 값을 포함하고 호출할 때 논스를 1 올립니다. 서명 길이가 65바이트면 ecrecover로 서명자 주소를 복원해 registrant와 비교합니다. 복원 주소가 0이거나 registrant와 다르면 registrant 컨트랙트의 isValidSignature를 호출해 EIP-1271 서명으로 다시 검증하고 반환값이 셀렉터와 다르면 ERC6538Registry__InvalidSignature 오류로 되돌립니다. 이렇게 EOA 서명과 컨트랙트 지갑 서명을 모두 지원합니다.
논스와 incrementNonce
nonceOf는 등록자별로 다음에 쓸 논스를 저장합니다. 유효한 registerKeysOnBehalf 호출마다 1씩 올라가므로 같은 서명을 두 번 쓸 수 없습니다. 등록자는 incrementNonce를 직접 호출해 논스를 올릴 수 있고 그러면 그 전에 만들어 둔 서명이 전부 무효가 됩니다. 호출 시 NonceIncremented(registrant, newNonce) 이벤트가 발생합니다.
도메인 구분자와 체인 포크 대응
EIP-712 도메인 구분자는 이름 "ERC6538Registry", 버전 "1.0", 체인 ID, 컨트랙트 주소로 계산합니다. 배포 시점의 체인 ID(INITIAL_CHAIN_ID)와 도메인 구분자(INITIAL_DOMAIN_SEPARATOR)를 저장해 두고 DOMAIN_SEPARATOR()는 현재 block.chainid가 배포 시점과 같으면 저장된 값을 돌려주고 다르면 다시 계산합니다. 원문 주석은 체인 포크가 생기면 도메인 구분자를 다시 계산한다고 밝힙니다.
중앙 레지스트리를 두는 이유
원문은 네 가지 근거를 듭니다. 첫째, 다른 스마트 컨트랙트가 등록된 메타주소를 쉽게 조회해 쓸 수 있어 상호운용성이 보장되고 ENS나 그노시스 세이프 같은 애플리케이션이 스텔스 주소를 서비스에 통합할 수 있습니다. 둘째, 사용자의 메타주소를 가져올 때 오프체인 소스에 의존하지 않아도 됩니다. 셋째, 이 컨트랙트에 등록하는 것 자체가 스텔스 거래에 참여할 준비가 되었다는 표준적인 의사 표시가 됩니다. 넷째, 싱글톤으로 배포되므로 여러 프로젝트가 같은 메타주소 집합을 공유해 표준화에 기여합니다.
쓸 때 주의할 점
- 키가 유출되면 즉시 등록을 해제해야 합니다: 개인키가 유출된 경우 등록자는 스텔스 키 레지스트리에서 즉시 등록을 해제해야 합니다. 그래야 유출된 계정으로 앞으로 들어올 자산을 잃는 일을 막을 수 있습니다.