[Devcon 6] Day 2 on Update of EVM

What’s next in EVM

Steven Lee
Tokamak Network
10 min readOct 24, 2022

--

[Devcon 6] Devcon 시리즈
- [Devcon 6] Day 1 on Ethereum Transaction Insight
- [Devcon 6] Day 2 on Update of EVM
- [Devcon 6] Day 3 on Ethereum Assembly
- [Devcon 6] Day 4 on Post-Merge Ethereum Client Architecture

개요

EIP-3540 은 EOF(EVM Object Format) 라는 새로운 컨테이너 형식의 EVM 을 제안한다. 이 제안은 상하이 업그레이드에서 도입될 예정이다. 이 제안이 도입되고 다른 몇 가지 제안들이 도입되면, 이더리움 클라이언트에서 이전보다 더 적은 Computation(혹은, GasUsed 감소) 으로 Contract 를 실행할 수 있다. 이 문서에서는 어떻게 이 제안이 Computation 을 감소시킬 수 있는 지 분석하겠다.

분석

EVM Bytecode 는 원래 따로 구조가 없고 실행할 opcode 의 Byte 로 구성되어 있다.

EIP-3540 (상하이)

EIP-3540 가 적용된 이후, EVM Bytecode 는 다음과 같은 구조를 지니게 된다.

magic, version, (section_kind, section_size)+, 0, <section contents>
  • magic: 0xEF00 (EIP-3541)
  • version: 0x01–0xFF (EOF 버젼번호, 1인 경우 EOF1 이라고 불림)
  • section_kind: 0x01–0xFF (섹션의 종류, 1은 코드, 2는 데이터)
  • section_size: 0x0001–0xFFFF (섹션의 크기)
  • section contents: 섹션의 내용 (코드나 데이터)

몇가지 규칙이 추가된다.

  • 정확히 하나의 코드 섹션이 있어야 한다. (첫번째 섹션)
  • 하나의 데이터 섹션이 코드 섹션 뒤에 올 수 있다.
  • JUMP/JUMPI 는 코드섹션 안에서만 허용된다.
  • 만약, PC가 코드 섹션의 바깥을 가리킬 경우, 실행은 실패한다.
  • PC는 코드 섹션 안에서의 현재 위치를 리턴한다.
  • JUMP/JUMPI 는 코드 섹션 안에서 절대적인 offset 을 사용한다.

고찰

몇 가지 규칙이 더 있지만 이 글에서는 다루지 않겠다. 이 제안만으로서 얻는 이득은 크지 않고, 후행되는 EIP 제안들이 업그레이드에 반영되었을 때 많은 이득을 얻을 수 있다.

EIP-3670 (상하이)

Contract 생성 시점에 code가 EOF1 형식이라면, 다음 두 condition 을 만족하는 지 검사한다.

  • 코드가 현재 assign 되지 않은 명령어를 포함하지 않는다.
  • 마지막 opcode 가 STOP(0x00), RETURN(0xf3), REVERT(0xfd), INVALID(0xfe), SELFDESTRUCT(0xff) 중 하나로 끝난다.

이러한 조건을 만족하지 않는 code 는 생성에 실패한다.

고찰

마지막 opcode 를 종료 명령어로 끝내는 것을 강제하면 평균적으로 약 7% 정도의 성능향상을 얻을 수 있다고 한다. (https://github.com/ethereum/evmone/pull/295)

EIP-4200

Code 가 EOF1 인 경우에만 실행되는 두가지 opcode 가 추가된다.

  • RJUMP (0x5c)
RJUMP offset: PC = PC_post_instruction(다음명령어) + offset
  • RJUMPI (0x5d)
RJUMPI offset: stack 에서 condition 을 POP 하고,
PC = PC_post_instruction(다음 명령어) + ((condition == 0)? 0 : offset)

기존에 있던 JUMP/JUMPI 는 스택의 최상위 항목을 목적지로 사용하기 때문에 동적 분기(dynamic jump)이다. 반면에 이 두가지 opcode 는 offset byte가 정의되어야 하기 때문에 정적 분기(static jump)이다.

고찰

EVM 에서는 runtime 에 매 opcode 마다,

Exist opcode, Stack Overflow, Stack Underflow, Out of gas

위 네가지 조건을 체크하게 된다.

Exist opcode 는 굳이 runtime 에 체크해야할 이유가 없지만, 현재 EVM interpreter 는 체크하고 있다.

RJUMP/RJUMPI 는 JUMP/JUMPI 에 비해 가스비가 저렴하기 때문에 사용자들의 유인을 얻는다. 만약 코드 내에 동적 분기(JUMP/JUMPI)가 존재하지 않는다면, runtime 에 매번 실행 할 때 마다 검사하는 대신, 코드를 생성할 때 한 번의 유효성 검사Stack Over, Stack Underflow 를 검사할 수 있다. 이는 엄청난 계산 상의 이득이다.

이것이 가능한 이유는, opcode 마다 Stack 에 push 하거나 pop 하는 높이가 일정하기 때문이다. 예를 들어 PUSH1 opcode 는 언제나 1 byte 만큼 Stack 에 push 한다.

하지만 이 제안은 JUMP/JUMPI 를 제거하는 제안이 아니다. 따라서 안타깝게도, 이 제안이 통과되어도 코드를 생성할 때 한번만 유효성 검사를 할 수는 없다.

EIP-4750

함수 호출 및 리턴 기능을 EVM 레벨에서 제공하기 위해, EOF 컨테이너는 다음과 같이 변경된다.

magic, version, [type_section_header], (code_section_header)+, [data_section_header], 0, [type_section_contents], (code_section_contents)+, [data_section_contents]
  • type_section 은 code_section 의 각 input, output 개수를 알리기 위해 추가된다.
  • code_section 은 이제 여러개가 될 수 있다.
  • type_section_header, code_section_header, data_section_header: (section_kind, section_size)
  • type_section_contents: code_section 의 input 개수, output 개수 집합. 첫번 째 code_section 의 input과 output은 0, 0 이어야 한다.
    ex) type_section_contents := 0, 0, code_section_1_inputs, code_section_1_outputs, ..., code_section_n_inputs, code_section_n_outputs
  • EVM 은 항상 현재 실행 중인 section 의 index 를 추적한다.

데이터 스택과 별개로 리턴 스택을 도입한다. 리턴 스택은 코드 섹션이 다른 코드 섹션을 호출할 때, 돌아갈 정보가 담긴 스택이다.

code_section_index, offset, stack_height
  • code_section_index: 현재 섹션의 인덱스
  • offset: 현재 섹션의 다음 PC 값 (돌아가야할 곳)
  • stack_height: 현재 데이터 스택의 높이

새로운 opcode 가 추가된다.

  • CALLF(0x5e) code_section_index: 리턴 스택(현재 섹션의 인덱스, 현재 섹션의 다음 PC 값, 현재 데이터 스택의 높이) 를 push 하고,
    현재 섹션의 인덱스를 인자로 받은 code_section_index 로, PC 를 0 으로 설정한다. 실행은 인자로 받은 code_section 에서 계속된다.
  • RETF(0x5f): 리턴 스택을 pop 하여 현재 섹션의 인덱와 PC 를 설정한다.

몇 가지 실행 규칙이 추가된다.

  • JUMP/JUMPI 는 현재 코드 섹션 내에서만 허용된다.
  • CALLF 로 새로운 code_section 으로 이동하였을 때, 호출했을 때의 데이터 스택의 높이보다 낮아질 수 없다. (자신을 호출한 코드 섹션의 데이터를 참조할 수 없다)

고찰

코드 섹션을 여러 개로 허용하고, 각 세션(첫 번째 세션 제외)에 input, output 을 두어 서브루틴을 지원하게 하는 것은 동적 JUMP 의 필요성을 제거하려는 목표를 가지고 있다. 이 제안으로 동적 JUMP/JUMPI 는 완전히 대체될 수 있는데 그 이유는, JUMP 할 목적지를 시작으로 하는 code_section 을 만들면 되기 때문이다. 또한, 섹션의 스택을 분리하여 분석할 수 있게 한다. Stack 의 높이는 각 code_section 에서 0 에서 시작한다고 봐도 무방하다. (호출했을 때의 데이터 스택의 높이보다 낮아질 수 없으므로)

위 이유들로, Contract 를 Create 할 때 각 code_section 별 로 Stack Underflow 를 검사할 수 있다.

EIP-5450

EIP-5450 제목은 Stack Validation 이다. EIP-5450 을 위하여 위의 많은 EIP 들을 제안한 것이다.

  • Contract Create 시, Stack 유효성을 검사하는 규칙을 추가한다.
  • runtime 에 Stack Underflow 검사할 필요가 없지만, 구현은 유지할 수도 있다.

단순하다.

고찰

Stack Underflow 는 검사할 수 있지만 Stack Overflow 는 어떨까? 코드 섹션을 호출하면서 이동할 때, Data Stack 이 초기화 되지는 않는다. 따라서 runtime 에 변경되는 코드의 흐름에 따라 Stack Overflow 가 발생할 수 있다. 그렇다면, Stack Overflow 는 runtime 에 모든 명령어에 대해 검사(런던포크 기준 이더리움)해주어야 할까? 현재 EIP 에서는 변경되지 않을 예정이다. 하지만 좋은 가능성이 있다.

각 code_section 마다 max_stack_height 를 계산할 수 있다. CALLF 가 발생했을 때, 새로운 code_section의 max_stack_height 를 더하여 1024 (최대 스택 크기)를 초과할 경우에만 새로운 code_section 내부에서 명령어 마다 검사하면 된다. 이는 모든 명령어에 대해 수행하던 것을 비약적으로 줄이는 것이고 대부분의 간단한(모든 code_section 의 max_stack_height 를 더해도 1024 가 안 넘는) Smart Contract 들은 거의 검사할 것이 없을 것이다.

결론

결론적으로, 이 글은 execution 에 각 opcode 마다 분석하던 Stack overflow/underflow 검사를 contract creation 때 한 번만 검사함으로써, 많은 Computation 을 줄이는 Next EVM 을 설명하고 있다.

이 글에 쓰여진 EIP 들은 EVM 발전에 대해 일하는 Ethereum Foundation의 Ipsilon 팀의 leader 인 Alex Beregszaszi 에 의해 소개된 EIP 를 기준으로 하였다. EIP-3540 과 EIP-3670 은 상하이 업그레이드에 도입될 예정이고, 그 이후의 EIP 들은 아직 불확실하다. 비슷한 제안을 하는 EIP 들도 많이 있다. (EIP-615, EIP-2315) 따라서, 많은 가능성이 열려 있지만 최종 목적지는 비슷할 것이다.

이 EIP 들이 적용되면 Ethereum 은 더욱 더 친환경적으로 발전할 것이고, 판다의 더욱 많은 사랑을 받을 수 있을 것이다.

참고

--

--