이더리움의 트랜잭션 분석 — Part 2

Santony Choi
Santony’s Blog
Published in
8 min readMay 26, 2018

이전 시리즈:
이더리움의 트랜잭션 분석 — Part 1

지난 시간에는 이더리움에서 데이터를 인코딩하는 방식인 RLP를 살펴보았다. 말미에 RLP로 인코딩된 값 하나를 던지며 끝이 났는데, 다시 한 번 불러오자.

0xf87f81c98504a817c80083061a80941234567890123456789012345678901234567890880de0b6b3a764000091d08773616e746f6e7982697384636f6f6c29a0fd3263ef5b7550ec3b9ad789c8ae5930c78f55b2e4ac1984e22bf276cf4ba4e7a0399b9daad024031975346d195043e49e3a3a1ebbec494502978b0aa86a00bae7

결론부터 말하면, 위의 값은 아래의 평문 트랜잭션을 개인키 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef 으로 사인(Sign)한 후, RLP 인코딩한 결과 값이다.

nonce: ‘0xc9’,
gasPrice: ‘0x4a817c800’,
gasLimit: ‘0x61a80’,
to: ‘0x1234567890123456789012345678901234567890’,
value: ‘0xde0b6b3a7640000’,
data: ‘0xd08773616e746f6e7982697384636f6f6c’,
chainId: 3

이제 우리는 RLP 인코딩을 알고 있기 때문에 직접 값을 디코딩하고 해석하는 것은 독자 여러분께 맡기겠다. 물론 각 언어별로 RLP를 인코딩 / 디코딩하는 편리한 라이브러리가 존재하지만 학습을 위해 한 번쯤 손으로 디코딩해보는 것도 좋다.

다시 오늘의 주제로 돌아가 위의 트랜잭션이 어떤 정보를 담고 있는지 순서대로 살펴보자.

nonce

가장 위에 위치한 nonce는 “이 트랜잭션이 트랜잭션을 생성하는 어카운트 내에서 몇 번째로 발생하는 트랜잭션인지”를 나타내는 16진수 값이다. 이더리움은 이중 지불과 같은 악의적인 공격이나 트랜잭션 순서가 뒤섞이는 것을 방지하기 위해 nonce 값을 활용한다. 201번째인 위 트랜잭션이 성공적으로 수행된 뒤에는 반드시 nonce 값 0xca(202)를 가진 트랜잭션이 발생해야 하며 그 외의 경우에는 client가 잘못된 트랜잭션으로 판단해 네트워크에 반영하지 않는다.

gasPrice & gasLimit

가스는 이더리움에서 비용을 책정하는 방법이며 블록사이즈를 제한하는 방법이기도 하다. 이번주 미디엄에서 뜨거웠던 이더리움의 블록체인 사이즈에 관한 논쟁에서 비탈릭 뷰테린은 가스가 블록 사이즈를 제한하는 역할을 한다고 언급했다. 왜 그런지 한 번 보자.

가스는 EVM에서 실행되는 처리에 할당되는 비용이다. 각 OPCODE가 얼마의 가스를 소모하는지는 이더리움 옐로 페이퍼(일반 대중을 대상으로 하는 화이트페이퍼와 달리 기술적인 부분을 상세히 기술한 명세서)에 표로 자세히 나와 있다. ADD, SUB과 같이 정적으로 값이 정해지는 OPCODE도 있는 반면 SSTORE(값을 Stroage에 저장하는 OPCODE)처럼 런타임에 동적으로 가스 값을 정하는 OPCODE도 있다. 옐로 페이퍼의 내용에 대해서는 기회가 될 때 보다 상세히 설명하겠다.

그렇다면 Price는 무엇이고 Limit은 무엇일까? Price는 단순하다. 옐로 페이퍼에서 OPCODE가 소비하는 가스의 양은 이더리움의 화폐 단위인 Wei 단위가 아니라 그저 상수로 정해져있다. Price는 바로 이 상수 단위 소모량에 곱하는 계수로서 동일한 100 가스를 소모하는 트랜잭션이라 하더라도 Price를 10으로 설정하면 1로 설정한 경우에 비해 10배의 Wei가 소모된다.

Limit은 이 트랜잭션이 최대로 소모할 것으로 예상하는 가스 값이다. 이 값은 트랜잭션을 생성하는 사용자가 임의로 정하도록 되어있는데, 일반적인 경우에는 사용자가 숫자를 입력하기보다 트랜잭션의 내용을 Geth, Parity 등의 클라이언트 혹은 언어별 라이브러리를 활용해 계산하는 것을 권장한다.

만약 Limit을 넘어가거나 Limit보다 적은 양의 gas만 소모한 경우에는 어떻게 처리될까? 전자의 경우에는 사용자의 Ether는 모두 소모된 채로 트랜잭션이 실패하고, 후자의 경우에는 사용한 만큼의 Ether만 지불하고 트랜잭션이 정상 종료한다.

지금까지의 설명을 정리해보면, 한 트랜잭션이 최대로 소모하는 Ether의 양(가스의 양이 아니라)은 곧, (gasPrice * gasLimit) Wei 로 계산할 수 있다. 실제로 소모된 값은 (gasPrice * 실제 소모된 gas의 양) Wei로 계산된다.

우리의 예시에서 gasPrice는 20 GWei, gasLimit 400,000 이므로 최대 소모 가능한 값은 8,000,000 Gwei, 즉, 0.008 Ether 가 된다.

to

트랜잭션의 목적지이다. Ether나 메시지를 전송하기 위해 외부 계정(External Account)이 목적지가 될 수도 있고 컨트랙트의 함수를 호출하기 위해 컨트랙트 계정(Contract Account)의 주소를 목적지로 둘 수도 있다.

value

16진수로 표현한 Wei 단위의 Ether 전송량이다. 1 Ether를 전송하기 위해서는 10진수로 1x10¹⁸, 즉, 0xde0b6b3a7640000 를 담으면 된다. 예시에서도 1 Ether를 전송하고 있다.

앞서 Wei라는 단위를 쓰고 있는데, 이는 비트코인의 Satoshi와 마찬가지로 ETH를 쪼갤 수 있는 최소 단위이다. Satoshi가 BTC를 1억분의 1로 나눈 것처럼, Wei는 ETH를 10¹⁸분의 1로 나눈 값이다. Wei의 10⁶배인 Mwei, 10⁹배인 Gwei 등이 흔히 쓰이는 변환 단위이다. 단위를 즉시 변환해볼 수 있는 사이트도 많이 있으니 직접 테스트해보길 바란다.

data

이더리움은 트랜잭션에 사용자가 임의의 값을 담아 보낼 수 있다. 최근 중국 북경대의 한 학생이 정부의 검열에 저항하기 위해 트랜잭션에 UTF8로 인코딩된 글을 올렸는데, 이 때 글을 담은 필드가 바로 data이다. 해당 트랜잭션은 송신자와 수신자가 같다. 즉, 수신자가 외부 계정이란 뜻인데, 위의 경우처럼 특수한 목적을 가진 경우 외에는 외부 계정으로 트랜잭션을 발송하는 경우에 data 필드 값이 특별한 역할을 가지지는 않는다.

컨트랙트 계정으로 트랜잭션을 보내는 경우에는 보다 중요한 역할을 수행할 수 있다. 바로 컨트랙트 함수의 실행이다. data 필드에 ABI라는 방식으로 인코딩된 값을 담아 보내면 수신하는 측의 컨트랙트가 이를 함수명과 인자로 디코딩하여 미리 프로그램된 기능을 실행한다. ABI 인코딩에 대해서는 다음 포스팅에서 설명하겠다.

필자는 이 data 필드가 바로 비트코인과 이더리움을 결정적으로 분리하는 요인이라 본다. 비트코인에도 트랜잭션에 메시지를 담아 보내려는 시도들이 있었고, 라이트닝 프로토콜 등이 여기서 발전했으나 비트코인 코어 개발자들은 이 기능을 핵심이라 생각하지 않았고 코어가 아닌 하위 레이어 등을 응용해서 처리하는 방향으로 발전했다. 반면, 이더리움 개발자들은 적극적으로 이 기능을 지원하는 프로토콜을 만들고 싶어했다. 그 결과 이더리움이라는, 튜링 완전한 컴퓨팅 언어를 지원하는 프로토콜이 등장하게 된 것이다.

ChainId

마지막 필드는 하드포크와 관련된 필드이다. EIP-155 에서 여기에 관련된 상세한 배경을 알 수 있는데, Spurious Dragon 하드포크 과정에서 리플레이 공격을 막기 위한 수단으로 이 필드를 도입했다고 설명하고 있다.

사실 이더리움 메인 체인을 사용하는 우리 입장에서는 그저 사용만 할 수 있으면 된다. 이더리움 메인넷은 이 필드의 값으로 1을, 테스트넷인 Ropsten은 3을 사용하고 있다는 것정도만 알아두자. 그 외에 Backward compatibility를 위해 legacy 방식인 ChainId 0 도 지원하고 있으며 이 값은 체인의 종류와 관계없이 수행된다.

그런데 이 ChainId 값은 위의 RawTransaction에서 찾을 수가 없다. 어찌된 일일까?

나머지 값

위의 RawTransaction을 직접 디코딩해본 독자라면 여기까지의 설명이 부족하다는 것을 알 수 있을 것이다. 총 129 바이트의 값 중, 상위 62 바이트는 위에서 설명한 필드들의 값을 차례로 인코딩한 값으로 우리가 손으로 풀어볼 수 있었다. 하지만 ChainId의 값은 여기 포함되어 있지 않고 나머지 67바이트는 총 세 개의 아이템을 담고 있는데 값을 쉽게 유추할 수 없다.

이 값들은 각각 v, r, s라는 필드로 표현되는 값으로 트랜잭션을 개인키로 사인(Sign)하여 이 값이 주소의 소유자로부터 생성된 내용이라는 것을 보증해주는 역할을 한다. 여기에 대해서는 이더리움이 사용하고 있는 ECDSA 알고리즘과 secp256k1 커브에 대해 알아야 정확히 이해할 수 있다. 구글에 관련된 설명이 충분히 있으니 보다 심화된 이해를 원하는 독자라면 해당 키워드로 구글링하길 추천한다.

우리가 찾지 못했던 ChainId의 값도 v 값에서 유추할 수 있다. 우리의 트랜잭션에서 v 값은 0x29, 즉 10진수 41이다. ChainId가 3인 Ropsten용 트랜잭션에서 v 값은 4142중 하나로 결정된다. 따라서 41을 역산하면 우리가 ChainId 3으로 트랜잭션을 보낸다는 것을 알 수 있다.

중간 결산

우리는 이제 16진수로 표현된 암호같은(실제로도 암호를 포함한) 값을 읽을 수 있게 되었다. 물론 우리가 계산기를 써가며 이 트랜잭션을 만들 일은 없을 것이다. 이미 이더리움은 유명한 프로젝트가 되었고 흔히 사용되는 대부분의 언어에서 이런 일을 대신해주는 라이브러리가 있기 때문이다. 필자가 요즘 사용하고 있는, 아마도 많은 독자들에게 생소할 언어인, Elixir에도 이런 역할을 하는 라이브러리가 복수개 존재할 정도이니 말이다.

다음 포스팅은 이 시리즈의 마지막 주제인 ABI 디코딩에 대해 다뤄보겠다.

--

--