Account / EVM

Luniversity에 오신 여러분을 환영합니다!😆 앞서 진행했던 Transaction은 재밌게 보고 오셨나요? 아직 진행하지 않았다면 여기를 클릭하여 Transaction에 대해서 배워보세요!

이번 시간에는 Account란 무엇인지, 어떤 구조로 구성되어 있는지 확인해보고 Account를 편리하게 이용할 수 있는 Luniverse Multichain API를 알아보도록 하겠습니다. 또한 지금까지 배운 내용을 통해 이더리움이 어떻게 동작하는지 정리해 보도록 하겠습니다.


Account란?

Account란 이더리움 네트워크에서 유일한 identifier로 이더리움 내에서 트랜잭션을 보낼 수 있는 Entity 입니다. Account는 EOA와 CA로 나눌 수 있으며 모든 Account는 Ethereum의 World State라는 Database에 State로 저장됩니다. Address와 World State에 저장되어 있는 Account State는 매핑되어 있어 Address를 이용하여 Account의 State를 확인할 수 있습니다.

World State는 노드마다 저장하고 있는 분산된 Ethereum Database로 가장 최신의 Account State(모든 계정의 현재 잔액, Storage State 등)을 저장하고 있습니다. 트랜잭션이 발생하면 Account의 data에 변경이 발생하므로 World State는 각 블록이 생성될 때 마다 업데이트 되어 최신화 됩니다.


  • EOA란?

Externally Owned Account의 약자로 비대칭 암호화 키 쌍으로 구성된 Account 입니다. 개인키를 이용하여 제어할 수 있으며 다른 EOA 혹은 Contract에 ETH를 전송하거나 상호작용하여 Method를 호출하는데 이용됩니다.

  • CA란?

Contract Account의 약자로 이더리움의 Account 중 하나인 Contract Account를 의미합니다. 보통 Contract Address의 의미로도 사용됩니다. 스마트 컨트랙트를 배포하면 자동으로 생성되며 트랜잭션 수신에 대한 응답으로만 트랜잭션을 실행할 수 있습니다.


Account의 구조

Account는 Address와 Mapping이 되어있으며 4개의 Field를 가지고 있습니다. 각 Field에 대한 설명은 다음과 같습니다.

  • Nonce : Ethereum에서 트랜잭션을 처리하기 위한 값으로 트랜잭션 Counter 역할을 합니다. 기본값은 0이며 트랜잭션을 실행할 때 마다 1씩 증가합니다.
  • Balance : Account가 소유하고 있는 ETH의 수량으로 단위는 Wei 입니다.
  • Storage hash : Contract의 상태 데이터를 저장한 머클 루트 입니다. CA에만 존재하는 값 입니다.
  • Code hash : EVM에서 실행될 Contract(EVM Code)의 Bytecode를 해시한 값 입니다. CA에만 존재합니다.


EOA의 경우, Contract가 아니기 때문에 Storage hash와 Code hash의 값이 빈 값으로 설정되어 있으나 CA에는 Storage hash와 Code hash의 값이 존재합니다. Account Storage는 계정의 상태 데이터를 해시하여 이를 머클 루트로 스마트 컨트랙트의 실행 결과에 따라 값이 변경됩니다. Code hash의 경우, 유저가 배포하는 컨트랙트를 컴파일한 Bytecode 값을 hash한 데이터가 저장됩니다.


EVM

EVM은 이더리움 가상 머신(Ethereum Virtual Machine)의 약자로 이더리움에서 스마트 컨트랙트를 실행하는데 사용되는 가상머신 입니다. 이더리움 노드마다 보유하고 있으며 EVM을 통해 모든 노드가 동일한 환경에서 계산을 수행하여 신뢰성을 보장합니다.

트랜잭션이 실행되면 EVM은 해당 트랜잭션에 대해 서명, gasPrice, gasLimit을 확인하여 유효성을 검사합니다. 이후 트랜잭션 실행에 필요한 모든 작업에 대한 Gas 비용을 계산하고, 해당 트랜잭션이 호출하는 스마트 컨트랙트를 불러옵니다. 불러온 스마트 컨트랙트의 코드를 실행한 후, 결과를 반환하여 이를 적용하여 상태(State)가 변경됩니다.

EVM의 구조는 다음과 같습니다.

  • Virtual ROM : EVM Code를 불러오는 가상의 ROM 입니다.
  • Program Counter : 실행 중인 명령어의 위치를 추적하기 위해 사용되는 Counter 입니다.
  • Gas Available : 스마트 컨트랙트의 작업이 실행될 떄마다 소비되는 Gas 비용을 추적하여 남은 Gas 양을 나타냅니다. 작업 중에는 항상 Gas Available의 값이 갱신되며 이 값이 0이 되면 작업이 중지됩니다. 트랜잭션의 gas 한도는 Raw Data에 작성된 gasLimit을 바탕으로 계산됩니다.
  • Stack : 트랜잭션 처리 중 계산 결과를 저장하는데 사용되며 32Bytes words를 최대 1024개 저장할 수 있습니다.
  • Memory : 동적 할당 메모리로 프로그램 실행 중 생성된 데이터를 저장하는데 사용됩니다.
  • Account Storage : Key-Value 형태로 저장되며 256bits 로 load/store 합니다. 해당 Account의 데이터를 불러와 트랜잭션을 실행하고 결과에 따라 상태를 변경하여 최신화 합니다. Account의 Storage 데이터를 EVM에 적용하여 사용합니다.

이더리움 중간 정리!

여기까지 Luniversity를 진행하였다면 정말 간단하게 이더리움이 어떻게 동작하는지 정리할 수 있을 것입니다. 이더리움이 동작하는 프로세스를 간단하게 정리해 볼까요?


  1. Account가 트랜잭션을 생성하고 서명하여 노드에게 전달합니다. (Account를 가진 유저라면 누구든 트랜잭션을 생성할 수 있습니다.)


  1. 노드는 이러한 트랜잭션을 검증한 뒤 트랜잭션을 Transaction pool에 저장한 후, 자신이 원하는 트랜잭션을 선택하여 트랜잭션을 실행합니다. (보통 수수료가 높은 트랜잭션을 선택합니다. 블록에 담기는 트랜잭션의 gas 크기가 제한되어 있기 때문에 높은 수수료를 주는 트랜잭션을 선택해야 많은 수수료를 받을 수 있기 때문입니다. 현재 이더리움에서 제한하는 블록 당 최대 가스량은 30,000,000 입니다.) 트랜잭션에 대한 자세한 정보는 이 곳(Transaction Part Link)을 클릭하여 확인할 수 있습니다.


  1. 노드는 실행된 트랜잭션과 결과(Receipt), 상태 변화(State)를 반영한 뒤 이러한 데이터를 이용해 새로운 블록을 생성합니다.


  1. 노드는 자신이 생성한 새로운 블록을 주변 노드들에게 전파하고 새로운 블록을 전파받은 노드들은 블록을 검증합니다. 검증이 완료되면 새로운 블록을 이전 블록에 연결하고 다음 블록 생성을 위해 위의 과정을 반복합니다. 검증하는 과정은 새로운 블록을 전파받은 노드가 전파받은 블록에 담긴 트랜잭션을 다시 실행하게 되며 이 결과로 얻은 데이터가 전파받은 블록의 데이터와 일치한다면 검증이 성공한 것 입니다.


Account를 편리하게 확인할 수 있는 Luniverse Multichain API

Luniverse는 개발자가 Account에 대해 필요한 데이터를 편리하게 확인할 수 있도록 다양한 API를 제공합니다. 이번 시간에는 Luniverse가 제공하는 Account API를 체험해 보도록 하겠습니다.

이번 시간에 체험할 API의 목록은 다음과 같습니다.

listAccountBalance API는 조회하는 계정이 보유한 ETH의 잔고와 토큰의 잔고를 확인할 수 있는 API 입니다. 먼저 curl을 이용하여 JSON-RPC로 Account의 Balance를 조회할 수 있는 eth_getBalance를 실행해 보겠습니다.

📘

How to get the RPC-Endpoint?

Luniverse Multichain API를 이용하기 위해서는 해당 Node를 특정하는 RPC-Endpoint가 필요합니다. 아래 링크를 통해 Luniverse에서 제공하는 RPC-Endpoint를 얻을 수 있습니다.

노드 연동하여 RPC-Endpoint 구하기(링크)


JSON-RPC를 이용하여 eth_getBalance를 실행하는 코드는 다음과 같습니다.

curl -X POST "https://ethereum-sepolia.luniverse.io/{Your Node ID}" \
--header 'Content-Type: application/json' \
--data '{
    "jsonrpc":"2.0","method":"eth_getBalance",
    "params":["{Your EOA Address}", "latest"],
    "id":1
    }'

eth_getBalance에 대한 결과로 아래와 같은 값을 반환받을 수 있습니다. 16진수 값으로 반환되며 ETH의 잔고를 반환합니다.

{
"jsonrpc": "2.0",
"id": 1,
"result": "0xf66479d81641780"
}

JSON-RPC를 이용하여 ERC20과 같은 토큰의 잔고를 조회하기 위해서는 eth_call을 통해 컨트랙트의 Method를 호출해야 합니다. 호출하는 방법은 다음과 같습니다.

curl -X POST "https://ethereum-sepolia.luniverse.io/{Your Node ID}" \
--header 'Content-Type: application/json' \
--data '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "eth_call",
  "params": [{
      "to": "{Contract Address}",
      "data": "0x70a08231000000000000000000000000{your EOA Address except for 0x}"
  }, "latest"]
}'

eth_call Method를 호출하였고 data field에 해당 컨트랙트의 balanceOf Method를 호출하기 위한 데이터를 입력합니다. 즉, 입력한 EOA의 해당 Contract로 발행한 토큰의 Balance를 조회하는 것입니다. 이에 대한 응답은 다음과 같습니다.

{
"jsonrpc": "2.0",
"id": 1,
"result": "0x000000000000000000000000000000000000000000000000016345785d8a0000"
}
  • Node.js를 이용한 Luniverse Multichain API 호출

이번엔 Node.JS를 이용하여 Luniverse에서 제공하는 listAccountBalance API를 호출해 보도록 하겠습니다.

📘

How to get Access Token?

Access Token은 API를 호출하기 위해 필요한 Header의 Authorization 입니다. Access Token을 생성하는 방법은 아래 링크를 클릭하여 확인할 수 있습니다.

Access Token 생성하기(클릭)

🚧

INSTALLATION

예제를 실행하기 위해서는 라이브러리를 설치해야 합니다. 아래 명령어를 입력하여 필요한 라이브러리를 설치할 수 있습니다.

$ npm install axios -save


const axios = require('axios');

const address = "{Your EOA Address}";
const options = {
  method: 'GET',
  url: 'https://web3.luniverse.io/v1/ethereum/sepolia/accounts/${address}/balance',
  headers: {
    accept: 'application/json',
    Authorization: 'Bearer {Your Access Token}'
  },
	params: {
		page : 1,
		rpp : 10,
	}
};

axios
  .request(options)
  .then(function (response) {
    console.log(response.data);
  })
  .catch(function (error) {
    console.error(error);
  });

Path Params에 Address를 입력한 후, 해당 코드를 실행하면 다음과 같은 응답을 받을 수 있습니다.

{
  "code": "SUCCESS",
  "data": {
    "count": 1,
    "page": 1,
    "rpp": 20,
    "items": [
      {
        "asset": {
          "path": "/ethereum/sepolia/assets/native",
          "type": "native",
          "symbol": "ETH",
          "name": "ethereum-mainnet",
          "decimals": 18
        },
        "amount": "6235463429461539"
      }
    ]
  }
}

위와 동일한 API에 간단한 Parameters를 추가하는 것으로 FT 혹은 NFT 잔고를 확인할 수 있습니다. 추가되는 Parameters는 Type(ft, nft)과 contract(contract Address)가 있습니다. 테스트 호출을 위해 USDT의 CA와 Type(ft)를 입력하여 호출했을 때 반환받는 값은 다음과 같습니다.

//FT

const axios = require('axios');

const address = "{Your EOA Address}";
const options = {
  method: 'GET',
  url: 'https://web3.luniverse.io/v1/ethereum/sepolia/accounts/${address}/balance',
  headers: {
    accept: 'application/json',
    Authorization: 'Bearer {Your Access Token}'
  },
	params : {
		type : "ft",
		contract : "{Contract Address}",
		page : 1,
		rpp : 10
	}
};

axios
  .request(options)
  .then(function (response) {
    console.log(response.data);
  })
  .catch(function (error) {
    console.error(error);
  });
{
  "code": "SUCCESS",
  "data": {
    "count": 1,
    "page": 1,
    "rpp": 20,
    "items": [
      {
        "asset": {
          "name": "Tether USD",
          "symbol": "USDT",
          "contract": "0xdac17f958d2ee523a2206206994597c13d831ec7",
          "decimals": 6,
          "type": "ft",
          "supply": "35283904986788565",
          "path": "/ethereum/mainnet/assets/0xdac17f958d2ee523a2206206994597c13d831ec7"
        },
        "amount": "2890173400"
      }
    ]
  }
}

listMultiAccountsBalance API의 경우, 위와 같은 기능의 Method이나 address를 addresses 라는 이름의 배열로 받아 배열에 포함되어 있는 모든 address의 ETH 잔고를 확인할 수 있는 API 입니다. 실행하는 방법은 다음과 같습니다.

const axios = require('axios');

const options = {
  method: 'POST',
  url: 'https://web3.luniverse.io/v1/ethereum/sepolia/accounts/balance',
  headers: {
    accept: 'application/json',
    'content-type': 'application/json',
    Authorization: 'Bearer {Your Access Token}'
  },
  data: {
    addresses: [
      '{Address 1}',
      '{Address 2}',
      '{Address 3}'
    ]
  }
};

axios
  .request(options)
  .then(function (response) {
    console.log(response.data);
  })

실행 후, 다음과 같은 응답을 받을 수 있습니다. Body Params의 addresses Field에 입력한 모든 주소가 보유한 ETH의 잔고를 반환합니다.

{
  "code": "SUCCESS",
  "data": {
    "asset": {
      "path": "ethereum/mainnet/assets/native",
      "type": "native",
      "symbol": "ETH",
      "name": "ethereum-mainnet",
      "decimals": 18
    },
    "0xdAC17F958D2ee523a2206206994597C13D831ec7": {
      "amount": "1"
    },
    "0xB8c77482e45F1F44dE1745F52C74426C631bDD52": {
      "amount": "13114317343654680563"
    }
  }
}

listAccountTransactions API는 특정 계정과 관련된 트랜잭션 이력을 조회할 수 있는 API 입니다. 다음과 같이 입력하여 실행할 수 있습니다.

const axios = require('axios');

const address = "{Your EOA Address}";
const options = {
  method: 'GET',
  url: 'https://web3.luniverse.io/v1/ethereum/sepolia/accounts/${address}/transactions',
  headers: {
    accept: 'application/json',
    Authorization: 'Bearer {Your Access Token}'
  }
};

axios
  .request(options)
  .then(function (response) {
    console.log(response.data);
  })
  .catch(function (error) {
    console.error(error);
  });

실행 후, 다음과 같은 응답을 받을 수 있습니다.

{
  "code": "SUCCESS",
  "data": {
    "path": "/ethereum/sepolia/accounts/0xdbfd76af2157dc15ee4e57f3f942bb45ba84af24/transactions",
    "count": 379,
    "page": 1,
    "rpp": 10,
    "items": [
      {
        "path": "/ethereum/sepolia/transactions/0xa56b5fad2e9d076a5f5961d9888f2acaafaa1e28e7010a2d0041bb43108a0dbe",
        "hash": "0xa56b5fad2e9d076a5f5961d9888f2acaafaa1e28e7010a2d0041bb43108a0dbe",
        "from": "0x45fa00ccab9f3dc4ec1830dc516c9f48ec671046",
        "to": "0xdbfd76af2157dc15ee4e57f3f942bb45ba84af24",
        "value": "0x0",
        "data": "0x844819530000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000002345",
        "nonce": "0x8b",
        "gas": "0x5ce27",
        "gasPrice": "0xff3e695f6",
        "maxFeePerGas": "0x161e85491f",
        "maxPriorityFeePerGas": "0x5f5e100",
        "status": "success",
        "timestamp": 1681965227,
        "block": "0xebd38f272ace51c743ab7dc84e18d0be79076094025e677b75a5a3b35c069ada",
        "receipt": {
          "from": "0x45fa00ccab9f3dc4ec1830dc516c9f48ec671046",
          "to": "0xdbfd76af2157dc15ee4e57f3f942bb45ba84af24",
          "gasUsed": "0x46ec1",
          "logs": [
            {
              "blockHash": "0xebd38f272ace51c743ab7dc84e18d0be79076094025e677b75a5a3b35c069ada",
              "address": "0x00000000000076a84fef008cdabe6409d2fe638b",
              "logIndex": 326,
              "data": "0x000000000000000000000000dbfd76af2157dc15ee4e57f3f942bb45ba84af2400000000000000000000000045fa00ccab9f3dc4ec1830dc516c9f48ec671046000000000000000000000000bc4ca0eda7647a8ab7c2061c2e118a18a936f13d00000000000000000000000000000000000000000000000000000000000023450000000000000000000000000000000000000000000000000000000000000001",
              "removed": false,
              "topics": [
                "0xe89c6ba1e8957285aed22618f52aa1dcb9d5bb64e1533d8b55136c72fcf5aa5d"
              ],
              "blockNumber": 17085387,
              "transactionIndex": 113,
              "transactionHash": "0xa56b5fad2e9d076a5f5961d9888f2acaafaa1e28e7010a2d0041bb43108a0dbe"
            }
          ],
          "cumulativeGasUsed": "0xcc1782",
          "effectiveGasPrice": "0xff3e695f6",
          "contractAddress": null,
          "transactionIndex": 113,
          "transactionHash": "0xa56b5fad2e9d076a5f5961d9888f2acaafaa1e28e7010a2d0041bb43108a0dbe",
          "status": "0x1",
          "logsBloom": "0x00000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000"
        }
      },
	    {
      ...
      }

이렇게 Account와 EVM에 대해서 알아보고 Nova를 이용해서 Account Data를 조회하는데 필요한 API까지 호출해 보았습니다. 이제 이더리움 네트워크가 동작하는 방식에 대해서 이해가 되시나요? 😄😄

다음 시간에는 이더리움 Transaction에서 볼 수 있는 Event와 Log가 무엇인지 알아보고 실제 Event가 작성된 컨트랙트를 배포, Method를 호출하여 Log 값을 확인해 보도록 하겠습니다!


Luniverse는 Public Blockchain을 손 쉽게 이용할 수 있도록 다양한 API와 Node Provider Service를 제공하고 있습니다. 오늘 학습한 내용 외에도 다양한 API를 제공하고 있으며 여기(Luniverse Multichain API 확인하기)를 클릭하여 Nova가 제공하는 다양한 API를 확인해 볼 수 있습니다! 😄😄