EIP-5 RETURN과 CALL* 가스 사용 개선 (Gas Usage for `RETURN` and `CALL*`)
최종 확정 · EVM·실행 · 원문 상태 Final · 2015-11-22 작성
컨트랙트가 다른 컨트랙트를 호출할 때 출력용으로 미리 잡아 둔 메모리 전체가 아니라 호출이 끝난 시점에 실제로 쓰인 메모리에만 가스를 물리도록 바꿉니다. 이로써 길이를 미리 알 수 없는 문자열과 배열을 돌려받을 수 있게 됩니다.
왜 필요한가
지금의 이더리움 가상 머신(EVM)에서는 컨트랙트가 다른 컨트랙트의 함수를 호출할 때 결과를 받을 공간의 크기를 미리 정해야 합니다. 그런데 결과의 크기는 호출해 보기 전에는 알기 어려운 경우가 많습니다. 상대 컨트랙트의 저장소에 있는 데이터는 밖에서 직접 볼 수 없어서 크기를 알아내려면 또 한 번 호출해야 하기 때문입니다. 공간을 넉넉하게 크게 잡는 방법도 있지만 지금은 실제로 쓰이지 않는 메모리에도 가스를 내야 해서 비용이 커집니다. 그 결과 문자열이나 길이가 변하는 배열을 돌려받는 일이 비싸고 불편해서 사실상 쓸 수 없는 상태입니다. 이 EIP는 실제로 쓰인 메모리에만 가스를 물려 두 문제를 한 번에 풉니다.
무엇이 바뀌나
지금
- 호출 전에 출력 크기를 미리 정해야 합니다
- 공간을 크게 잡으면 쓰이지 않는 메모리에도 가스를 냅니다
- 문자열 같은 가변 길이 데이터를 돌려받기가 사실상 불가능합니다
EIP-5 이후
- 출력 공간을 아주 크게 예약해도 실제로 쓰인 만큼만 가스를 냅니다
- 호출 뒤 MSIZE 명령어로 반환된 데이터의 크기를 알 수 있습니다
- 문자열과 가변 길이 배열을 유연하게 돌려받을 수 있습니다
누구에게 영향이 있나
- 스마트컨트랙트 개발자: 문자열과 가변 길이 배열을 반환하는 함수를 호출할 수 있고 인터페이스를 모르는 컨트랙트의 입출력을 그대로 전달하는 프록시 컨트랙트를 만들 수 있습니다.
- 클라이언트(VM 구현) 팀: CALL 주변 두 시점에서 메모리를 키우고 가스를 청구하도록 구현을 바꿔야 합니다.
- 기존 배포 컨트랙트: 가스는 이전보다 더 들지 않지만 MSIZE에 의존해 메모리를 할당하던 코드는 객체의 메모리 주소가 달라질 수 있습니다.
심층 설명
적용 범위: CALL, CALLCODE, DELEGATECALL
가스·메모리 규칙 변경은 CALL, CALLCODE, DELEGATECALL(원문은 이 셋을 CALL*로 묶어 부릅니다)에 적용됩니다. CREATE는 메모리에 쓰지 않으므로 영향을 받지 않습니다. CALL*의 인자는 gas, address, value, input_start, input_size, output_start, output_size입니다.
가스를 두 시점에 나눠 청구
명령어 시작 시점에는 input_start + input_size까지 메모리를 키우는 가스만 청구하고 output_start + output_size에 대해서는 청구하지 않습니다. 피호출 컨트랙트가 크기 n의 데이터를 반환하면 호출자의 메모리는 output_start + min(output_size, n)까지 커지고 그만큼의 가스가 그때 청구됩니다. 출력은 [output_start, output_start + min(n, output_size)) 구간에 쓰입니다. 그래서 호출자는 명령어의 시작과 끝 두 시점 모두에서 가스 부족(out of gas)이 날 수 있습니다.
호출 후 MSIZE와 프록시 패턴
호출이 끝난 뒤 MSIZE 명령어는 메모리가 실제로 커진 크기를 반환해야 합니다. 호출자가 output_start = MSIZE, output_size = 2**256-1로 호출하면 실제로 쓰인 영역은 (output_start, 호출 후의 MSIZE)가 되어 반환 데이터의 크기를 알 수 있습니다. 이 성질 덕분에 출력 크기를 미리 알 필요가 없고 호출 뒤에 크기를 알아낼 수 있어, 인터페이스를 모르는 컨트랙트의 입력과 출력을 그대로 전달하는 프록시 컨트랙트가 가능해집니다.
설계 대안과 선택 이유
명령어 자체나 인자 개수를 바꾸는 방안, output_size가 2**256-1일 때만 가스 규칙을 바꾸는 방안도 검토됐습니다. 그러나 CALL 주변 두 지점에서 메모리를 키워야 한다는 것이 구현의 주된 어려움이므로, 조건부 방식이 구현을 더 단순하게 만들지 않습니다. 초기에는 반환 데이터의 크기를 스택에 올리자는 제안도 있었지만 위의 MSIZE 방식으로 충분하고 하위 호환성이 훨씬 낫다고 판단해 EVM을 최소한만 바꾸는 현재 방식이 선택됐습니다.
하위 호환성: 가스와 메모리 크기
컨트랙트는 가스 잔량과 메모리 크기를 읽을 수 있으므로 이 변경은 기존 컨트랙트의 의미를 바꿀 수 있습니다. 다만 가스 면에서는 VM이 이전보다 더 많은 가스를 물리는 일이 없고 컨트랙트는 호출자에게 가스를 더 돌려줄 수만 있으므로 문제가 될 가능성이 낮습니다. MSIZE 면에서는 두 가지 영향이 있습니다. 첫째, CALL로 확보하려던 메모리 조각이 이전보다 작아져 이후 MSIZE 기반 할당과 겹칠 수 있는데, 그런 컨트랙트가 존재할 가능성은 낮습니다. 둘째, MSIZE로 할당한 객체들의 메모리 주소가 달라지는데, 객체는 절대 위치와 상대 위치에 의존하지 않게(재배치 가능하게) 작성돼야 하므로 일반적으로 문제가 없습니다.
구현 주의사항과 상태
VM 구현자는 호출이 끝나는 시점까지 기다렸다가, 가스가 충분히 남아 있는지 확인한 뒤에야 메모리를 키워야 합니다. 전형적인 사용법은 출력용으로 2**256-1 바이트를 예약하는 것입니다. 원문에는 변경 전후의 파이썬 참조 구현 링크가 실려 있습니다. 이 EIP는 Final 상태로 확정된 표준입니다.