ERC-3668 CCIP Read 체인 밖 데이터 조회 (CCIP Read—Secure offchain data retrieval)

Final · 기타 · 2020-07-19 작성

컨트랙트가 필요한 데이터가 체인 밖에 있다고 에러로 알리면, 클라이언트가 지정된 서버에서 데이터를 받아 컨트랙트에 돌려주고 컨트랙트가 검증하는 공통 절차를 정합니다.

왜 필요한가

이더리움에 데이터를 저장하고 거래를 보내는 비용이 비싸서 컨트랙트 개발자들은 해시, 머클 트리(Merkle Tree), L2 같은 방법으로 데이터를 체인 밖으로 옮겨 왔습니다. 방식은 달라도 체인 위에는 밖에 둔 데이터를 검증할 만큼의 정보가 남는다는 점은 같습니다. 지금까지 앱마다 제각각 방식을 만들어 온 것이 문제입니다. 저장 방식이 하나뿐이면 그럭저럭 쓸 수 있지만 사용자마다 다른 저장소나 L2를 고르는 환경에서는 클라이언트가 방식 하나하나를 따로 지원해야 해서 금방 감당하기 어려워집니다. 컨트랙트가 체인 밖 데이터를 가져오는 공통 절차가 있으면 클라이언트는 어떤 저장소를 쓰는지 몰라도 되고 컨트랙트 내부 사정을 모르는 새로운 앱도 만들 수 있습니다. 원문은 에어드랍 명단을 머클 트라이에 둔 컨트랙트, L2에 있는 토큰 정보를 L1 토큰처럼 보기, ENS 도메인을 여러 L2에 위임하기 같은 예를 듭니다.

구현해야 하는 것

심층 설명

OffchainLookup 에러의 다섯 필드

체인 밖 데이터가 필요한 함수는 반드시 OffchainLookup(address sender, string[] urls, bytes callData, bytes4 callbackFunction, bytes extraData) 에러로 되돌려야 합니다. sender는 에러를 던진 컨트랙트 주소이며 클라이언트는 이 값으로 자신이 호출한 컨트랙트가 던진 것인지 중첩 호출에서 올라온 것인지 가립니다. urls는 게이트웨이 URL 템플릿 목록이고 빈 배열 []이면 클라이언트가 직접 URL 템플릿을 정해야 합니다. 시도 순서는 클라이언트 몫이지만 컨트랙트는 중요한 순서대로 넣는 것이 좋습니다. callData는 게이트웨이에 보낼 데이터로 클라이언트에게는 의미가 없는 바이트열입니다. 보통 ABI 인코딩을 쓰지만 형식은 컨트랙트와 게이트웨이가 정합니다. callbackFunction은 결과를 받을 원래 컨트랙트 함수의 4바이트 셀렉터입니다. extraData는 콜백에 필요한 부가 데이터로, 클라이언트는 이 값을 보관했다가 고치지 않고 그대로 콜백에 넘겨야 합니다.

콜백 함수의 요건

컨트랙트는 게이트웨이 응답을 해석하고 검증하는 콜백 함수도 구현해야 합니다. 이름은 자유지만 매개변수는 반드시 (bytes response, bytes extraData)여야 하고 반환 형식은 OffchainLookup을 던진 원래 함수와 같아야 합니다. 클라이언트가 게이트웨이 호출에 성공하면 response에 게이트웨이가 돌려준 값을, extraData에 에러에 담겼던 값을 넣어 콜백을 부릅니다. 콜백 안에서 다시 OffchainLookup을 던져 조회를 이어갈 수 있지만 반복 횟수 제한은 클라이언트마다 다르다는 점을 염두에 둬야 합니다. 읽기 호출에서는 콜백의 반환값이 처음 부른 함수의 반환값처럼 사용자에게 돌아갑니다. 원문 예시가 addr를 callData와 extraData 양쪽에 넣는 이유는 게이트웨이가 응답에 돌려주는 값만 믿으면 다른 질문에 대한 답을 끼워 넣을 수 있기 때문입니다.

URL 템플릿과 GET·POST 규칙

URL 템플릿에는 {sender}와 {data} 두 치환 변수를 쓸 수 있습니다. {sender}는 소문자 0x 접두 16진수 주소로, {data}는 0x 접두 16진수 callData로 바꿉니다. 템플릿에 {data}가 있으면 클라이언트는 GET 요청을 보냅니다. 없으면 Content-Type을 application/json으로 한 POST 요청에 data와 sender 두 필드를 담은 JSON 본문을 보냅니다. 게이트웨이는 application/json으로 data 필드에 0x 접두 16진수 결과를 담아 응답해야 합니다. 실패하면 알맞은 HTTP 상태 코드를 써야 합니다. 예를 들어 지원하지 않는 sender면 404, callData 형식이 잘못되면 400, 서버 내부 오류면 500입니다. 4xx·5xx 응답이 application/json이면 message 필드에 사람이 읽을 오류 문구를 담아야 합니다. 클라이언트는 GET과 POST를 모두 지원해야 하고 게이트웨이는 필요한 쪽만 구현해도 됩니다. 이 명세는 HTTPS URL 처리만 정의합니다.

클라이언트 조회 절차

클라이언트는 먼저 컨트랙트를 평소처럼 호출하고 성공하면 결과를 그대로 돌려줍니다. OffchainLookup이 아닌 에러는 평소처럼 전달합니다. OffchainLookup이면 다섯 필드를 풀고 sender가 호출한 컨트랙트 주소와 다르면 오류로 끝냅니다. URL을 만들어 요청하되 목록 앞쪽을 우선합니다. 응답이 400~499면 오류로 끝내고 500~599면 다음 URL을 시도하며 더 없으면 멈춥니다. 성공하면 callbackFunction 셀렉터에 응답 데이터와 extraData를 ABI 인코딩한 호출 데이터로 바꿔 처음 단계부터 다시 수행합니다. 4xx·5xx 응답의 Content-Type이 application/json이 아니면 본문을 JSON으로 해석하려 해서는 안 됩니다. 한 호출에서 조회가 여러 번 일어날 수 있으므로 클라이언트는 횟수 제한을 두어야 하며 그 한도는 4회 이상이어야 합니다.

중첩 호출에서 OffchainLookup 다루기

CCIP Read를 아는 컨트랙트가 다른 컨트랙트를 부를 때 상대도 CCIP Read를 구현했을 가능성이 있으면, 상대가 던진 OffchainLookup을 모두 잡아야 하고 sender가 상대 주소와 다르면 다른 에러로 되돌려야 합니다. 모든 OffchainLookup을 다른 에러로 바꿔 버려도 되는데, 그러면 중첩 지원 구현은 피하지만 중첩 조회 자체가 불가능해집니다. 솔리디티가 기본으로 하는 중첩 호출 에러 전파를 그대로 두면 안 됩니다. A가 CCIP를 모르는 B를 부르고 B가 다시 A를 불러 A가 OffchainLookup을 던지면, B와 A가 차례로 이를 전파해 sender가 A와 일치하는, 겉보기엔 정상인 에러가 클라이언트에 도달하지만 실제로는 안쪽 호출만 완료되는 문제가 생깁니다. 원문의 NestedLookup 예시는 상대의 에러를 잡아 sender를 확인한 뒤 자신의 aCallback 셀렉터와 상대 주소, 상대 콜백, 상대 extraData를 묶어 다시 던지는 방법을 보여 줍니다.

트랜잭션에도 쓸 수 있습니다

명세는 eth_call 같은 읽기 호출을 기준으로 쓰였지만 체인 밖 데이터가 필요한 트랜잭션에도 그대로 적용할 수 있습니다. eth_estimateGas나 eth_call로 트랜잭션을 미리 실행해 보다가 OffchainLookup을 받으면 같은 조회 절차를 거친 뒤, 마지막 단계에서 호출 대신 트랜잭션을 보내면 됩니다. 체인 밖 증명 데이터로 뒷받침되는 온체인 청구(claim) 같은 용도에 알맞다고 원문은 설명합니다. 다만 보안 고려 사항에서는 클라이언트 라이브러리가 트랜잭션에 대한 CCIP Read를 기본으로 끄고 호출자가 명시적으로 켜게 하라고 권고합니다.

설계 근거: revert, sender, extraData, GET과 POST

에러로 되돌리는 방식을 택한 이유는 기존 함수를 이름이나 ABI를 바꾸지 않고 체인 밖 데이터 지원으로 바꿀 수 있기 때문입니다. ABI에 호출 종류를 표시하면 같은 이름에 다른 반환값을 주어 해석 오류를 내거나 offchainBalanceOf처럼 함수를 전부 복제해야 합니다. sender 주소는 범용 게이트웨이를 만들 수 있도록 게이트웨이에 넘깁니다. 게이트웨이가 원래 컨트랙트를 조회해 필요한 정보를 얻을 수 있어 같은 인터페이스를 쓰는 여러 컨트랙트를 게이트웨이 하나로 받을 수 있습니다. extraData는 컨트랙트가 호출 사이에 상태를 유지하지 않으므로 두 호출 사이에 맥락을 넘기고 게이트웨이가 답한 질문이 원래 질문인지 확인하는 데 씁니다. GET은 정적 파일만 두는 HTTP 서버나 IPFS로도 게이트웨이를 만들 수 있을 만큼 단순하지만 URL 길이가 2킬로바이트로 제한되므로, 복잡한 용도를 위해 POST를 선택지로 두었습니다.

호환성과 상태

이 명세를 쓰지 않는 기존 컨트랙트는 영향을 받지 않습니다. 클라이언트는 추가 부담이나 비호환 없이 모든 컨트랙트 호출에 CCIP Read 지원을 넣을 수 있습니다. 반대로 CCIP Read가 필요한 컨트랙트는 이를 구현하지 않은 클라이언트와는 동작하지 않으며 그런 클라이언트가 호출하면 컨트랙트가 던진 예외가 그대로 사용자에게 전달됩니다. 이 제안의 상태는 Final로 내용이 확정되었습니다.

쓸 때 주의할 점

ERC 원문 · ← ERCs