본문 바로가기

Tech

[Tech] 이더리움 트랜잭션 분석

 

 

안녕하세요! 헤이비트 개발자 Qwerty입니다.

 

저는 헤이비트에서 디지털 자산 투자 서비스를 위한 스마트 컨트랙트를 분석하고 있습니다.

스마트 컨트랙트는 비탈릭 부테린(Vitalik Buterin)이 비트코인을 포크(fork)하여 이더리움 블록체인 네트워크에 구현한 것을 시작으로, 현재는 클레이튼, 바이낸스 스마트 체인, 테라, 솔라나와 같은 수많은 블록체인 네트워크에서 지원하고 있습니다.

 

스마트 컨트랙트는 토큰처럼 간단한 것으로부터 시작해서 스마트 컨트랙트가 복잡하게 상호작용하는 디파이(DeFi)와 같은 형태로 배포되어 있습니다.

 

그런데 이 스마트 컨트랙트는 절대로 스스로 동작하지 않습니다. 반드시 메타 마스크와 같은 지갑을 통해 트랜잭션을 생성해야만 비로소 스마트 컨트랙트가 동작을 하는데요, 오늘은 스마트 컨트랙트를 움직이게 하는 트랜잭션의 구조에 대해서 소개해보려 합니다.

 

web3

Web3는 데이터가 중앙 서버에 집중된 형태로 기록된 기존의 네트워크와 달리, 탈중앙화 된 형태의 네트워크를 의미하는데요,
블록체인이 대표적인 Web3의 형태에 해당합니다.

 

하지만, 이런 블록체인과 상호작용을 할 수 있는 방법이 없다면 접근성은 낮을 수밖에 없겠죠.

블록체인 네트워크마다 방식은 다르지만 저마다 각각 상호작용할 수 있는 방법이 있습니다.

 

이더리움 블록체인은 web3.js 자바스크립트 라이브러리 또는 web3.py 파이썬 라이브러리를 주로 사용합니다.

이들은 HTTP, Web Socket을 동해 json rpc 메시지를 주고 받음으로써 상호작용을 합니니다.

 

상호작용의 방식은 크게 두 가지로 나눌 수 있습니다. 단순히 상태를 읽어보는 방식(Query)과 상태를 변경하는 방식(Transaction)이 있습니다. 이 두 방식을 적용하는 대상도 블록체인 그 자체가 될 수도 있고, 또는 스마트 컨트랙트가 될 수도 있습니다.

 

 

쿼리와 트랜잭션

블록체인 네트워크에 배포된 스마트 컨트랙트는 스스로 동작하지 않습니다.

스마트 컨트랙트는 메시지를 받아 동작하는데, 상태를 읽는 쿼리(Query)와 상태를 변경하는 트랜잭션(Transaction)으로 나눌 수 있습니다.

 

우리가 정말 자주 사용하고 있는 지갑 잔액을 조회하는 쿼리 메시지부터 확인해봅시다.

 

ETH 잔액 조회

이더리움 블록체인에 getBalance 요청을 보냄으로써 특정 주소의 ETH 잔액을 확인할 수 있습니다.

 

아래 코드는 바이낸스의 이더리움 지갑이 보유한 ETH의 잔액을 조회하는 예시입니다.

const Web3 = require('Web3');
const web3 = new Web3(/* Ethereum Mainnet RPC Node */);

const balance = await web3.eth.getBalance(0x4976A4A02f38326660D17bf34b431dC6e2eb2327);
console.log(balance);

위 코드를 실행하면 getBalance 함수의 매개변수로 전달된 주소 0x4976A4A02f38326660D17bf34b431dC6e2eb2327 의 ETH 잔액이 출력됩니다.

 

앞에서 서술했듯이, web.js 라이브러리는 json rpc 메시지를 이더리움 네트워크 노드로 전송합니다.

전송되는 json rpc 메시지는 아래와 같습니다.

{
  "jsonrpc":"2.0",
  "id":1,
  "method":"eth_getBalance",
  "params":[
    "0x4976A4A02f38326660D17bf34b431dC6e2eb2327",
    "latest"
  ]
}

 

위 json rpc 메시지에서 나타난 키의 의미는 아래 표에서 확인할 수 있습니다.

Key Description
jsonrpc json rpc 버전을 의미합니다.
id 클라이언트에서 요청에 대한 응답을 식별하기 위한 값입니다. 응답의 id는 항상 요청의 id와 동일합니다.
method 호출하려는 메서드의 이름을 지정합니다.
params 메서드에 전달하는 매개변수를 지정합니다.
json rpc 버전을 2.0을 주로 사용하는 이유는 이더리움 네트워크 노드가 2.0 버전을 공통적으로 지원하기 때문입니다.
관련 문서에서 json rpc 지원 현황을 확인할 수 있습니다.

 

ERC20

이더리움 블록체인에 getBalance 메시지를 전달하여 ETH 잔액을 조회할 수 있었습니다.

하지만 이더리움 블록체인 상에서 거래되는 나머지 토큰들은 같은 방식으로 잔액을 조회할 수 없습니다.

그 이유를 이해하기 위해서는 먼저 코인(Coin)과 토큰(Token)의 차이점을 이해할 필요가 있습니다.

 

코인은 한 블록체인 상에서 거래되는 표준 통화를 의미합니다.

트랜잭션 검증 수수료인 가스비로 사용되기도 하며, 반드시 하나만 존재하지도 않습니다.

블록체인 코인 토큰
비트코인 BTC -
이더리움 ETH USDT, USDC, Wrapped BTC, ...
솔라나 SOL USDT, USDC, Wrapped Ethereum, ...
테라 LUNA, UST, KRT, ... ANC, aUST, bLUNA, ...

하지만 표준 통화인 ETH와 달리, 토큰은 토큰으로서 인정받기 위한 최소 요건을 만족하는 스마트 컨트랙트입니다.

블록체인마다 토큰 표준의 기준은 다릅니다. 가령, 이더리움에서 대체 가능한 토큰으로 인정받기 위해서는 EIP-20에서 제안한 표준 함수를 구현해야 하며, 테라는 CW20, 클레이튼은 KIP-7 표준을 구현해야 비로소 토큰으로 인정을 받을 수 있습니다.

 

ERC20 잔액 조회

ERC20 토큰은 곧 이더리움 블록체인 네트워크에 배포된 스마트 컨트랙트입니다.

EIP-20에서 제안하는 대체 가능한 토큰은 balanceOf 함수를 통해 특정 지갑 주소가 보유한 토큰의 수량을 조회할 수 있습니다.

 

아래 코드는 바이낸스의 이더리움 지갑이 보유한 USDT의 잔액을 조회하는 예시입니다.

const Web3 = require('Web3');
const web3 = new Web3(/* Ethereum Mainnet RPC Node */);

const ERC20_ABI = [
  {
    "constant":true,
    "inputs":[
      {
        "name":"who",
        "type":"address"
      }
    ],
    "name":"balanceOf",
    "outputs":[
      {
        "name":"",
        "type":"uint256"
      }
    ],
    "payable":false,
    "stateMutability":"view",
    "type":"function"
  }
];

const USDT_TOKEN = '0xdAC17F958D2ee523a2206206994597C13D831ec7';

const usdt = new web3.eth.Contract(ERC20_ABI, USDT);
const blanceOf = usdt.methods.balanceOf('0x4976A4A02f38326660D17bf34b431dC6e2eb2327');

const balance = balanceOf.call();
console.log(balance);

web3.eth.Contract 함수는 스마트 컨트랙트의 ABI와 주소를 받아 스마트 컨트랙트의 프록시 객체를 생성합니다.

이 프록시 객체를 통해 스마트 컨트랙트 호출 객체를 생성하여 호출할 수 있습니다.

 

위 코드를 호출하면 아래와 같은 json rpc 메시지가 생성하여 이더리움 네트워크 노드로 전송합니다.

{
  "jsonrpc":"2.0",
  "id":2,
  "method":"eth_call",
  "params":[
    {
      "data":"0x70a082310000000000000000000000004976A4A02f38326660D17bf34b431dC6e2eb2327",
      "to":"0xdAC17F958D2ee523a2206206994597C13D831ec7"
    },
    "latest"
  ]
}

 

ETH 잔액 조회 요청과 ERC20 토큰 잔액 조회 요청 비교

ETH 잔액을 조회하는 요청과 ERC20 토큰 잔액을 조회하는 요청의 눈에 띄는 차이를 아래 표에 정리했습니다.

  ETH 잔액 조회 ERC20 잔액 조회
method eth_getBalance eth_call
params 지갑의 주소 'data'와 'to'를 Key로 갖는 json object

먼저 method가 달라졌습니다.

eth_getBalance는 네트워크 params로 전달한 지갑의 주소가 보유한 ETH 잔액을 요청하는 목적으로 사용됩니다. 반면, eth_call은 스마트 컨트랙트 함수 호출을 요청하는 목적으로 사용됩니다.

eth_call은 스마트 컨트랙트의 상태를 조회하기 위한 목적으로만 사용할 수 있습니다.

 

eth_call 요청과 함께 전달되는 params의 json object에는 두 개의 키 todata가 있습니다.

to는 호출할 스마트 컨트랙트의 주소며, data는 호출할 메서드와 매개변수가 인코딩 된 데이터입니다.

 

data의 인코딩 형식은 ABI 인코딩으로, 스마트 컨트랙트에 정의된 함수를 식별하기 위한 셀렉터(Selector)와 매개변수로 구성됩니다.

 

ABI 인코딩

ABI는 Application Binary Interface의 약자로, 애플리케이션을 구성하는 모듈 간의 호출 규약(Calling Convention)을 정의합니다.

스마트 컨트랙트에 정의된 함수를 호출하기 위해서는 함수를 식별하고, 매개변수를 전달하기 위한 일종의 호출 규약이 필요한데, ABI 인코딩은 스마트 컨트랙트가 인식할 수 있는 메시지의 형식입니다.

 

앞에서 살펴본 json object의 data 키의 값은 두 부분으로 나뉩니다.

  1. 함수 셀렉터 (Function Selector)
  2. 매개변수

함수 셀렉터

함수 셀렉터는 스마트 컨트랙트에 정의된 함수를 식별하기 위한 값입니다.

이 셀렉터는 4바이트로 구성되며, ABI를 이용해 만들어집니다.

 

앞의 ERC20 토큰 잔액을 조회하면서 살펴본 코드에 아래와 같이 ABI를 나타내는 object가 선언되어 있었습니다.

const ERC20_ABI = [
  {
    "constant":true,
    "inputs":[
      {
        "name":"who",
        "type":"address"
      }
    ],
    "name":"balanceOf",
    "outputs":[
      {
        "name":"",
        "type":"uint256"
      }
    ],
    "payable":false,
    "stateMutability":"view",
    "type":"function"
  }
];

이 값은 스마트 컨트랙트에 정의된 함수의 정보를 나타내는 리스트입니다.

여기 정의된 balanceOf 함수는 매개변수로 address 타입의 주소를 받아, uint256 타입의 잔액을 반환합니다.

실제 ERC20 ABI는 balanceOf 외에도 transfer, approval, approve 등 더 많은 함수가 정의되어 있지만, 코드가 너무 길어지기 때문에 예시로 사용할 balanceOf 함수만 정의했습니다.
ERC20 토큰 표준에 정의된 모든 함수는 EIP-20 문서를 참고하시기 바랍니다.

 

함수 셀렉터를 생성하는 데에는 함수의 이름과 매개 변수만 있으면 충분합니다.

매개변수를 공백 없이 쉼표로 구분하여 함수 시그니쳐 형태의 문자열을 만들어 keccak256 해시한 결과 중 MSB 4바이트를 취합니다.

 

예를 들어, 위에서 사용된 balanceOf 함수의 셀렉터를 생성하기 위해서는 문자열 balanceOf(address)을 해시합니다.

70a08231b98ef4ca268c9cc3f6b4590e4bfec28280db06bb5d45e689f2a360be

이 중, MSB(최상위 바이트) 4바이트 70a08231가 balanceOf(address)의 함수 셀렉터가 됩니다.

 

매개변수

스마트 컨트랙트 함수에 전달하기 위한 매개변수는 이더리움의 ABI 인코딩을 이용하여 표현해야 합니다.

ABI 인코딩의 형식은 타입에 따라 다르지만 공통적으로 32바이트로 패딩(padding)됩니다.

 

아래 표는 흔히 사용되는 타입의 ABI 인코딩 형식입니다.

Type Header Tail
address address -
uint uint -
boolean boolean -
string offset length, byte sequence
list offset item count, items

 

ABI 인코딩의 자세항 사양은 ABI Specification 문서를 참고하시기 바랍니다.

 

balanceOf 함수의 매개변수 address 타입은 20바이트로 표현되기 때문에, 32바이트가 되도록 zero-padding이 추가됩니다.

address 4976A4A02f38326660D17bf34b431dC6e2eb2327
abi-encoded address 0000000000000000000000004976A4A02f38326660D17bf34b431dC6e2eb2327

 

인코딩 결과

이렇게 만들어진 함수 셀렉터와 ABI 인코딩 된 매개변수를 이어 붙여 스마트 컨트랙트 호출에 필요한 data 필드의 값이 완성됩니다.

Selector 70a08231
Arguments 0000000000000000000000004976A4A02f38326660D17bf34b431dC6e2eb2327
0x70a082310000000000000000000000004976A4A02f38326660D17bf34b431dC6e2eb2327

 

 

사실 메타 마스크와 카이카스 지갑에서는 여러분들이 서명을 할 때, 아래 이미지와 같이 ABI 인코딩 된 데이터를 보여주고 있습니다.

즉, 이 중 셀렉터와 매개변수를 분리하고, 매개변수를 디코딩하면 어떤 함수에 어떤 매개변수가 전달되는지를 알아낼 수 있습니다.

 

반환 값 디코딩

balanceOf 함수가 잔액을 조회하는 함수기 때문에, 반환 값을 uint256 타입으로 반환합니다.

물론 이 값 역시 ABI 인코딩 된 채로 반환되기 때문에, 디코딩이 필요합니다.

 

아래 코드는 ABI 인코딩 된 uint256 타입의 값을 디코딩하는 예시입니다.

const abi = require('ethereumjs-abi');
const BALANCE = '0000000000000000000000000000000000000000000000000000000ba43b7400';

const data = Buffer.from(BALANCE, 'hex');
const balance = abi.rawDecode(['uint256'], data).toString();
console.log(balance);

 

트랜잭션

지금까지 확인한 내용은 eth_getBalance와 eth_call 호출이었습니다.

그중에서도 eth_call을 호출하며 전달되는 두 개의 필드 to와 data를 살펴보았고, data 필드가 어떻게 만들어지는지를 소개했습니다.

하지만 블록체인과 스마트 컨트랙트의 상태를 조회하기만 했을 뿐, 상태를 변경하기 위한 메시지를 아직 살펴보지 않았습니다.

 

상태를 변경하기 위해서는 반드시 트랜잭션을 생성해야 합니다.

이더리움의 트랜잭션은 크게 세 가지로 분류합니다.

  1. ETH를 송금하는 트랜잭션
  2. 스마트 컨트랙트를 실행하는 트랜잭션
  3. 스마트 컨트랙트를 배포하는 트랜잭션

이 중에서 ETH 송금 트랜잭션과 스마트 컨트랙트를 실행하는 트랜잭션에 대해 살펴봅시다.

 

EIP-2718이 적용된 베를린 하드 포크에서는 이더리움 트랜잭션에 타입이 추가되었습니다.
하지만, 내용의 간결성을 위해 EIP-155 이후의 레거시 트랜잭션만 다루도록 하겠습니다.

 

트랜잭션 구조

이더리움 트랜잭션은 아래와 같이 구성됩니다.

Field Description
nonce 트랜잭션이 네트워크에서 재실행되는 것을 방지하기 위한 필드입니다.
nonce는 0에서부터 시작하여 매 트랜잭션이 발생할 때마다 1씩 증가합니다.
gasPrice 트랜잭션이 실행될 때 발생하는 가스의 단가를 나타냅니다.
gasLimit 연산이 실행될 때 발생하는 가스 수량을 제한합니다.
gasLimit보다 더 많은 가스가 발생할 경우, 트랜잭션은 실패합니다.
to 트랜잭션을 수신하는 주소입니다.
지갑 주소 혹은 스마트 컨트랙트의 주소가 될 수 있습니다.
value 트랜잭션을 통해 전송할 ETH의 수량을 나타냅니다.
data 호출하려는 스마트 컨트랙트의 함수 및 매개변수 정보를 ABI 인코딩 형태로 담고 있습니다.
v 트랜잭션이 다른 네트워크에서 재실행되는 것을 방지하기 위한 필드입니다.
트랜잭션이 실행되는 체인의 ID를 담고 있습니다.
r, s 트랜잭션의 서명 정보를 담고 있는 필드입니다.
nonce 필드는 트랜잭션을 복제하여 재실행하는 것을 방지하기 위한 필드입니다.
nonce가 없을 경우, 네트워크에 전송한 트랜잭션은 유효한 서명을 갖고 있기 때문에 몇 번이든 복제하여 재실행할 수 있게 됩니다.
송금 트랜잭션이 복제되면 ETH가 의도치 않게 전송될 위험이 있습니다.
때문에, nonce는 해당 지갑 주소의 트랜잭션 발생 횟수를 담아, 0부터 순차적으로 증가해야만 합니다.
v 필드는 트랜잭션을 다른 네트워크에 복제하는 것을 방지하기 위한 필드입니다.
예를 들어, BSC(바이낸스 스마트 체인)은 이더리움과 동일한 주소 체계와 레거시 트랜잭션을 지원하는데, 악의적 공격자가 어떤 주소의 과거 BNB 전송 트랜잭션을 복제하여 ETH에 전송할 수 있습니다.
이를 방지하기 위해 v는 2 * chain ID + (35 or 36)의 값을 갖습니다.
이더리움 메인 넷의 체인 ID는 1, BSC는 65입니다.

 

트랜잭션에 필요한 모든 필드를 채운 후, 두 가지 방법으로 네트워크에 전송할 수 있습니다.

  1. 서명되지 않은 트랜잭션을 eth_sendTransaction 메시지를 통해 전송하기
  2. 서명된 트랜잭션을 eth_sendRawTransaction 메시지를 통해 전송하기'

서명되지 않은 트랜잭션을 전송하는 1번 방법은 개인 키를 관리하는 네트워크 노드를 별도로 준비해야만 합니다.

따라서 우리는 서명된 트랜잭션을 가정하고 eth_sendRawTransaction으로 전달하는 방법에 대해 알아보도록 합시다.

 

서명된 트랜잭션을 eth_sendRawTransaction 메시지를 통해 전달하기 위해서는 먼저 RLP 형식으로 인코딩해야 합니다.

서명이 관련한 내용은 글의 주제에서 벗어나기 때문에 제외하였습니다.

 

RLP 인코딩

RLP 인코딩은 중첩된 배열을 바이너리 형태로 변환하기 위한 목적으로 설계되었습니다.

트랜잭션의 모든 필드를 [nonce, gasPrice, gasLimit, to, value, data, v, r, s] 형태의 1차원 배열 형태로 구성하여 인코딩합니다.

 

RLP 인코딩 규칙은 아래와 같습니다.

  1. 0x00 - 0x7F 사이의 값은 값 그대로 표현합니다.
  2. 0x80 - 0xB7 사이의 값은 0x80을 뺀 길이(최대 55) 만큼의 연속된 바이트를 의미합니다.
  3. 0xB8 - 0xBF 사이의 값은 0x87을 뺀 만큼의 길이 정보를 갖는 연속된 바이트를 의미합니다
    • 0xB7을 뺀 값만큼의 바이트 사이즈가 Big-Endian 정수 형태로 이어집니다.
    • 바이트 사이즈만큼의 연속된 바이트가 이어집니다.
  4. 0xC0 - 0xF7 사이의 값은 0xC0을 뺀 길이(최대 55)만큼의 사이즈를 갖는 배열을 의미합니다.
  5. 0xF8 - 0xFF 사이의 값은 0xF7을 뺀 만큼의 길이 정보를 갖는 배열을 의미합니다.
    • 0xF7을 뺀 값만큼의 바이트 사이즈가 Big-Endian 정수 형태로 이어집니다.
    • 바이트 사이즈만큼의 연속된 바이트가 이어집니다.
위 규칙에서 주의할 점은 배열의 크기는 배열 요소의 개수가 아니라, 배열 내 모든 데이터의 바이트 사이즈를 나타낸다는 것입니다.

RLP 인코딩의 이해를 돕기 위해 과거의 한 트랜잭션을 직접 디코딩해보았습니다.

이 트랜잭션 ID는 후오비의 한 지갑에서 다른 지갑으로 400 ETH를 전송한 내역입니다.

해당 트랜잭션 ID의 Raw 트랜잭션은 아래와 같습니다.

0x0000 | f8708301e8be851c2c297a0082520894fd54078badd5653571726c3370afb127
0x0020 | 351a6f268915af1d78b58c4000008025a05085a24dd51d1f88026a18a31f19c3
0x0040 | cfa6779a608330a1fbb23d790382b20203a0159f9a134823ef94353b96cd9281
0x0060 | 35eb75f2f83770c9b1b282850ea7e6c1ae0c​

 

트랜잭션 규칙을 통해 디코딩하면 아래와 같은 결과를 확인할 수 있습니다.

Field Type Length Value Description
- F8 112 (0x70) - array (112 bytes)
nonce 83 3 (0x83-0x80) 0x01E8BE 125118
gasPrice 85 5 (0x85 - 0x80) 0x1C2C297A00 121 Gwei
gasLimit 82 2 (0x82 - 0x80) 0x5208 21,000
to 94 20 (0x94 - 0x80) 0xfd54078badd5653571726c3370afb127351a6f26  
value 89 9 (0x89 - 0x80) 0x15AF1D78B58C400000 400 ETH
data 80 0 (0x80 - 0x80) - -
v 25 - 0x25 (chain id * 2 + 35)
=> chain id = 1
r A0 32 (0xA0 - 0x80) 0x5085a24dd51d1f88026a18a31f19c3cfa6779a608330a1fbb23d790382b20203  
s A0 32 (0xA0 - 0x80) 159f9a134823ef94353b96cd928135eb75f2f83770c9b1b282850ea7e6c1ae0c  

 

ERC20 토큰 transfer 호출 트랜잭션

그렇다면 직접 트랜잭션 정보를 생성하여 RLP 인코딩을 해보면 어떨까요?

간단하게 이더리움 블록체인의 ERC20 토큰 중 하나인 USDT의 transfer 함수를 호출하는 트랜잭션을 만들어 봅시다.

 

먼저, 가장 간단한 정보부터 채워봅시다.

Field Type Length Value Description
nonce 81 1 (0x81-0x80) 0x00 0
gasPrice 85 5 (0x85 - 0x80) 0x174876e800 100 Gwei
gasLimit 82 2 (0x82 - 0x80) 0x4E20 20,000
to 94 20 (0x94 - 0x80) 0xdAC17F958D2ee523a2206206994597C13D831ec7 USDT
value 80 0 (0x80 - 0x80) 0x 0 ETH

이더리움 네트워크 상 USDT 토큰의 주소는 0xdAC17F958D2ee523a2206206994597C13D831ec7입니다.

그리고 ETH 전송이 아닌 ERC20 토큰을 전송하려는 목적이기 때문에 value는 0으로 지정합니다.

나머지 nonce, gasPrice, gasLimit은 임의로 지정했습니다.

 

transfer 함수의 시그니처는 transfer(address,uint256)이며, 함수 셀렉터는 a9059cbb입니다.

토큰을 받을 주소는 null주소로 지정하고, 1USDT(decimals: 6)만 전송하는 호출 정보는 아래와 같습니다.

selector: 0xa9059cbb (transfer)
args[0]:  0x0000000000000000000000000000000000000000000000000000000000000000
args[1]:  0x00000000000000000000000000000000000000000000000000000000000f4240

 

실제로 null 주소로 지정하여 네트워크로 전송하지 않도록 주의하시기 바랍니다.

 

이더리움 메인 넷(chain id: 1)에서 USDT 토큰을 전송하기 때문에, v 값은 chain id * 2 + (35 or 36)인 37 또는 38로 지정합니다.

 

Field Type Length Value Description
nonce 81 1 (0x81-0x80) 0x00 0
gasPrice 85 5 (0x85 - 0x80) 0x174876e800 100 Gwei
gasLimit 82 2 (0x82 - 0x80) 0x4E20 20,000
to 94 20 (0x94 - 0x80) 0xdAC17F958D2ee523a2206206994597C13D831ec7 USDT
value 80 0 (0x80 - 0x80) 0x 0 ETH
data B8 68 (0x44) 0xa9059cbb000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f4240 to: null
amount: 1,000,000
v 25 - 0x25 Ethereum Mainnet
r A0 32 (0xA0 - 0x80) 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff -
s A0 32 (0xA0 - 0x80) 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff -

임의의 서명 값을 추가하여 트랜잭션을 완성했습니다.

 

위 트랜잭션을 RLP 인코딩하면 아래와 같이 표현됩니다.

0x0000 | f8a90085174876e800824e2094dac17f958d2ee523a2206206994597c13d831e
0x0020 | c780b844a9059cbb000000000000000000000000000000000000000000000000
0x0040 | 0000000000000000000000000000000000000000000000000000000000000000
0x0060 | 00000000000f424025a0ffffffffffffffffffffffffffffffffffffffffffff
0x0080 | ffffffffffffffffffffa0ffffffffffffffffffffffffffffffffffffffffff
0x00A0 | ffffffffffffffffffffff

RLP 인코딩 된 데이터를 네트워크 노트로 전송하기 위한 json rpc 메시지는 아래와 같습니다.

{
    "jsonrpc":"2.0",
    "id":3,
    "method":"eth_sendRawTransaction",
    "params":[
        "f8a90085174876e800824e2094dac17f958d2ee523a2206206994597c13d831ec780b844a9059cbb000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f424025a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
    ]
}

 

스마트 컨트랙트 함수 호출

스마트 컨트랙트를 호출하는 요청과 트랜잭션은 어떻게 함수를 식별하는 것일까요?

이 궁금점을 풀고자, 이더리움 스마트 컨트랙트를 직접 리버스 엔지니어링 해봤습니다.

스마트 컨트랙트 리버스 엔지니어링에 대해서 깊게 다루고 싶지만, 내용이 방대하기 때문에 메시지 식별에 관해서만 소개하겠습니다.

 

스마트 컨트랙트가 호출되면 함수 셀렉터를 비교하여 함수의 루틴으로 점프하는 점프 테이블을 발견할 수 있습니다.

점프 테이블은 다른 스마트 컨트랙트에서도 비슷한 패턴으로 나타나는데, 대개 아래와 같은 패턴입니다.

  1. 메시지에 포함된 함수 셀렉터를 스택에 Push
  2. 스택 최상단의 함수 셀렉터를 복제
  3. 점프 테이블 엔트리의 함수 셀렉터와 비교
  4. 점프할 코드 offset을 스택에 Push
  5. 분기문 실행
    1. 3의 비교 결과가 true인 경우, 4에서 추가한 offset으로 분기
    2. 3의 비교 결과가 false인 경우, 다음 점프 테이블 엔트리에서 2번부터 반복

 

오늘 살펴봤던 두 함수 balanceOftransfer 함수의 점프 테이블 엔트리도 찾아볼 수 있었습니다.

balanceOf(address)
transfer(address,uint256)

점프 테이블의 끝에 도달했을 때는 abort 되어 실패로 끝나거나, 다른 스마트 컨트랙트로 delegate call을 호출하는 fallback 루틴이 존재하기도 합니다.

스마트 컨트랙트를 더 이상 깊게 분석하는 것은 더 많은 지식을 요구하며, 내용이 상당히 많기 때문에 여기까지만 소개하겠습니다.
기회가 된다면 스마트 컨트랙트 리버스 엔지니어링을 주제로 다뤄 보겠습니다.

 

마치며

스마트 컨트랙트에 대한 열기는 이전에도 많았지만, 최근 들어 NFT와 DeFi가 부상하면서 더 많은 스마트 컨트랙트가 배포되고 있습니다.

때문에 여러 블록체인 네트워크에서 대체 가능한 토큰 및 대체 불가능한 토큰(NFT) 전송은 물론, 수많은 종류의 트랜잭션이 네트워크를 가로지르고 있습니다. 이러한 트랜잭션을 분석하기 위해서는 오늘 소개한 레거시 트랜잭션뿐만 아니라, 다양한 타입의 트랜잭션의 구조와 목적에 대한 이해가 필요합니다.

클레이튼, 바이낸스 스마트 체인과 같은 이더리움 계열의 블록체인에서는 이러한 레거시 트랜잭션을 공통적으로 지원하고 있기 때문에, 오늘 소개한 내용을 이해했다면 해당 체인의 스마트 트랜잭션을 어렵지 않게 분석할 수 있을 것입니다.

 

트랜잭션 분석은 스마트 컨트랙트 분석과도 연관성이 깊습니다.

특히 스마트 컨트랙트의 코드를 공개하는 것이 의무사항이 아니며, 코드가 공개되지 않은 스마트 컨트랙트가 굉장히 많이 존재합니다.

때문에, 트랜잭션이 실패했을 때 실패의 이유를 이해하기 어려운 경우가 존재하는데, 트랜잭션을 디버깅하거나 스마트 컨트랙트를 리버스 엔지니어링 하는 복잡한 작업이 동반될 수도 있습니다.

 

안전한 디지털 자산 투자를 위해, 블록체인 네트워크를 가로지르는 트랜잭션을 꿰뚫어 볼 수 있는 안목을 가질 수 있기를 바라는 마음으로 오늘의 주제로 이더리움 트랜잭션의 구조를 선정하게 되었습니다.

 

긴 글을 끝까지 읽어 주셔서 감사합니다!

'Tech' 카테고리의 다른 글

[Tech] Pre-rendering 그리고 Data fetching  (0) 2022.09.21