All about Ethereum Gas
요즘 주변 개발자들 사이에서 흔히 듣리는 소리는, "컨트렉트를 짰는데 제대로 동작하지 않아서 확인해 보니 Gas문제였다." 인 것 같다.
얼마전 facebook에는 EstimateGas만큼 Gas를 설정해서 트렌젝션을 보냈는데, negative gas가 발생하는 트렌젝션이여서 최종 사용량이 적게 계산된것이더라. 그래서 실제 실행시에는 gas limit을 넘어가는 순간이 발생하더라라는 흥미로운 이야기도 들었다.
사실 Gas의 사용량은, 계약의 코드가 소모하는 량 + OnChain state의 변화량이 함께 존재하는 개념이기 때문에, 정확하게 예측하기가 어렵다. 심지어 Web3에서 제공하는 함수이름도 EstimateGas 이지 않은가?
그래서 오늘은 이더리움의 Gas와 EVM 상의 처리 로직을 정리해보고
EsimateGas함수의 동작원리를 파악해보고자 이 글을 시작하였다.
Gas가 뭐야?
이더리움의 내재가치는 당연히 Ether인데 왜 Gas라는 단어를 만들어 우리를 헷갈리게 할까? 이부분을 먼저 명확하게 이해하고 관리 로직을 들여다 보도록 하자.
Ethereum에서 Gas의 역할
Ethreum Yellow paper기준으로 Gas는 이더리움 프로토콜상에서 다음의 역할을 부여받았다.
- 이더리움의 가격 변동성에 대한 버퍼역할
- 마이너에 대한 보상의 역할(Reward 말고 연산에 대한 Fee)
- Dos등의 공격을 막는 역할
위의 역할중 1번이 이 글에서 가장 중요한 내용이다.(2번과 3번은 다루지 않는다)
먼저 위 개발자들이 말하는 가스비는 Gas Price와 Gas 사용량(Unit이라는 표현을 빌린다)의 조합이다. Gas Price는 Gas 1 unit당 이더리움 가격이다. Gas Unit은 Ethereum의 EVM operation에서 비용을 책정하는 고유의 단위이다. 그래서 opcode당 실제 비용은 Gast cost * unit/opcode가 된다. 이러한 비용구조는 Action을 생성하는 user가 unit당 Gas price를 조절함으로서 이더리움의 외부가격변동(원, 달러등)으로부터 자유로워 질수있는 기회를 준다.
만약 가스비로 천원을 내고 싶으면, 내 Action이 소모하게될 Gas unit의 수로 천원을 나누고, 유닛당 원화가격이 나오면, 해당 원화가격에 대응되는 Gas Price를 이더리움으로 입력하면 된다. 즉, 이더리움이 비싸지면 Gas price를 적게 적고, 이더리움이 싸지면 p높게 적어 내어서 최종 소비 금액을 천원에 맞출 수 있다
실제 트렌젝션을 생성하는 과정에서 우리는 Gas에 관련한 2개의 field인Gas Cost와 Gas Limit을 입력하게 되는데, Cost는 이더리움과 연결되고, Limit은 opcode가 소모하는 (최대)Unit수로 정의된다.
그래서 최종 지불하는 가스비는 Cost * Unit Limit으로 계산되는 것이다.
결국 Gas는 EVM상에서만 존재한다.
이더리움에서는 연산을 하는것도, State를 변경하는 것도 모두 EVM이다.
즉 가스를 소모하고 역할은 모두 EVM에서 한다.
나중에 나오겠지만, Refund할때는 이더리움으로 돌려줘야 하므로 남은 Gas 유닛수 * Gas Cost를 해서 변환된 가치를 돌려준다.
Ethereum의 Gas 정책
연산당 소모량
EVM은 op code를 실행하는 머신이며, 아래와 같이 op code를 실행할때 마다 Gas Unit을 소모하도록 설계가 되어있다.
(From: https://github.com/djrtwo/evm-opcode-gas-costs/blob/master/opcode-gas-costs_EIP-150_revision-1e18248_2017-04-12.csv)
State 변화에 대한 비용과 Negative Gas
돈먹는 하마같으니라고, 메모리나 OnChain에 기록될 Storage를 읽고 쓰는데도 비용이 든다. M은 Memory, S는 Storage이다. 다들 그래서 최종 저장할때 말고는 Memory를 사용한다.
Negative Gas가 재미있는 개념인데, 이더리움은 사용하지 않는 스토리지나 어카운트의 삭제를 장려하기 위해, Gas를 돌려주는 정책을 만들었다. 글의 시작부에 언급했던 Negative Gas 문제를 이해하는데 도움을 줄 것이다.
- Deleting a contract (SELFDESTRUCT) is worth a refund of 24,000 gas.
- Changing a storage address from a nonzero value to zero (SSTORE[x] = 0) is worth a refund of 15,000 gas.
과연 어떤 문제가 발생하게 될까!
Gas가 소비 되는 과정.
Transaction Gas Limit vs. Block Gas Limit
이더리움 상에는 2개의 Gas Limit이 존재한다.
실제 Gas의 소비를 이해하기 위해 두 Gas Limit에 대해 간단히 짚고 넘어가야한다.
윗 챕터에서 트렌젝션을 생성하기 위해 입력하는 2개의 필드중 하나가 Gas Limit이라고 했는데, 이것의 정확한 명칭은 Transaction Gas Limit으로, 이 트렌젝션을 실행하는 과정에서 소모할 최대 unit수를 가리킨다. 이 유닛수를 넘어가면 실행할 opcode가 남아있더라도 EVM의 실행이 멈추게 될 것이다.
두번째 Block Gas Limit은 해당 블록내에서 트렌젝션들이 사용가능한 최대 Gas unit수이다. 즉, 앞선 트렌젝션에서 Block Gas Limit만큼의 unit을 사용했다면, 다른 트렌젝션은 실행 될 수 없다.(실제로 그런일은 발생하지 않는다. 트렌젝션이 블록에 포함되는 과정에서 이미 걸러지기 때문에)
EVM의 Gas 사용방식 - Gas Pool
Gas Pool은 블록의 state를 Processing할때 사용되는 개념이다.
어떤 블록을 처리할때(정확히는 블록내의 transaction을 처리) 사용가능한 가스를 관리하기 위한 pool로서, 블록 처리 시작전 생성되며 BlockGasLimit값으로 초기화된다 (중요!)
아래 함수 호출 구조를 보면, Gas Pool을 생성하고 블록 상의 트렌젝션을 실행하기 위해 매 트렌젝션마다 Gas Pool에서 Transaction Limit만큼 가스를 사고, 사둔 가스에서 opcode의 cost를 소모하는 것을 확인할 수있다. 이후 남은 가스나 환불 될 가스의 unit수는 Pool에 반환된다.
이러한 로직을 통해 트렌젝션이 실제 사용한 Gas unit량만을 고려하여 블록에 트렌젝션을 최대한 가득 채울 수 있게 된다.
Estimate Gas의 동작방식
EstimateGas함수는 트렌젝션의 사용량을 예측하기 위해 사용하는 함수이다. 이 함수는 최소값을 21000유닛으로, 또 최대값을 현재 가장 최신의 블록(마이닝을 하고있을경우 마이닝되었지만 아직 canonical확정이 되지 않은 블록이 존재할수 있으므로, 최신 마이닝된 블록)의 Block Gas Limit으로 설정한 후 바이너리 서치를 통해 Contract호출시 transaction이 성공하는 경우의 Gas를 반환한다.
실제로 Contract호출함수는 이렇게 생겼다
오류가 나지 않도록 임시 balance도 최대로 주고, 가스풀도 최대값으로 준다.
이런 호출 방식이라면, Gas Pool을 설명했던 함수 호출 부분에서 이부분을 공통으로 사용하고 있음을 알수 있다.
그렇다면 처리로직은 아래와 같이 요약 할 수 있다.
- 바이너리 서치를 통해 입력한 가스 값만큼 가스풀에서 가스를 산다.
- 트렌젝션을 실행해본다.
- 실행결과를 확인한다.
- halt가 나면 스테이트를 원상복원하고, 다른 입력을 넣는다.
- 성공했다면 입력한 값을 리턴한다 (!)
이상하다. 실제 사용한 가스값을 리턴하는 거라면, negative gas를 고려하여 실제 peak상황보다 작은 값이 반환되겠지만, 코드 분석에 따르면 이미 산 가스를 가지고 실행이 되는지 안되는지를 검증해보고 해당 가스를 반환하기 때문에 문제가 되면 안된다!
다시 문제로 돌아가서.
그럼 언제 문제가 발생한 것일까?
예상되는 부분으로는 예측에 사용된 state가 실행 당시와 다를 경우다.
예측에 사용된 최신 블록의 다음 블록에 내 트렌잭션이 첫번째로 포함된다는 보장이 없기 때문에, 문제가 발생할수 있다.
또한 다음블록에 포함되더라도, 앞쪽의 트렌젝션이 내가 사용하는 state를 변경했을 경우에도, 문제가 발생가능하다.
마지막 Mining한 블록이 uncle블록으로 처리되었을 경우, 바로 다음 블록에 첫번째로 포함된다 하더라도 문제가 발생할수 있을것으로 예상한다.
마치며,
실제로 테스트를 해본것은 아니기 때문에, 100% 확신할 순 없지만
최소한 실제 사용된 gas를 리턴하는 것이 아니라, 입력한 GasLimit으로 트렌젝션이 동작하는지를 확인한 값을 리턴하는 로직으로 구현이 되어 있다는 것을 확인하였다.
다만, Estimate Gas로 예측한 가격은 마이닝 상태와, 블록에 포함되는 시간에 따라 언제든 무효해 질수 있다는 것을 확인하였기 때문에, 최초 언급된 제보는 분석이 틀렸을 확률이 높다는것만 확인하고, 글을 마무리 하기로 했다.
잘못 분석된 내용이 있거나, 수정할 부분이 있다면
alphabet@hotmail.co.kr로 언제든 연락부탁드립니다.
Congratulations @sigmoid! You have completed the following achievement on the Steem blockchain and have been rewarded with new badge(s) :
You can view your badges on your Steem Board and compare to others on the Steem Ranking
If you no longer want to receive notifications, reply to this comment with the word
STOP
Vote for @Steemitboard as a witness to get one more award and increased upvotes!