오류가 나는 트랜잭션에 대한 가스비 소모

안녕하세요, caverjs 를 통해서 스마트 컨트랙트에 트랜잭션을 보내는 과정에서 오류가 났을 때, 가스비 리밋까지 과도하게 가스비가 발견하는 현상 때문에 글을 남깁니다. 아래의 모든 진행은 baobab 네트워크에서 진행하였으며, 모든 트랜잭션을 보낸 계정의 주소는 “0x8Dcb5c195ef30ed66DE9CedeaA5cEc621b6b99FE” 입니다. 이 계정을 실험계정 이라고 칭하겠습니다.

먼저 평범한 KIP-17 기준에 따른 NFT를 발행합니다. 해당 NFT의 소스코드는 다음과 같습니다. 해당 NFT의 컨트랙트 주소는 “0xcB1026eb511ACaA5F75BFCF167Df01E7593b7620” 입니다. 이를 NFT Contract라고 칭하겠습니다.

pragma solidity ^0.5.6;

import "./token/KIP17/KIP17Full.sol";
import "./token/KIP17/KIP17Mintable.sol";
import "./token/KIP17/KIP17Pausable.sol";
import "./ownership/Ownable.sol";

contract Test is
    Ownable,
    KIP17Full("ABCD", "aaaa"),
    KIP17Mintable,
    KIP17Pausable
{
    string public baseURI = "https://test.com/";
    string public baseURIBack = ".json";

    function uint2str(uint256 _i)
        internal
        pure
        returns (string memory _uintAsString)
    {
        if (_i == 0) {
            return "0";
        }
        uint256 j = _i;
        uint256 len;
        while (j != 0) {
            len++;
            j /= 10;
        }
        bytes memory bstr = new bytes(len);
        uint256 k = len;
        while (_i != 0) {
            k = k - 1;
            uint8 temp = (48 + uint8(_i - (_i / 10) * 10));
            bytes1 b1 = bytes1(temp);
            bstr[k] = b1;
            _i /= 10;
        }
        return string(bstr);
    }

    function tokenURI(uint256 tokenId) public view returns (string memory) {
        require(
            _exists(tokenId),
            "KIP17Metadata: URI query for nonexistent token"
        );

        string memory _baseURI = baseURI;
        string memory idstr;
        string memory _baseURIBack = baseURIBack;

        uint256 temp = tokenId;
        idstr = uint2str(temp);

        return
            bytes(baseURI).length > 0
                ? string(abi.encodePacked(_baseURI, idstr, _baseURIBack))
                : "";
    }

    function setBaseURI(string memory _baseURI) public onlyOwner {
        baseURI = _baseURI;
    }

    function setBaseURIBack(string memory _baseURIBack) public onlyOwner {
        baseURIBack = _baseURIBack;
    }

    function massMint(uint256 k) public {
        uint256 from = totalSupply();
        uint256 to = from + k;
        for (uint256 i = from; i < to; i += 1) {
            mint(msg.sender, i);
        }
    }
}

그 뒤에 NFT Contract 에서 대량으로 mint 함수를 호출하기 위한 컨트랙트를 작성하였습니다.

pragma solidity ^0.5.6;

import "./token/KIP17/KIP17Mintable.sol";
import "./math/SafeMath.sol";
import "./ownership/Ownable.sol";

contract MintNFT is Ownable {
    using SafeMath for uint256;

    KIP17Mintable public nft;

    constructor(KIP17Mintable _nft) public {
        nft = _nft;
    }

    function nftMint(uint256 num) external onlyOwner {
        for (uint256 i = 1; i < num; i += 1) {
            nft.mint(msg.sender, i);
        }
    }
}

제가 배포한 스마트 컨트랙트의 소스코드는 위와 같습니다. 발행된 KIP-17 NFT를 대량으로 민팅하기 위한 스마트 컨트랙트입니다. 위 컨트랙트의 주소는 “0x3C2b2e68Ac04d2E48A248f1D0E8347322a29802e” 입니다. 이 컨트랙트를 Mint Contract라고 칭하겠습니다. Mint Contract 에서 NFT Contract의 mint 함수를 실행하기 위해 NFT Contract 로부터 Mint Contract 에 addminter 함수를 통해 민팅 권한을 주었습니다.

그리고 caver-js 를 통해 다음과 같은 코드를 실행하였습니다.

mintContract.send({ from: keyring.address, gas: 1000000000 }, "nftMint", 1000)

그랬더니

Error: reached the opcode count limit

라는 오류를 뱉으면서 가스량의 한도치까지 모든 클레이가 가스비로 빠져나가는 현상이 발생하였습니다. 해당 트랜잭션의 해시는 "0x72cb490cfee2b0568f4cc2eec6d873f62612975e41dfb85dcee2917a60638978"이며 해당 블록은 baobab의 “81025655” 블록에 해당합니다. 편의를 위해 스코프 링크 첨부합니다.

트랜잭션 스코프 링크

추가적으로 실험해본 결과 가스비 사용량 제한을 더 높이면 실험계정의 잔고가 가스비 사용량 보다 높은 경우 가스량의 한도치까지 무조건적으로 빠져나가는 현상을 목격했습니다.

단, 아래와 같이 인자로 전달하는 값을 낮추면 문제없이 코드가 작동하며, 가스비도 정상적으로 사용되었습니다.

mintContract.send({ from: keyring.address, gas: 1000000000 }, "nftMint", 10)

(질문1) Mint Contract 의 코드에 문제가 있습니까?
(질문2) Mint Contract 의 nftMint 함수는 무한반복에 빠지는 함수는 아닌 것으로 이해됩니다. 가스비가 제한량까지 높아지는 이유는 무엇입니까?
(질문3) 위 처럼 가스비가 무제한으로 발생하는 것이 오류인지 아닌지가 궁금합니다.
(질문4) 가스사용량 제한을 늘리면 가스 사용량이 무수히 높아집니까?
(질문5) error 명인 reached the opcode count limit은 KLVM에서 계산하는 opcode count limit에 도달한 것으로 이해됩니다. 그렇다면 KLVM은 opcode count limit 을 넘는 트랜잭션 발생 시 가스를 맥시멈까지 사용하고 error를 뱉습니까?
(질문6) klaytn에서 가스비가 너무 높게 계산될 경우 오류를 뱉는 등의 장치는 존재하지 않습니까?

긴 글 읽어주셔서 감사합니다:)

@something

안녕하세요.
모든 질문 항목에 대한 상세 답변까지는
시간이 다소 걸릴 수 있는 점 양해 부탁드립니다.

가장 궁금해하실 법한
"가스비가 기재하신 제한량까지 높아지는 이유"에 대한 부분에 대해서는 먼저 답변을 드려봅니다.

무한 반복에 빠지지 않아도 gas 소모량은 기재하신 1000000000 까지 높아질 수 있습니다.

스마트 컨트랙트를 실행한다 라는 것은 결국 옵코드들을 실행한다는 것이고 각 옵코드들에 소요되는 가스량은 Klaytn Gas Table 에 기재된 만큼 소요가 되게 됩니다.

설령 트랜잭션이 정상 처리되지 못하고 종료된다하더라도 가스가 왜 차감이 될까요? 라는 건 DoS 관점에서 생각해보면 좋을 거 같습니다.
“많은 연산을 요구하는 트랜잭션의 가스를 차감하지 않는다면?” 이라는 질문을 역으로 해보면, 패널티 없이 지속적으로 Klaytn Network에 부하를 줄 수 있겠네? 라는 계산이 나오겠죠.

컴퓨팅 자원은 한정되어 있고, 처리할 수 있는 연산량에도 일정 제한이 있는 상태라면 당연히 패널티가 없는 DoS 형태의 행위들을 원천 방지하는 것이 좋겠죠. 그러지 않으면 다수의 유저들이 불편을 겪을테니까요.

질문 5와 6에 대한 답은 된 거 같네요.

Baobab 테스트넷에서 테스트로 이런 것들을 미연에 알아보시고 방지하신 것은 정말 잘하신 거 같습니다.

도움이 되었길 바랍니다.
감사합니다 :slight_smile:

cc. @Aidan @Jamie @Kale

답변 감사합니다.

해당 연산이 무한의 가스비가 들 정도의 연산인지 궁금합니다. 저 함수의 일정 숫자를 넣는 것 까지는 아무 문제 없이 정상적인 가스비를 소모하는데 그 숫자가 어느정도를 넘어가는 순간 가스비가 기하급수적으로 커지는 것 같습니다.

위의 질문주신 내용처럼 동작하는 것이 맞습니다.
@something 께서 링크주신 Tx처럼 computation cost limit에 도달하는 순간 실행이 종료되며, tx에 서명한 gas limit만큼 가스가 소모됩니다.
Klaytn은 블록 gas limit이 없기 때문에 하나의 트랜잭션에서 과도한 연산을 요구하는 형태의 DoS 공격을 막는 장치가 필요했습니다. 이는 computation cost limit 을 도입한 이유 중 하나이며, 트랜잭션 처리 중 허용값 이상의 과도한 연산이 처리되는 즉시 트랜잭션 실행이 중단되며 모든 가스가 소모되됩니다.

2개의 좋아요