겨울철 대비 (이더리움) 가스 절약 방법

in #kr-dev6 years ago

안녕하세요. 개발자 모도리입니다.
이번에는 거꾸로 타는 보일러를 소개해 드리려는 것은 아니고요. 이더리움 가스 소모를 조금이라도 줄여보고자 발버둥 친 흔적을 공유하려고 합니다.


[출처 : 동아일보]


이더리움에 데이터를 저장할 때 발생하는 가스 비용을 줄이는 방법을 고민하던 중에 이더리움 황서(Yellow Paper)에 나와 있는 가스 환불(Refund)에 대한 부분을 다뤄보려고 합니다.

이더리움 가스 테이블

이더리움 황서 Appendix G. Fee Schedule에 보면 이더리움에 적용되는 가스 비용을 정의해 놓은 표가 있습니다.

많이 봤던 숫자도 보이네요. 21000은 이더 전송 시 기본으로 발생하는 가스입니다.


그 중에서 우리가 관심있게 볼 내용은 데이터 저장 및 초기화 관련 부분입니다.

  • SSTORE를 통해서 스토리지의 값을 zero에서 non-zero로 바꿈 : 20000
  • SSTORE를 통해서 스토리지 값의 zeroness(0으로 설정되어 있던 부분)이 변함이 없거나 0으로 설정 됨 : 5000
  • 스토리지의 값을 non-zero에서 zero로 바꿈 : -15000 (refund counter에 추가)
  • 컨트랙트를 self-destruct할 경우 : -24000 (refund counter에 추가)

환불 받기

selfdestruct 실행해 보기

pragma solidity ^0.4.25;

contract Terminator {

    constructor() public payable {}

    function iwillbeback() public {
        selfdestruct(msg.sender);
    }
}
  • 배포 후 iwllbeback 함수 실행하여 selfdestruct 명령 실행
    • gas (가스 사용 예상 & 가스 리밋) : 26395
    • transaction cost(실제 사용 된 가스) : 13198

selfdestruct하면 24000 환불해 주는 것 아닌가?? 그런데 환불이란 것이 내 계좌로 gas * gas price 만큼의 이더를 채워주는 것이 아니라, 단지 실행 시 발생하는 gas에서 해당 만큼은 차감해 주는 것입니다. 그리고 해당 양 만큼을 온전히 차감해 주는 것도 아닙니다.

(65) 수식을 보면 환불 해줄 가스(g*)는 트랜잭션 생성 시 입력 했던 gas limit (T_g)에서 사용하고 남은 가스(g')에 추가적으로 환불 해 줄 가스양을 더하는 것입니다. 추가적으로 환불 해 줄 가스양은 전부 다 주는 것도 아니고, 사용한 가스양의 절반을 넘을 수 없습니다. :(

가스 소모 비교

Zero to Non-zero / Non-zero to Non-zero / Non-zero to Zero

pragma solidity ^0.4.25;
contract SimpleStorage {
    uint private number;
    
    constructor() public {}
    
    function () public payable {}
    
    function getNumber() public view returns (uint) {
        return number;
    }
    
    function setNumber(uint _number) public {
        number = _number;
    }
    
}

Remix 에서 Ganache(Ropsten에서 실행할 경우네는 gas가조금 다르게 책정되지만 사용 된 transaction cost는 동일하여서, 빠른 실행을 위해서 Ganache 사용)와 연결 한 후 위의 코드를 배포하고, 테스트를 해보았습니다.

  • 초기 상태 : 0
  • 1 입력 (zero -> non-zero)
    • gas (가스 사용 예상 & 가스 리밋) : 41603
    • transaction cost(실제 사용 된 가스) : 41603

  • 2 입력 (non-zero -> non-zero)
    • gas : 26603
    • transaction cost : 26603

  • 3 입력 (non-zero -> non-zero)
    • gas : 26603
    • transaction cost : 26603

  • 0 입력 (non-zero -> zero)
    • gas : 28270
    • transaction cost : 13270

  • 0 입력 (zero -> zero)
    • gas : 26539
    • transaction cost : 26539

가스 소모 정리

  • zero -> non-zero : 41603
  • non-zero -> non-zero : 26603
  • non-zero -> zero : 13270
  • zero -> zero : 26539
  • (zero -> non-zero)-(non-zero -> non-zero) = 15000
  • (non-zero -> non-zero)-(non-zero -> zero) = 13333

이런 비교를 해 본 이유는 가스비를 위임하는 형태의 토큰 입출금 시스템을 만들 때 사용자의 출금 요청에 의해서 잔고를 0으로 만들고 모두 출금해 주는 것 보다 1을 남겨 놓고 출금해 주는 것이 가스 소모가 덜 하지 않을까 하는 의문에서 였습니다.

토큰 입출금 시나리오

실제로 가상의 토큰 입출금 시나리오를 진행해 보았습니다.

pragma solidity ^0.4.25;

contract SimpleMultipleStorage {
    uint[] private numbers;

    constructor() public {}

    function () public payable {}

    function getNumbers() public view returns (uint[]) {
        return numbers;
    }

    function setNumbers(uint[] _numbers) public {
        numbers = _numbers;
    }

}

ERC20 토큰 코드에서는 mapping 자료형을 사용하여 주소를 key로 해서 해당 얼만큼의 토큰을 가지고 있는지를 value로 가지고 있습니다.
위의 코드에서는 해당 코드를 단순화하여 uint 배열에 잔고를 변경하는 시나리오로 진행하였습니다.

  • 초기 상태
    • 배포 후 setNumbers에 [0,0,0] 값을 넣어서 3개의 계정의 잔고 초기화

Zero <-> Non-zero 반복 시나리오

  • Zero -> Non-zero
    • setNumber에 [1000,1000,1000] 입력
    • transaction cost : 88445

  • Non-zero -> Zero
    • setNumber에 [0,0,0] 입력
    • transaction cost : 21531

  • Zero -> Non-zero
    • setNumber에 [1000,1000,1000] 입력
    • transaction cost : 88445

  • Non-zero -> Zero
    • setNumber에 [0,0,0] 입력
    • transaction cost : 21531

  • 평균 가스 소모
    • 4회 트랜잭션 : (8445 + 21531) * 2 / 4 = 54988
    • 10회 트랜잭션 : (8445 + 21531) * 5 / 10= 54988
    • 100회 트랜잭션 : (8445 + 21531) * 50/ 100= 54988
    • Zero -> Non-zero, Non-zero -> Zero의 평균으로 항상 동일

Non-zero <-> Non-Zero 반복 시나리오

  • Zero -> Non-zero (처음 생성되었을 때는 0에서 시작)
    • setNumber에 [1000,1000,1000] 입력
    • transaction cost : 88445

  • Non-zero -> Non-zero
    • setNumber에 [1,1,1] 입력
    • transaction cost : 43253

  • Non-zero -> Non-zero
    • setNumber에 [1000,1000,1000] 입력
    • transaction cost : 43445

  • Non-zero -> Non-zero
    • setNumber에 [1,1,1] 입력
    • transaction cost : 43253

  • 평균 가스 소모
    • 4회 트랜잭션 : (88445 + 43253 + 43445 + 43253) / 4 = 54599
    • 10회 트랜잭션 : (88445 + (43253 + 43445 + 43253) * 3) / 10 = 47829.8
    • 100회 트랜잭션 : (88445 + (43253 + 43445 + 43253) * 33) / 100 = 43768.28
    • 횟수가 늘어날 수록 Non-zero -> Non-zero 가스 소모량으로 수렴(감소)

결론

모든 잔고를 출금하고 다시 입금하고 하는 트랜잭션이 매우 자주 일어나고, 해당 트랜잭션의 가스비를 대신해서 부담해야 될 경우에는 출금 시에 매우 소량의 토큰을 남겨두어 값을 0이 아닌 상태로 유지하는 것으로 가스비 부담을 줄일 수도 있습니다.
사용자의 잔고에 제한을 둔다는 점에서 문제제기가 있을 수 있으나, 대부분의 토큰의 경우 decimal을 18로 설정하여 1(ether를 토큰이라고 가정한다면 1 wei)만 남겨 두어도 0이 아닌 상태를 유지할 수 있습니다. 아니면 사용자의 잔고가 아닌 운영 측의 토큰을 사용하는 방법도 있을 것 같습니다.
물론 해당 실험은 매우 제한적인 상황에서 진행 된 것으로 실제와 다소 차이가 있을 수 있습니다. 하지만 이런 식으로도 접근할 수 있다는 의견을 제시하고, 같은 데이터를 저장 하면서도 왜 가스 비용이 다른지에 대한 궁금증이 해고자 하는 목적이 큽니다.
긴 글 읽어 주셔서 감사합니다.^^


  • 저는 블록체인 개발사 (주)34일에서 블록체인 엔지니어로 일하고 있습니다.
  • 880만 팔로워 전세계 1위 한류 미디어 케이스타라이브(KStarLive)와 함께 만든 한류 플랫폼에서 사용되는 케이스타코인(KStarCoin) 프로젝트를 진행 중입니다. 팬 커뮤니티 활동을 하면서 코인을 얻을 수 있으며, 한류 콘텐츠 구매, 공연 예매, 한국 관광 상품 구매, 기부 및 팬클럽 활동 등에 사용 될 계획입니다.