Transaction Fee(GAS) Delegated Execution Model in Py-EVM

https://www.pinterest.co.kr/pin/396598310919054878/

Onther에서는 Ethereum의 Transaction Fee(GAS)를 Stamina 컨트랙트에게 위임하는 구조의 실행 모델을 개발했습니다. 본 모델은 GAS를 구매할 때 Transaction Sender의 balance로 구매하는 기존의 모델과 더불어 GAS 구매를 Stamina 컨트랙트의 Stamina로도 구매가 가능한 모델입니다.

본 모델에 대한 자세한 설명과 GETH 구현체는 아래 링크에서 확인 가능합니다.

또한 이러한 모델을 구상하게 된 배경에 대한 설명도 온더의 Danny(한량)님 발표에서 확인할 수 있습니다.

아래에서부터는 본 모델에서 사용하는 Stamina 컨트랙트와 Py-EVM에서 본 모델을 어떻게 구현했는지에 대한 설명으로 이루어지게 됩니다. (계속해서 Transaction Fee(GAS) Delegated Execution Model을 본 모델, 그리고 gasLimit * gasPrice를 GAS라고 칭함)

Stamina Contract

우선 Stamina Contract(developed by 4000d)의 코드는 여기서 확인이 가능합니다.

https://medium.com/@preethikasireddy/how-does-ethereum-work-anyway-22d1df506369

Blockchain은 Genesis Block에서부터 체인이 시작됩니다. Genesis Block에서는 Genesis State를 지정할 수 있습니다. 본 모델에서는 Genesis State에 Stamina Contract, 즉 Contract Account를 Genesis State에 지정하고 Transaction을 Execute할 때마다 Stamina Contract의 함수를 호출하게 됩니다. (본 모델에서는 Stamina Contract Account의 주소를 0x..dead로 지정하고 사용하게 됩니다.)

아래에는 Stamina 컨트랙트의 주요 함수를 설명하고 있습니다.

  1. init
    : Stamina 컨트랙트를 사용하기 위해서는 0x..dead의 Stamina Contract의 init 함수를 호출해야 합니다. init 함수에서는 minDeposit, recoveryEpochLength, withdrawDelay의 변수를 셋팅합니다.
  2. setDelegator
    : delegatee는 setDelegator 함수를 호출해서 delegator를 지정합니다.
  3. getDelegatee
    : getDelegatee 함수를 호출했을 때, Transaction의 sender가 만약 delegatee가 지정한 delegator라면 delegatee의 주소를 리턴하게 됩니다.
  4. deposit
    : deposit 함수를 통해 ETH를 deposit하고, deposit한 ETH만큼 delegatee는 Stamina를 얻게됩니다.
  5. getStamina
    : delegatee의 남은 Stamina 양을 조회하는 함수입니다.
  6. subtractStamina
    : GAS를 Stamina로 구입할 때 호출되는 함수입니다. 이 때 구매한 GAS만큼 delegatee의 Stamina는 차감됩니다.
  7. addStamina
    : Transaction을 실행하고 남은 GAS는 delegatee의 Stamina로 refund됩니다. 단, Stamina로 GAS를 구매했을 때만 해당됩니다. Stamina의 또 다른 특징은 특정 기간이 되었을 때 ETH를 deposit한 만큼 Stamina가 새로 충전된다는 것입니다. 여기서 특정 기간은 init 함수에서 지정한 recoveryEpochLength입니다.

subtractStamina 함수와 addStamina 함수는 onlyChain 모디파이어를 가지고 있습니다. onlyChain 모디파이어는 msg.sender 주소가 NULL_ADDRESS(0x…00)인지 체크합니다. 즉 onlyChain 모디파이어를 가지는 함수는 체인 내부에서만 호출이 가능하도록 설계되어 있습니다(Chain이 Message Sender의 주소를 0x…00로 지정하여 Message를 Execute함, 더 자세한 내용은 아래의 내용에서 확인할 수 있습니다.).

Transaction Fee(GAS) Delegated Execution Model

Py-EVM에서는 본 모델을 구현하기 위해서는 다음과 같은 일을 진행해야 합니다.

  1. Genesis State에 Stamina Contract Account를 지정
  2. Transaction Execution 과정에서 Stamina Contract 함수 호출

Genesis State에 Stamina Contract Account를 지정

Ethereum의 Account는 nonce, balance, codeHash, storageRoot의 자료구조로 이루어져 있습니다. 또한 Ethereum의 Account는 EOA(Eternally Owned Account) 그리고 CA(Contract Account) 두 종류로 나뉩니다. 두 개의 Account 자료 구조는 같지만, EOA는 codeHash와 storageRoot의 값이 empty입니다. 이에 반해 CA는 codeHash와 storageRoot에 값이 존재합니다.

https://medium.com/@preethikasireddy/how-does-ethereum-work-anyway-22d1df506369

따라서 CA, 즉 Stamina Contract Account를 Genesis State에 지정하기 위해서는 nonce, balance, codeHash, storageRoot를 지정해야 합니다. Py-EVM에서 Genesis State에 CA를 지정하기 위해서 nonce, balance, code 값이 필요합니다(Stamina Contract 초기에 상태 변수 값 할당을 하지 않기 때문에 Storage를 추가하지 않음). Stamina Contract의 code bytecode를 넣으면 codeHash를 만들어 code를 저장하고 codeHash를 Account의 state에 반영합니다. 이 때 Stamina Contract Account의 address는 0x…dead입니다.

이후 0x…dead 주소의 Stamina Contract의 init 함수를 호출해야 합니다.

Transaction Execution 과정에서 Stamina Contract 함수 호출

Transaction Execution 과정에서 Stamina Contract의 어떤 함수가 호출되어야 할까요?

우선 기존 모델에서 Transaction의 Execution은 다음과 같은 과정을 거치게 됩니다(아래 과정은 Py-EVM의 Frontier VM 기준입니다.).

  1. intrinsic gas 체크
  2. transaction signature 체크
  3. upfront cost 체크
  4. nonce 체크
  5. buy gas
  6. increment nonce
  7. set up VM message
  8. execute message
  9. refund self-destruct
  10. gas refund
  11. process self-destructs

1, 2, 4, 6, 7, 8, 9, 11 과정은 Stamina Contract와 상관이 없는 로직입니다. 3번 과정은 Transaction Sender와 맵핑된 delegatee가 있을 경우 getDelegatee 함수 호출 — , delegatee의 Stamina를 구하고 — getStamina 함수 호출 — , 구한 Stamina가 GAS보다 많은지 비교를 하게 됩니다.

그리고 5번 과정에서는 Stamina로 GAS를 구매 — subtractStamina 함수 호출 — 합니다. 그리고 마지막으로 10번 과정에서 Message Execution이 끝난 후 남은 GAS를 Stamina로 되돌려줍니다 — addStamina 함수 호출 — .

따라서 정리해보면 Transaction Execute 과정에서 필요한 함수는 getDelegatee, getStamina, subtractStamina, addStamina 함수입니다.

getDelegatee 함수는 Transaction Execution 과정에서 Transaction을 validate하기 전에 호출되어야 합니다. Transaction을 validate하는 과정은 위의 1번부터 4번까지의 과정입니다.

Transaction Fee(GAS) Delegated Execution Model in Py-EVM

여기서부터는 본 모델을 Py-EVM에서 어떻게 구현했는지에 대해 좀더 자세한 내용으로 이루어지게 됩니다.

Py-EVM은 trinitiy 클라이언트가 사용하는 EVM 구현체입니다. trinity는 python으로 구현된 이더리움 구현체이며 아직 알파 버전으로 실제 메인넷에서 동작하도록 개발중에 있습니다. 기존의 python 이더리움 구현체인 pyethereum이 존재하지만 여러 문제를 가지고 있어 Py-EVM으로 다시 개발되고 있습니다. 이에 대한 자세한 설명은 여기서 확인이 가능합니다. 현재 온더에서는 Py-EVM을 주로 개발 커뮤니케이션 용도로 사용하고 있습니다.

trinity는 geth에서와 같이 private chain을 구성하는 기능이 아직 개발되지 않았습니다. 따라서 본 모델을 구현한 Py-EVM을 실험하기 위해서는 따로 스크립트를 작성해서 Py-EVM을 구동해야 합니다.

Py-EVM의 VM 클래스 구조

Py-EVM의 VM 클래스 구조를 추상화하면 다음과 같습니다.

VM 클래스는 BaseVM 클래스를 상속하고 있고, FrontierVM 클래스는 VM 클래스를 상속하고 있습니다. 이런 식으로 최종 ByzantiumVM 클래스는 SpuriousDragonVM 클래스를 상속하고 있는 구조입니다. 즉 하드포크가 발생하여 추가된 기능은 기존 VM을 상속해서 구현하고 있습니다. 이러한 구조 덕분에 Ethereum의 hardfork 역사를 Py-EVM 코드로 쉽게 확인이 가능합니다.

옆에 숫자(#1, #1150000, …, #4370000)는 hardfork 되었을 때의 block number를 의미합니다. Py-EVM은 block number를 기준으로 VM을 가져오는 구조로 되어있습니다. 만약 2600000 block에서 EVM을 실행할 때, 2600000 block은 TangerineWhistleVM 이후이면서 SpuriousDragonVM 이전이기 때문에 TangerineWhistleVM을 가져오고, TangerineWhistleVM 상에서 Transaction을 Execute합니다.

apply_transaction

https://www.google.com/search?q=Ethereum+transaction&tbm=isch#imgrc=t-0-N-FM_UWU0M:

노드가 Transaction을 받게 되면 chain.apply_transaction 함수를 호출하여 Transaction을 Execute합니다. chain.apply_transaction은 vm 객체의 apply_transaction 함수를 호출하고, 그 안에서 또 state 객체의 apply_transaction 함수를 호출합니다.

  • chain.apply_transaction
    : VM 객체를 가져오고, 현재 block 객체를 가져옵니다. 만약 이 때 VM 객체가 ByzantiumVM 객체이면, Block 객체는 ByzantiumBlock 객체입니다. Py-EVM의 모든 객체는 거의 이러한 구조로 이루어져 있습니다. 이후 vm.apply_transaction 함수를 호출시키고, 함수의 결과 값을 이용해 새로운 block을 만듭니다.
  • vm.apply_transaction
    : block gasLimit을 체크하고 state.apply_transaction 함수를 호출합니다. state.apply_transaction 함수 호출의 결과 값을 바탕으로 Receipt를 만들고 new block header를 만들고 리턴하게 됩니다.
  • state.apply_tranasction
    : Transaction을 가지고 실제 EVM을 구동시킵니다. 이 때 위에서 말했던 11가지의 과정이 수행됩니다(FrontierVM 기준).

구현

EVM을 구동하기 위해서는 Chain 객체와 그 안에 VM 객체 등 여러 객체가 필요합니다. scripts/benchmark 디렉토리에는 쉽게 EVM을 구동시킬 수 있도록 여러 스크립트들이 존재합니다. 그 중 get_chain 함수를 이용하면 쉽게 EVM을 구동시킬 수 있습니다. get_chain 함수는 Memory DB를 사용하며, difficulty가 1인 MiningChain을 리턴합니다. get_chain 함수는 parameter로 VM 객체를 가지는데 자신이 원하는 VM 객체 타입을 넣어주면, 해당 VM 객체로 Transaction을 Execute합니다. get_chain 함수는 2개의 EOA를 Genesis State에 지정합니다.

Setting Genesis State

get_chain 함수는 두 개의 EOA를 Genesis State에 지정했습니다. FUNDED_ADDRESS의 Account는 EOA이기 때문에 code를 가지지 않습니다.

AddressSetup(
address=FUNDED_ADDRESS,
balance=DEFAULT_INITIAL_BALANCE,
code=b''
)

위와 같이 Stamina Contract Account를 Genesis State에 지정하기 위해서는 아래와 같이 코드를 작성해야 합니다. 구현 코드

AddressSetup(
address=STAMINA_CONTRACT_ADDRESS,
balance=0,
code=b'`\x80`@R`\x046\x10a\x01\x12W`\x005|\x01\x00\x0...'
)

STAMINA_CONTRACT_ADDRESS는 0x…dead입니다. 이제 get_chain 함수를 호출하면 두 개의 EOA와 함께 Stamina Contract Account도 Genesis State에 포함되어 MiningChain 객체를 리턴하게 됩니다. 이에 대한 코드는 여기서 확인이 가능합니다.

Call Stamina Contract

Transaction을 Execute하는 과정에서 Stamina Contract의 함수가 호출되어야 합니다. 호출되어야 하는 Stamina Contract의 함수는 위에서 설명드린 것처럼 getDelegatee, getStamina, subtractStamina, addStamina 4개의 함수입니다. 따라서 이 함수를 호출하기 위한 코드를 작성해야 합니다. 단 주의할 점은 이 4가지의 함수 호출이 Transaction으로 포함되는 것이 아니라, 단지 Stamina Contract의 상태만을 변경시켜야한다는 것입니다. 즉 apply_transaction 함수를 호출하는 것이 아니라 Stamina Contract의 상태를 변경시킬 Message를 만들고 Message를 Execute해야 합니다(apply_message 함수 호출).

이 4개의 함수 구현체는 아래 링크에 각각 구현되어 있습니다. (해당 코드는 개발 커뮤니케이션을 위한 것이기 때문에 리팩토링이 되어 있지 않습니다.)

subtract_stamina와 add_stamina는 Message의 sender가 0x..00으로 되어 있습니다. 그 이유는 Stamina 컨트랙트의 subtractStamina 함수와 addStamina 함수는 onlyChain modifier에 의해 NULL_ADDRESS(0x…00)만이 호출할 수 있기 때문입니다.

해당 함수는 Transaction 실행 과정에서 호출됩니다.

  1. 가장 먼저 Transaction을 validate하기 전에 Transaction의 Sender가 delegatee와 맵핑되어 있는지 확인합니다.
  2. delegatee가 존재하면, delegatee의 Stamina를 가져옵니다.
  3. 이후 validate_stamina_transaction 함수를 호출하여 delegatee의 Stamina가 gasPrice * gasLimit보다 큰 지 확인합니다. (현재 구현체는 Stamina가 gasPrice * gasLimit보다 작으면 revert합니다. 하지만 본 모델은 Stamina가 gasPrice * gasLimit보다 작으면 Transaction Sender의 balance로 GAS를 구매합니다.)
  4. 마지막으로 Transaction Execution 이후 남은 GAS는 다시 delegatee의 Stamina로 refund됩니다.

(중간 중간에 print 함수는 Stamina가 적절하게 차감되고 충전되는지 확인하기 위한 용도입니다.)

본 모델을 구동하기 위해 작성한 스크립트는 scripts/stamina.py에 구현되어 있습니다. 가장 먼저 ByzantiumVM 객체를 파라미터로 get_chain 함수를 호출하여 MiningChain을 가져옵니다. 이후 여러 함수를 호출하여 Transaction GAS 위임 모델이 잘 동작하는지 확인하게 됩니다.

마치며

본 모델은 기존 모델에 비해 EVM을 최대 4번 더 구동시킵니다(Stamina Contract 함수 호출을 해야 하기 때문에). 따라서 기존 모델보다 실행 속도는 떨어집니다. 하지만 GAS를 제 3자에게 위임할 수 있기 때문에 서비스 제공자(delegatee), 서비스 사용자(delegator)의 구조를 만들 수 있습니다. 즉 이런 구조에서 서비스 사용자는 GAS를 부담하지 않아도 되기 때문에 좀더 유저 친화적입니다. 본 모델은 온더에서 연구하고 있는 Plasma EVM의 실행 모델로 사용될 계획입니다. 긴 글 읽어주셔서 감사합니다.

Onther-Tech

Building an Ethereum Blockchain ECO system to Change the World

신건우(Thomas Shin)

Written by

Onther-Tech

Building an Ethereum Blockchain ECO system to Change the World