Technology

Toward Ethereum Equivalence #2 — Precompiled Contract 주소 변경

Klaytn v1.8.0에서는 precompiled contracts의 주소가 변경되었습니다. 이 포스팅에서는 precompiled contracts 주소가 변경된 이유와 이 변경이 precompiled contract를 사용하는 기존에 배포된 contract에 어떠한 영향을 미치는지에 대해 설명합니다.

TLDR

  • 하드포크 적용 이전에 배포된 contract가 precompiled contract를 call로 사용하고 있다면, 하드포크 적용 이전 precompiled contract의 주소 기준으로 동작합니다. 즉 기존과 동일하게 동작합니다.
  • 하드포크 적용 이전에 배포된 contract가 precompiled contract를 delegateCall로 호출해서 사용하고 있다면, 호출자의 context에 따라 다르게 동작할 수 있습니다.

Precompiled contract 주소 변경의 목적

Ethereum에서 새롭게 추가되는 precompiled contract들을 Klaytn에서도 동일하게 지원하기 위해 기존의 Klaytn에서 제공하는 precompiled contract들의 주소가 Ethereum의 precompiled contract들의 주소와 겹치지 않도록 변경되었습니다.

변경되는 Precompiled contracts

위의 표에서 vmLog의 경우 클레이튼에서 제공하는 vmLog의 주소가 이더리움에서 제공하는 Blake2F의 주소와 겹치기 때문에 충돌을 방지하기 위하여 주소가 변경되었습니다. vmLog 이외에도 Klaytn에서 추가로 제공하는 precompiled contracts인 feePayer와 validateSender 의 경우에는 아직 이더리움의 precompiled contracts 주소와 겹치지는 않지만 추후 이더리움에서 precompiled contracts를 추가하면 주소가 충돌이 발생하기 때문에 변경되었습니다.

Precompiled contract 주소 변경의 영향

Klaytn에서는 precompiled contract의 주소 변경으로 인한 영향을 최소화하기 위해 contract를 블록체인에 저장할 때, 당시 반영되어 있던 가장 최신의 하드포크(프로토콜 업그레이드) 버전 정보를 함께 저장하고 있습니다. 이로 인해, 이스탄불 하드포크가 적용되기 전에 contract를 배포한 경우, 이 contract는 이스탄불 하드포크 이전에 배포되었다는 것을 구분할 수 있습니다.

하드포크(프로토콜 업그레이드)란 Klaytn 블록체인에 적용되는 프로토콜이 업그레이드된다는 뜻으로 Klaytn 블록체인을 구성하는 모든 노드들은 해당 업그레이드를 함께 반영해주어야 합니다. Klaytn은 체인별 분기가 발생하지 않기 때문에 블록체인 업계에서 많이 사용하는 하드포크라는 용어가 “프로토콜 업그레이드”라는 의미로 사용 됩니다.

Klaytn은 contract를 실행할 때, contract가 이스탄불 하드포크 이전에 배포되었는지 확인합니다. 만약, 이스탄불 하드포크 이전에 배포되었다면 배포 시점과 동일한 주소록인 Constantinople precompiled contract map를 사용합니다. 이를 통해 이미 배포된 컨트랙트가 하드포크 이후에도 동일하게 동작할 수 있습니다.

그러나, delegateCall을 사용하여 Precopmiled contract를 호출하는 contract는 문제가 발생할 수 있습니다. 이 명령어는 호출대상이 아닌 호출자의 시점에서 컨트랙트를 실행시키는 특징을 가지고 있습니다. 만약 Precompiled contract를 호출하는 Account가 EOA(Externally Owned Account)일 경우 하드포크 버전 정보를 저장하지 않으므로 항상 최신 버전의 precompiled contract 주소록을 사용하게 됩니다. 이로 인해 EOA 계정으로 delegateCall을 사용해서 precompiled contract를 호출하고 있을 경우 문제가 발생할 수 있습니다.

Call을 사용해서 precompiled contract를 호출하는 경우

하드포크 이전(과거)에 배포한 contract에서 precompiled contract를 호출할 때 아래와 같이 call을 사용했다면 이번 주소 변경으로 인해 문제될 부분은 전혀 없습니다. 하드포크 이후 precompiled contract의 주소가 변경되어도 기존에 동작하던 대로 잘 동작합니다. (아래 코드는 feepayer precompiled contract를 호출하는 예제 contract코드입니다.)

contract Example {
   event CallFeePayer(address indexed _from, address indexed _ret);
   function feePayer() public returns (address addr) {
       assembly {
           let freemem := mload(0x40)
           let start_addr := add(freemem, 12)
           if iszero(call(gas(), 0xa, 0, 0, 0, start_addr, 20)) {
               invalid()
           }
           addr := mload(freemem)
       }
       emit CallFeePayer(msg.sender, addr);
   }
}

이는 하드포크가 적용되면서 precompiled contract의 주소가 변경될지라도, 호출하는 contract가 “어떤 하드포크 때 배포되었는지”를 구분할 수 있으므로 그에 맞는 주소록을 사용해서 precompiled contract로 적절히 매핑해주면 됩니다.

아래의 그림은 이스탄불 하드포크 전에 precompiled contract인 feePayer(0x0a)를 사용하는 contract를 배포한 상황을 나타내는 그림입니다.

하드포크(주소 변경)가 적용된 이후 feePayer를 호출하는 Contract A의 함수를 호출 했다고 가정해보겠습니다. contract A는 여전히 변경 전 주소인 0x0a와 함께 feePayer를 Call하고 있는 상황입니다. Precompiled contract feePayer를 호출하는 msg.sender는 contract A이고, contract A는 하드포크 적용 전에 배포되었다는 것을 알 수 있기 때문에 그에 맞는 precompiled contract 주소록을 사용해서 feePayer를 호출하게 됩니다.

위와 같이 call을 사용해서 precompiled contract를 호출하는 코드라면 이번 주소 변경으로 인해 영향받는 부분은 없습니다.

DelegateCall을 사용해서 precompiled contract를 호출하는 경우

하드포크 적용 이전에 배포한 contract는 대부분의 경우 하드포크 적용 후에도 동일하게 동작합니다. 그러나, contract 가 delegateCall을 이용하여 precompiled contract 0x09, 0x0a 또는 0x0b를 호출하도록 작성되어 있으면 문제가 발생할 수 있습니다 (Klaytn Docs의 예제와 동일하게 작성했다면 문제 없음).

주소 변경이 되기 전에, 즉 하드포크가 적용되기 전에 사용하던 주소인 “0x0a”로 feePayer를 delegateCall로 호출하는 Example contract를 배포했다고 가정해보겠습니다. (아래 코드는 feepayer precompiled contract를 delegatecall로 사용하는 예제 contract 코드입니다.)

contract Example {
   event CallFeePayer(address indexed _from, address indexed _ret);
   function feePayer() public returns (address addr) {
       assembly {
           let freemem := mload(0x40)
           let start_addr := add(freemem, 12)
           if iszero(delegatecall(gas(), 0x0a, 0, 0, start_addr, 20)) {
               invalid()
           }
           addr := mload(freemem)
       }
       emit CallFeePayer(msg.sender, addr);
   }
}

하드포크가 적용된 이후 precompiled contracts의 주소록이 최신 주소록으로 업데이트가 된 뒤, 이미 배포했었던 Example contract의 feePayer 함수를 EOA로 호출을 하면 어떻게 될까요?

Example Contract의 feePayer 함수가 실행되면서 내부적으로 delegateCall로 precompiled contract를 호출을 하게 되는데, 이 때 호출자 Address(msg.sender)는 Example contract의 feePayer 함수를 호출한 EOA가 됩니다. EOA의 경우 하드포크와 관련된 별도의 정보를 기록하지 않기 때문에 아래 그림처럼 언제나 최신 버전의 precompiled contract 주소록이 사용됩니다.

Contract A 내부에서 call을 통해 0x0a 주소를 실행할 때, 0x0a 주소가 feePayer인지 아닌지 판단하는 context는 contract의 배포 시점입니다. 따라서, contract가 하드포크 적용 이전에 배포되었다면 해당 contract를 호출하는 트랜잭션이 하드포크 전에 실행되던 후에 실행되던 모두 동일하게 feePayer를 호출하게 됩니다.

반면에 delegateCall(대리호출)은 호출 대행자(contract A)의 context가 아닌 호출자(EOA)의 context를 사용합니다. 호출자(EOA)의 context는 트랜잭션이 실행되는 그 시점에 기반하고, 0x0a 주소가 feePayer인지는 트랜잭션이 실행되는 블록번호가 하드포크 적용 전인지 후인지에 따라 다르게 결정됩니다. 이렇게 되면 본래 Example contract를 배포했을 때의 의도(feePayer를 호출하고자 했음)와는 다르게 동작하게 됩니다. 왜냐하면 주소 0x0a는 호출하는 시점(하드포크 적용 이후)에 더이상 precompiled contract인 feePayer의 주소가 아니기 때문입니다.

결론

하드포크 이전에 배포하셨던 contract에서 precompiled contract를 호출할 때 call을 사용하고 있다면 영향받는 부분은 없습니다.

하지만 delegateCall을 이용하여 precompiled contract 0x09, 0x0a 또는 0x0b를 호출하고 있었다면 호출자의 Context에 따라 문제가 될 수 있습니다. 이 부분에 유의해주시고 필요시 contract를 재배포해주시기 바랍니다. 이러한 상황에서 다음과 2개의 방안 중 하나를 사용할 수 있습니다.

  • Precompiled contract를 호출하는 contract를 새로 배포하고자 하는 경우, delegateCall 대신 call을 사용하도록 변경하여 배포하시길 바랍니다. 만약, delegateCall을 사용하는 contract를 수정없이 재배포 하는 경우에는 이스탄불 하드포크 이후에 배포하시길 바랍니다. 그러지 않으면 동일한 문제가 발생할 수 있으니 주의하시길 바랍니다.
  • 만약 delegateCall을 호출하는 contract가 EOA의 context에 노출된 경우라면, 새롭게 wrapper contraft를 배포하는 방법이 있습니다. 이 wrapper contract를 거쳐 기존의 contract를 호출하게된다면 EOA의 context에 노출되지 않습니다. 단, 이 wrapper contract는 이스탄불 하드포크 (메인넷에 v1.8.0 이후 적용) 이후에 배포하여야합니다.

관련 미디엄 포스팅 목록