즐코

json-rpc 프로토콜 / web3 라이브러리 / GAS 란?! 본문

BlockChain

json-rpc 프로토콜 / web3 라이브러리 / GAS 란?!

YJLEE_KR 2022. 6. 28. 18:14

web3를 써보기 이전에 이더리움 프로그램이 지원하는 json-rpc 프로토콜에 대해서 가볍게 정리해보았다.

그리고 web3 라이브러리를 테스트 코드로 돌려보았고, 가스비에 대해서도 정리해봄

 

JSON RPC 

RPC = Remote Procedure Call 원격 프로시저 호출..? 

이더리움 클라이언트 프로그램은 RPC 명령어 인터페이스를 자바스크립트의 JSON 형태로 지원한다.

이것을 JSON-RPC API라고 하는데, 이것을 이용해서 블록체인의 정보를 얻거나 거래를 생성하는 명령을 원격으로 할 수 있다.

 

JSON RPC

하나의 간단한 프로토콜이라고 생각하면 된다. HTTP 또는 TCP 프로토콜 하에서 돌아가는 건데, 

URL은 깔끔하게 유지하고 하나의 특정 메소드만 요청, 실행시켜서 하나의 결과물만 리턴받는 프로토콜이라고 보면 된다.

 

간단하게 어제 설치해둔 ganache 테스트용 이더리움 네트워크에 이 json-rpc 요청을 보내는 연습을 해보는데,

요청을 보내기 위한 도구인 curl 이나 postman, thunder client를 써서 이 jsonrpc 에 대해서 이해해본다.

 

geth 대신 로컬에서만 돌어가는 ganache 테스트용 이더리움 네트워크 또한 json-rpc를 받아들이므로,

해당 가나쉬 서버 포트인 8545번으로 jsonrpc 프로토콜 요청을 날려본다. 

 

요청 헤더

: http 프로토콜을 기본으로 따를것이므로 요청 헤더로 "Content-type:application/json" 을 입력해줘야한다.

요청 바디

- id :  optional한 속성 - 가나쉬를 쓸 경우 가나쉬에서 제공해주는 chain id를 써준다.

- jsonrpc : json-rpc 버전

- method : 요청 내용에 대해 적는다. 이더리움 네트워크에서 구현된 메소드를 적어준다.

https://ethereum.github.io/execution-apis/api-documentation/ : 이더리움 json-rpc 메소드 모음

- params: 요청 메소드 상에 매개변수가 필요하다면 params 속성에 넣어준다.

 

opt1. CURL 로 요청하기

curl -X POST \  
     -H "Content-type:application/json" \
     --data '{ "jsonrpc":"2.0", "method":"eth_accounts", "params":[]}'\
     http://localshost:8545

opt2. thunder client 로 요청하기

요청 헤더

요청 바디

참고로, eth_accounts 메소드는 요청하는 노드가 관리하는 계정을 모두 보여달라는 뜻이다. 즉, 우린 가나쉬쪽에 저 요청을 보냈기 때문에 가나쉬가 제공해주는 임의의 계정들을 모두 응답으로 받을 수 있다.

 

eth_getBalance 메소드 : 특정 계정의 잔액 조회 메소드

params 배열의 두번째 인자로는 n번째 블럭까지의 상태값(잔액)을 가져올 때 쓴다.

인자값 안 넣으면 가장 최신 블럭까지의 잔액을 리턴해준다.

이 외에 자주 쓰이는 메소드 

evm_snapshot [] : 저장 용도

evm_revert ["0x위에서 받은 숫자"] : 위에서 스냅샷 찍은 곳으로 돌아간다.

evm_mine ["timestamp값"]

 

이와 같이 블록체인 네트워크와 소통할 수 있겠지만 보통은 web3 라이브러리를 통해 쉽게 블록체인 서버로 요청을 한다.

 

web3 라이브러리

http, ipc, websocket 을 사용하여 로컬 또는 원격으로 이더리움 클라이언트와 소통할 수 있게끔 도와주는 라이브러리 모음집 

web3.js is a collection of libraries that allow you to interact with a local or remote ethereum node using HTTP, IPC or WebSocket

https://web3js.readthedocs.io/en/v1.2.11/getting-started.html : 공식 api를 보면 좀 더 직관적이다.

 

아래와 같이 다양한 모듈의 모음이라고 보면 된다. 우리는 여기서 eth, utils를 많이 사용할 예정이다.

 

  • web3-eth : 이더리움 블록체인과 스마트 계약을 위한 모듈
  • web3-shh : whisper 프로토콜(p2p 기반의 커뮤니케이션과 브로드캐스트)을 위한 모듈
  • web3-bzz : swarm 프로토콜 (탈중앙화 파일 저장소)을 위한 모듈
  • web3-utils :  Dapp 개발자를 위한 여러 유용한 함수를 제공해주는 모듈 

 

테스트 코드를 작성하면서 web3를 연습해보는게 편하므로 jest도 설치해준다. 

$ npm i web3
$ npm i -D jest

jest.config.js 도 작성해준다.

const config = {
  verbose: true,
  testMatch: ["<rootDir>/**/*.test.js"],
};

module.exports = config;

본격적인 테스트 코드 작성!

 

web3를 설치하고 나면, web3 인스턴스를 생성해줄때 provider를 세팅해줘야한다. 즉, 이더리움 블록체인과 연결할때 쓸 노드를 세팅해줘야하는데, 연결할 노드의 연결 프로토콜과 주소/포트 등을 포함한 provider를 넣어줘야한다.

web3는 총 3개의 provider를 제공하는데, HttpProvider, WebsocketProvider, IpcProvider가 있다고 한다.

우린 가나쉬로 돌아가고있는 이더리움 클라이언트를 넣어줄건데 해당 노드는 http에서 동작하기 때문에 HttpProvider를 써준 것이다.

// web3.test.js

const Web3 = require("web3");

describe("web3 테스트코드", () => {
    let web3;
    let acct;
    
    it("web3 연결테스트", () => {
    	web3 = new Web3(new Web3.providers.HttpProvider("http://127.0.0.1:8545"))
        console.log(web3); 
        // 엄청나게 긴 객체가 나온다. 
        // 여기서 web3.eth, web3.utils, web3.shh, web3.bzz를 많이 쓴다고 함)
    })
})

 

기본적으로 web3는 블록체인 네트워크쪽에 요청을 해서 값을 가져오므로 프로미스 객체를 반환하기 때문에 async-await 문법을 쓸 것이다. 

 

web3.eth.getBlockNumber() : 마지막 블록 높이 가져오기

web3.eth.getAccounts() : 전체 계정 가져오기

아래에서 트랜잭션 실행 시 트랜잭션 발행인과 받는 이가 있어야하므로 그냥 임의로 acct의 0번째, 1번째를 각각 발신인, 수신인으로 할당해두었다.

// web3.test.js

const Web3 = require("web3");

describe("web3 테스트코드", () => {
    let web3;
    let acct;
    let sender;
    let recipient;
    
    it("web3 연결테스트", () => {
    	web3 = new Web3(new Web3.providers.HttpProvider("http://127.0.0.1:8545"))
    })
    
    it("latest block height 가져오기", async () => {
    	const latestBlock = await web3.eth.getBlockNumber();
        console.log(latestBlock) // 0
    })
    it("전체 acct 가져오기", async () => {
        accts = await web3.eth.getAccounts();
        console.log(accts);
        /*
       [
         '0x35DdEB3c2e158704F4b5CFB578DFf13C7492F74A',
         '0x1718552C7623778302b3D0193ac95836Ff52cC03',
         '0x20e01A12b3b0FB7959051E857fdcF9FAE641bBb6',
         '0x908EbeD74AF16bfB1C052Ed9eC5CbE628e9F9AE0',
         '0xCA51C87d62907290DB2b4086797d8dC8329FFE7C',
         '0x1359ea14b31D05c3593f0181a86C7066e269090c',
         '0xA38F327cDf5a3c44c3002655eC1ef8Dd51e75002',
         '0xea1A199FaBe3740e10893264Ac13a1803250c9EB',
         '0x88af4f55510D9cd2EADa232Fb94A82d1F2b6819C',
         '0xe75e30Bf09148Fa073E2c3d9671b00DB6e3aEe91'
      ]
        */
        sender = accts[0];
        recipient = accts[1];
    })
})

web3.eth.getBalance() : 특정 계정의 잔고 가져오기

보통 wei 단위로 표현된다. ETH 단위로 보고싶다면 10**18로 나눠주면 된다!

// web3.test.js

it("첫번째 계정 balance 가져오기", async () => {
    const balance = await web3.eth.getBalance(accts[0]);
    console.log(balance); // 100000000000000000000wei = 100ETH (wei로 표현)
    console.log("ETH :", balance / 10 ** 18); // ETH : 100
  });

 

이더리움의 단위

단위 설명
wei 이더의 가장 작은 단위
거의 기술적, 코드 작성에만 사용
 
gwei 가스(네트워크 거래 수수료)에 대해 말할 때 가장 일반적으로 사용하는 단위  1gwei = 10^9 wei
ETH 가장 일반적인 액면가
실질적인 거래 대부분은 ETH의 관점에서 생각 
1ETH = 2gwei = 10^18 wei

 

web3.utils.toWei('amount', '단위') : 다른 단위인 gwei, ETH를 wei로 변환시킨 값 리턴

// web3.test.js

it("ETH 단위 변경해보기", async () => {
    console.log(web3.utils.toWei("1", "gwei")); // 1,000,000,000 = 10**9
    console.log(web3.utils.toWei("1", "ether")); // 1,000,000,000,000,000,000 = 10**18
  });

web3.eth.getTransactionCount() : 인자로 받은 계정의 트랜잭션 횟수를 구해주는 메소드

web3.utils.toHex() : 인자로 받은 값을 16진수로 변환해주는 메소드

// web3.test.js

it("트랜잭션 횟수 구해오기", async () => {
    const txCount = await web3.eth.getTransactionCount(sender);
    console.log(txCount); // 0
    console.log(web3.utils.toHex(txCount)); // 0x0
  });

위에서 나온 트랜잭션 횟수 (transaction Count)는 nonce 라고 한다.

이 트랜잭션이 트랜잭션을 생성하는 계정 내에서 몇번째로 발생하는 트랜잭션인지를 나타내는 16진수 값이다.

이중 지불과 같은 악의적인 공격이나 트랜잭션 순서가 뒤섞이는 걸 방지하기 위해 이 nonce를 활용한다고한다.

즉, 이전에 POW를 위해서 채굴에서 썼던 nonce 값과 이 이더리움 트랜잭션에 쓰는 nonce는 다른 개념이다.

 

 

GAS

 

저번 포스팅에서 정리한 바 EVM 은 스마트 컨트랙트 이행을 위한 바이트 코드를 실행시키는 가상 머신이고, 모든 이더리움 블록체인 노드가 공유한다고 했다. 그렇기 때문에 무한루프 같은 과부하나 독점 방지를 위해 GAS 를 도입했다고 보면 쉽다.

 

1. GAS 란 무엇일까..

모든 이더리움 플랫폼에서 트랜잭션을 실행하기 위한 네트워크 수수료의 단위

ETH, wei처럼 화폐개념의 단위가 아닌 일의 양을 나타내는 단위이다. 

 

채굴자들은 목표 hash에 맞는 nonce값을 찾게 되면 블럭 채굴에 대한 보상과 동시에 트랜잭션 수수료를 받게 되는데, 

가스는 EVM 상에서 트랜잭션을 동작시키기위해 소모되는 비용을 뜻한다. 따라서 코드의 복잡성에 따라 가스양이 다르게 측정된다.

간단한 계산일수록 적은 gas가 필요하고, 복잡한 계산일수록 더 많은 gas가 필요하다. 

 

2. 왜 수수료를 이더리움으로 표시하지 않고 gas라는 개념을 사용했을까?

이더리움은 시장가격이기때문에 계속해서 가격이 변동된다. 이 이더리움의 가격에 따라 수수료를 정한다면 수수료는 계속해서 달라질 것이다. 물론 gas도 완전히 고정된 단위는 아니지만 이 gas 수에 따라 수수료를 표시한다. 

 

GAS Limit

: 트랜잭션을 생성하는 쪽에서 트랜잭션 실행 시 얼마의 가스가 소모될지 예측해서 넣어주는 부분, 작업량 예상치 

 

GAS Price

: GAS 개념과 달리 이 GAS price는 ether 가격에 따라 변동된다. (gwei가 기본 단위)

모든 이더리움 내 거래가 채굴자들에 의해 선택되는 것이므로, 이 부분을 조정해줌으로써 채굴자들의 선택을 받게끔 수수료 조정이 가능하다.

 

총 수수료 = '쓰이는 Gas의 총량' x 'Gas Price'

 

처음 트랜잭션을 만들어서 보낼땐 'Gas Limit' (startgas) x 'Gas Price' 를 보낸다.

이 때, 이 gaslimit을 좀 높게 잡아서 전체 계약이 실행되고 gas가 남으면 그 gas에 해당하는 ether는 발신자에게 환급된다.

하지만, gaslimit을 낮게 책정해서 실제 사용량보다 모자르다면, 그 컨트랙트의 실행은 멈추고 다시 처음으로 돌아가며 gas비는 환급되지 않느다. 마치 네비에 도착지를 잘못 찍어서 기름을 다 써버리고 다시 새 목적지로 가야하는 꼴이 되버린다. 

 

EVM 동작순서

1/ 트랜잭션 검증

2/ 트랜잭션 수수료 계산 = gas limit x gas price, 트랜잭션 실행될때 해당 수수료가 송신 계정에서 차감됨

3/ gas 지불 초기화 = 이 시점부터 트랜잭션에서 처리된 바이트 양만큼 가스를 차감

4/ 트랜잭션 금액을 수신 계정으로 보냄 + smart contract도 이 시점에서 실행됨

   - 이 때 트랜잭션이 완료되었는데 가스비가 남았다면?

    트랜잭션 종료와 동시에 송신 계정으로 환불되고 채굴자는 (startgas - 환불 gas) * gasPrice만큼 보상을 받는다.

5/ 송신 계정에서 트랜잭션 완료할만큼 가스비가 충분하지 않으면?

    - 이 수수료는 환불되지 않고 채굴자에게 지불되버림

 

 

ethereumjs-tx 라이브러리

트랜잭션 객체를 이더리움 클라이언트가 이해할수 있게 만들어주는 라이브러리인데, 서명도 만들어준다.

$ npm i ethereumjs-tx
const ethTx = require("ethereumjs-tx").Transaction;

const tx = new ethTx(txObj);

tx.sign(privateKey)  : 트랜잭션 객체에 서명을 포함해준다.

tx.serialize() : 자바스크립트 트랜잭션 객체를 바이트 코드로 인코딩해줘서 언어나 환경에 상관없이 실행될수 있게끔 도와주는 메소드

// web3.test.js

const ethTx = require("ethereumjs-tx").Transaction;

it("트랜잭션 실행하기", async () => {
    const txCount = await web3.eth.getTransactionCount(sender);
    const privateKey = Buffer.from(
      "bd318e46b63dc35a078a147e252cab7121200ba8c932f39f639b55edc98d6cc4",
      "hex"
    );
    const txObj = {
      nonce: web3.utils.toHex(txCount), 
      // 해당 논스는 16진수로 변환해줘야한다. (+1은 네트워크 자체에서 더해준다)
      from: sender,
      to: recipient,
      // 보내는 금액 단위는 wei로 + 16진수로 바꿔서 보내줘야함
      value: web3.utils.toHex(web3.utils.toWei("1", "ether")),
      // 해당 트랜잭션 실행 시 필요한 예상 가스량
      gasLimit: web3.utils.toHex(6721975), 
      // 1 가스당 가격 (단위는 gwei로 + 16진수로 바꿔서)
      gasPrice: web3.utils.toHex(web3.utils.toWei("1", "gwei")), 
      // smart contract 배울 때 다룰 예정
      data: web3.utils.toHex(""), 
    };
    const tx = new ethTx(txObj);
    tx.sign(privateKey); // tx 상에 시그니처 포함만 해주는 메소드 그래서 반환값이 void
    console.log(tx); // 아래 스크린샷 확인

    const serializedTx = tx.serialize();
    const TransactionObject = await web3.eth.sendSignedTransaction(
      "0x" + serializedTx.toString("hex")
    );
    console.log(TransactionObject); // 트랜잭션 오브젝트에는 시그니처 내용이 안보임
    /*
   {
      transactionHash: '0xdf062609ec32dbc9dd7290118658ff5e0dc95cc5445e55f5545d3d0292101fd3',
      transactionIndex: 0,
      blockHash: '0x2546c5a987a88aa41986edfb58505f3fbfb1d11f36f8a78de5649ce12ec1f382',
      blockNumber: 57,
      from: '0x35ddeb3c2e158704f4b5cfb578dff13c7492f74a',
      to: '0x1718552c7623778302b3d0193ac95836ff52cc03',
      gasUsed: 21004,
      cumulativeGasUsed: 21004,
      contractAddress: null,
      logs: [],
      status: true,
      logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
    }
    */
  });

서명이 포함된 tx 객체는 아래와 같이 생겼다. 

 

web3.eth.getBalance(계정) : 인자로 받은 계정의 밸런스를 조회해주는 메소드

// web3.test.js

it("balance 확인", async () => {
    const SenderBalance = await web3.eth.getBalance(sender);
    const RecipientBalance = await web3.eth.getBalance(recipient);

    console.log("sender balance: ", SenderBalance / 10 ** 18);
    // sender balance:  40.998760764000004
    console.log("recipient balance: ", RecipientBalance / 10 ** 18);
    //  recipient balance:  159
  });

 

Comments