Mastering Ethereum 6

개발새발·2021년 11월 29일
0

Mastering Ethereum

목록 보기
6/7
post-thumbnail

Chapter 6. 트랜잭션

본 글은 『Mastering Ethereum』을 읽고 정리한 내용입니다.

트랜잭션은 외부 소유 계정(EOA)에 의해 서명된 메시지인데, 이더리움 네트워크에 의해 전송되고 이더리움 블록체인에 기록된다. 이 기본 정의는 놀라울 정도로 매력적인 의미를 담고 있다. 다른 관점에서 바라보면, 트랜잭션은 EVM에서 상태 변경을 유발하거나 컨트랙트를 실행할 수 있는 유일한 방법이라는 것이다. 이더리움은 글로벌 싱글톤 상태 머신이며, 트랜잭션은 이 상태 머신을 움직여서 상태를 변경할 수 있도록 만든다. 컨트랙트는 독자적으로 실행되지 않는다. 또한 이더리움도 자율적으로 실행되지 않는다. 모든 것은 트랜잭션으로부터 시작된다.

1. 트랜잭션 구조

먼저 이더리움 네트워크에서 시리얼라이즈(serialize)되어 전송되는 트랜잭션의 기본 구조를 살펴보자. 시리얼라이즈된 트랜잭션을 수신하는 각 클라이언트와 애플리케이션은 자체 내부 데이터 구조를 사용하여 트랜잭션을 메모리에 저장하며, 네트워크에서 시리얼라이즈된 트랜잭션 자체에는 존재하지 않는 메타데이터가 포함되어 있다. 네트워크 시리얼라이제이션은 트랜잭션의 유일한 표준 형식이다.

트랜잭션은 다음 데이터를 포함하는 시리얼라이즈된 바이너리 메시지다.

논스(nonce)

  • 발신 EOA에 의해 발행되어 메시지 재사용을 방지하는 데 사용되는 일련번호

가스 가격(gas price)

  • 발신자가 지급하는 가스의 가격(웨이)

가스 한도(gas limit)

  • 이 트랜잭션을 위해 구입할 가스의 최대량

수신자(recipient)

  • 목적지 이더리움 주소

값(value)

  • 목적지에 보낼 이더의 양

데이터(data)

  • 가변 길이 바이너리 데이터 페이로드

v, r, s

  • EOA의 ECDSA 디지털 서명의 세 가지 구성요소

트랜잭션 메시지의 구조는 이더리움에서 간단하고 완벽한 바이트 시리얼라이제이션을 위해 특별히 만들어진 RLP(Recursive Length Prefix) 인코딩 체계를 사용하여 시리얼라이즈된다. 이더리움의 모든 숫자는 8비트 배수 길이의 빅엔디안 정수로 인코딩된다.

명확성을 위해 여기에 필드 라벨(수신자, 가스 한도 등)이 표시되지만, 이 필드 값들은 RLP로 인코딩된 필드 값이 들어 있는, 시리얼라이즈된 트랜잭션 데이터의 일부가 아니라는 점을 유의하자. 일반적으로 RLP는 필드 구분자 또는 라벨을 포함하지 않는다. RLP의 길이 접두어는 각 필드의 길이를 식별하는 데 사용된다. 따라서 정의된 길이를 초과하는 것은 구조상 다음 필드에 속한다.

이것이 실제 전송되는 트랜잭션 구조인데, 내부 정보를 보여주거나 사용자 인터페이스를 시각화하기 위해서는 트랜잭션 구조 이외에도 트랜잭션이나 블록체인에서 파생된 추가 정보를 사용한다.

예를 들어 발신자 EOA를 식별하는 주소에 '발신자(from)' 데이터가 없는데, 이것은 EOA의 공개키를 ECDSA 서명의 v, r, s 구성요소로부터 알아낼 수 있으며, 이는 공개키를 통해 주소를 알아낼 수 있음을 의미한다. 즉, 주소는 공개키에서 파생될 수 있다. '발신자' 필드가 표시된 트랜잭션이라면, 시각화하는 데 사용된 소프트웨어에 의해 추가된 것이다. 클라이언트 소프트웨어에 의해 트랜잭션에 자주 추가되는 다른 메타데이터는 블록 번호(채굴되고 블록체인에 포함된)와 트랜잭션 ID(계산된 해시)를 포함한다. 다시 말하면, 이 데이터는 트랜잭션에서 파생되며 트랜잭션 메시지 자체의 일부가 아니다.

2. 트랜잭션 논스

논스(nonce)는 트랜잭션에서 가장 중요하고 이해하기 어려운 구성요소 중 하나다. 황서에서는 다음과 같이 정의한다.

논스 : 해당 주소에서 보낸 트랜잭션 건수 또는 연결된 코드가 있는 계정의 경우 이 계정에서 만든 컨트랙트 생성 건수와 동일한 스칼라 값

엄밀히 말하면, 논스는 발신 주소의 속성이며 단지 발신 주소의 컨트랙트 안에서만 의미를 갖는다. 그러나 논스는 명시적으로 블록체인 계정 상태에 저장되지 않고, 해당 주소에서 발생한 확인된 트랜잭션 건수를 세어서 동적으로 계산되는 값이다.

아래의 두 가지 상황을 통해서 트랜잭션을 세는 논스의 중요성에 대해 알아보자. 하나는 트랜잭션 생성 순서대로 포함된다는 점에서 생기는 사용성상의 기능(usability feature)이며, 다른 하나는 트랜잭션 복제 방지라는 주요 기능에서의 측면이다. 이들 각각에 대한 예제 시나리오를 살펴보자.

  1. 두 가지 트랜잭션을 한다고 생각해보자. 하나는 6개의 이더를 지급해야 하는 중요한 트랜잭션이고, 또 하나는 8개의 이더를 지급해야 하는 트랜잭션이다. 먼저 6이더 트랜잭션에 서명하고 전파하는 것이 더 중요하기 때문에 이 트랜잭션을 먼저 한 뒤, 두 번째 8이 더 트랜잭션에 서명하고 전파한다. 안타깝게도 계정에 이더가 10개밖에 없다는 사실을 간과했으므로, 네트워크에서 트랜잭션을 둘 다 받을 수 없어서 그중 하나는 실패한다. 더 중요한 6이더 트랜잭션을 먼저 보냈기 때문에 여러분은 당연히 앞선 트랜잭션은 성공하고 8이더 트랜잭션은 거부될 것으로 생각한다. 하지만 이더리움처럼 탈중앙화된 시스템에서 노드는 어떤 순서로든 트랜잭션을 수신할 수 있다. 특정 노드가 다른 노드보다 특정한 트랜잭션을 먼저 받을 것이라는 보장은 없다. 따라서 어떤 노드는 6이더 트랜잭션을 먼저 수신하고, 또 어떤 노드는 8이더 트랜잭션을 먼저 수신한다. 논스가 없다면 어느 것이 받아들여지고 어떤 것이 거부될지는 알 수 없다. 그러나 논스가 포함된 상태에서 보낸 첫 번째 트랜잭션은 논스 값이 예를 들어서 3이라고 하자. 8이더 트랜잭션은 다음 논스 값(즉, 4)을 갖는다. 그러면 0부터 3까지의 논스가 있는 트랜잭션이 처리될 때까지 해당 트랜잭션은 무시된다(8이더 트랜잭션이 먼저 수신되더라도).
  2. 이제 여러분의 계정에 100이더가 있다고 가정해보자. 여러분이 정말로 사고싶어 하는 mcguffin-widget에 대한 지급을 이더로 받을 판매자를 찾는다. 그들에게 2이더를 보내면 그들은 mcguffin-widget을 보낸다. 2이더를 지급하기 위해, 여러분의 계정에서 그들의 계정으로 2이더를 전송하는 트랜잭션에 서명한 다음, 트랜잭션을 이더리움 네트워크로 전파하여 검증한 후에 블록체인에 저장한다. 이제 논스 값 없이 2이더를 동일한 주소로 보내는 두 번째 트랜잭션은 첫 번째 트랜잭션과 정확히 동일하게 보인다. 즉, 이더리움 네트워크에서 트랜잭션을 보는 사람(수신자 또는 적을 포함하여 모든 사람을 의미함)은 원래 트랜잭션을 복사하여 붙여넣고 네트워크로 다시 보내는 방식으로 여러분의 이더가 소진될 때까지 계속 트랜잭션을 반복해서 '재실행(reply)'할 수 있다. 그러나 트랜잭션 데이터에 포함된 논스 값을 사용하면 동일한 수신자 주소에 동일한 양의 이더를 여러 번 보내는 경우에도 각각의 개별 트랜잭션은 고유하다. 따라서 트랜잭션의 일부로 논스를 증가시킴으로써 누군가가 지급한 금액을 '복제(duplicate)'하는 행위를 막을 수 있다.

요약하면, 비트코인 프로토콜의 '미사용 트랜잭션 아웃풋(Unspent Transaction Output, UTXO)' 메커니즘과 달리, 계정 기반(account-based) 프로토콜은 논스를 사용하는 것이 실제로 필수적임을 유의해야 한다.

2-1. 논스 추적

실질적으로 논스는 각 계정에서 발생한 확인된(즉, 체인상의) 트랜잭션 건수에 대한 최신 통계다. 논스 값을 알아내려면, 예를 들어 web3 인터페이스를 통해 블록체인을 조회해야 한다. 메타마스크를 실행 중인 상태에서 브라우저에게 자바스크립트 콘솔을 열거나 truffle console 명령을 사용하여 자바스크립트 web3 라이브러리에 접근한 후 다음을 입력하라.

> web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f")
40

논스는 0부터 시작하는 카운터로, 첫 번째 트랜잭션의 논스는 0이다. 이 예에서 트랜잭션 건수는 40으로, 0부터 39까지의 논스가 사용되었다. 다음 트랜잭션의 논스는 40이어야 한다.

여러분의 지갑은 그 지갑에서 관리하는 각 주소에 대한 논스를 추적한다. 하나의 지갑에서만 트랜잭션을 만드는 경우는 논스 추적이 매우 간단하다. 여러분이 자신의 지갑 소프트웨어 또는 트랜잭션을 발생시키는 어떤 애플리케이션을 작성한다고 하자. 논스를 어떻게 추적할 것인가?

새 트랜잭션을 만들 때 시퀀스상 다음 차례 논스 값을 부여한다. 그러나 이것이 컨펌될 때까지는 getTransactionCount 합계에 포함되지 않는다.

대기 중인 트랜잭션 건수 계산을 위해 getTransactionCount 함수를 사용할 때는 주의해야한다. 연속해서 몇 개의 트랜잭션을 보내는 경우 문제가 발생할 수 있기 때문이다.

예제를 살펴보자.

> web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", \
"pending")
40
> web3.eth.sendTransaction({from: web3.eth.accounts[0], to: \
"0xB0920c523d582040f2BCB1bD7FB1c7C1ECEbdB34", value: web3.utils.toWei(0.01, "ether")});
> web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", \
"pending")
41
> web3.eth.sendTransaction({from: web3.eth.accounts[0], to: \
"0xB0920c523d582040f2BCB1bD7FB1c7C1ECEbdB34", value: web3.utils.toWei(0.01, "ether")});
> web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", \
"pending")
41
> web3.eth.sendTransaction({from: web3.eth.accounts[0], to: \
"0xB0920c523d582040f2BCB1bD7FB1c7C1ECEbdB34", value: web3.utils.toWei(0.01, "ether")});
> web3.eth.getTransactionCount("0x9e713963a92c02317a681b9bb3065a8249de124f", \
"pending")
41

보다시피, 우리가 보낸 첫 번째 트랜잭션은 트랜잭션 건수를 41로 늘렸고 현재 상태는 보류 중이다. 그러나 우리가 연속적으로 세 번 더 트랜잭션을 보냈을 때 getTransactionCount 호출은 3개의 트랜잭션을 세지 않았다. 여러분은 멤풀(mempool)에 보류 중인 트랜잭션 3개가 있을 것이라고 생각하지만, 오직 하나의 트랜잭션만 대기 중이다. 네트워크 통신이 완료될 때까지 몇 초를 기다린 후에 getTransactionCount를 호출하면 이번에는 예상했던 숫자를 반환해 줄 것이다. 하지만 이것이 완료되기 전까지는 다수의 트랜잭션이 대기 중임에도 불구하고 이를 확인할 수 없다.

트랜잭션을 구성하는 애플리케이션을 만들 때 대기 중인 트랜잭션들을 확인하기 위해 getTransactionCount에 의존할 수 없다. 대기 중인 트랜잭션 개수와 확인된 트랜잭션 개수가 동일할 때만(미해결 트랜잭션이 모두 확인됨) getTransactionCount의 출력을 신뢰할 수 있고 논스 카운터를 시작할 수 있다. 그런 다음 각 트랜잭션이 확인될 때까지 애플리케이션의 논스를 추적하자.

패리티(Parity)의 JSON RPC 인터페이스는 트랜잭션에서 사용해야 하는 다음 논스를 반환하는 parity_nextNonce 함수를 제공한다. parity_nextNonce 함수는 각각의 트랜잭션을 확인하지 않고 빠르게 연속적으로 만들더라도 논스를 올바르게 계산한다.

$ curl --data '{"method":"parity_nextNonce", \
  "params":["0x9e713963a92c02317a681b9bb3065a8249de124f"],\
  "id":1,"jsonrpc":"2.0"}' -H "Content-Type: application/json" -X POST \
  localhost:8545

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

패리티에는 JSON RPC 인터페이스에 접근하기 위한 웹 콘솔이 있지만, 여기서는 커맨드 라인 HTTP 클라이언트를 사용하여 접근한다.

2-2. 논스의 간격, 중복 논스 확인

트랜잭션을 프로그램을 통해서 생성하는 경우, 특히 여러 독립 프로세스에서 동시에 트랜잭션을 생성하는 경우에는 논스를 추적하는 것이 중요하다.

이더리움 네트워크는 논스에 따라 트랜잭션을 순차적으로 처리한다. 즉, 논스가 0인 트랜잭션을 전송한 다음 논스가 2인 트랜잭션을 전송하면, 두 번째 트랜잭션은 어떤 블록에도 포함되지 않는다. 이더리움 네트워크가 누락된 논스가 나타날 때까지 기다리는 동안 두 번째 트랜잭션은 멤풀(mempool)에 저장된다. 모든 노드는 누락된 논스가 단순히 지연되었고 논스가 2인 트랜잭션은 순서가 맞지 않게 수신되었다고 가정한다.

그런 다음 논스가 1인 누락된 트랜잭션을 전송하면, 두 트랜잭션(논스 1과 2)이 처리되고 블록에 포함된다(물론 유효한 경우). 갭(gap)을 메우면 네트워크는 멤풀에서 보유한 순서가 잘못된 트랜잭션을 처리할 수 있다.

즉, 여러 트랜잭션을 순서대로 생성하고 그중 하나가 공식적으로 모든 블록에 포함되지 않으면 이후의 모든 트랜잭션이 '멈추고' 누락된 논스를 기다린다. 유효하지 않거나 가스가 모자란 트랜잭션은 논스 시퀀스에 의도치 않게 '갭'을 만들 수 있다. 다시 트랜잭션이 계속되게 하려면 누락된 논스가 있는 유효한 트랜잭션을 전송해야 한다. '누락'된 논스가 있는 트랜잭션이 네트워크에 의해 유효성이 검증되면, 이후의 논스가 있는 모든 브로드캐스트된 트랜잭션이 차례대로 유효해진다는 점도 똑같이 염두에 두어야 한다. 트랜잭션을 '회수(recall)'하는 것은 불가능하다.

반면, 예를 들어 논스가 같지만 수신자나 값이 다른 2개의 트랜잭션을 전송하는 것과 같은 논스의 중복이 일어나면, 그중 하나가 확정되고 하나는 거부된다. 어떤 트랜잭션이 확정되는지는 그 트랜잭션이 첫 유효에 도달하는 순서에 따라 결정된다. 즉, 이는 무작위적이다.

보다시피 논스를 추적하는 것은 필요하며, 애플리케이션이 해당 프로세스를 올바르게 관리하지 않으면 문제가 발생할 수 있다. 불행하게도, 이것을 동시에 수행하려는 경우 상황이 더 어려워진다.

2-3. 동시 실행, 트랜잭션 생성 및 논스

동시 실행은 컴퓨터 과학이 다루어야 하는 복잡한 문제이며, 때때로 예상치 못하게 발생할 수도 있는데, 특히 이더리움처럼 탈중앙화되어 있고 분산되어 있는 실시간 시스템에서는 더욱 그러하다.

간단히 말해, 동시 실행 문제는 여러 독립 시스템에 의한 동시적인 계산이 있는 경우다. 이들은 동일한 프로그램(예: 멀티스레딩), 동일한 CPU(예: 멀티프로세싱) 또는 다른 컴퓨터(즉, 분산 시스템)에 있을 수 있다. 이더리움은 정의상 작업(노드, 클라이언트, 댑)의 동시 실행을 허용하지만 합의를 통해 싱글톤 상태를 강제하는 시스템이다.

이제 동일한 주소에서 트랜잭션을 생성하는 여러 개의 독립적인 지갑 애플리케이션이 있다고 가정해 보자. 이러한 상황의 한 예가 거래소의 핫 월렛(hot wallet, 키가 온라인에 저장되지 않는 cold wallet과 달리 키가 온라인에 저장된 지갑)에서의 출금이다. 이상적으로는 병목 현상이나 단일 실패 지점이 생기지 않도록 2대 이상의 컴퓨터가 출금 작업을 처리하도록 하는 것이 좋다. 그러나 이것은 문제가 될 수 있다. 둘 이상의 컴퓨터에서 출금을 수행하면 동시 실행 문제가 발생할 수 있다. 물론, 논스를 선택하는 것도 중요한 문제다. 동일한 핫 월렛 계정에서 여러 컴퓨터가 트랜잭션을 생성, 서명 및 브로드캐스트하는 방식은 어떻게 조율될 수 있을까?

단일 컴퓨터를 사용하여 트랜잭션에 서명하는 컴퓨터에 선착순으로 논스를 할당할 수 있다. 그러나 이 컴퓨터는 이제 단일 실패 지점이다. 더욱이, 여러 논스가 할당되고 그중 하나가 사용되지 않으면(해당 논스로 트랜잭션을 처리하는 컴퓨터 오류로 인해) 모든 후속 트랜잭션이 중단된다.

또 다른 방법은 트랜잭션을 생성하고 논스를 할당하지 않는 것이다(그러면 트랜잭션은 서명되지 않은 상태로 남는다. 논스는 트랜잭션 데이터의 필수 부분이므로 트랜잭션을 인증하는 디지털 서명에 포함시켜야 한다). 그런 다음, 이 서명되지 않은 트랜잭션들을 한 노드의 대기열에 올려서 이 노드가 트랜잭션을 서명하고 논스를 관리할 수 있게 하는 것이다. 물론, 이것이 프로세스상 병목 지점이 될 수는 있다. 서명하고 논스를 관리하는 작업은 시스템 부하가 늘어남에 따라 혼잡해질 수 있지만, 서명되지 않은 트랜잭션들을 생성하는 작업은 병렬 처리 문제를 고민하지 않아도 된다. 여전히 동시 실행 문제가 남아 있긴 하지만, 크리티컬한 프로세스 부분에서는 더 이상 존재하지 않게 된다.

결국, 이러한 동시 실행 문제는 각각의 독립적인 프로세스들이 어카운트 밸런스를 추적하고 트랜잭션을 컨펌해야 하는 어려움이 가중된다. 따라서 대부분의 구현 솔루션들이 동시 실행을 피하고 거래소에서 출금 트랜잭션을 처리하는 단일 프로세스를 만드는 것처럼 병목 지점을 어쩔 수 없이 받아들이거나, 독립적으로 작동하는 다수의 출금 담당 핫 월렛을 설치하고 중간중간에 각 지갑의 밸런스를 다시 채워주는 형식으로 해결하게끔 만든다.

3. 트랜잭션 가스

가스는 이더리움의 연료다. 가스는 이더가 아니라 이더에 대한 자체 환율을 가진 별도의 가상화폐다. 이더리움은 가스를 사용하여 트랜잭션이 사용할 수 있는 자원의 양을 제어한다. 이는 전 세계 수천 대의 컴퓨터에서 처리되기 때문이다. 개방형(튜링 완전) 계산 모델은 DoS(Denial-of-Servie) 공격이나 실수로 막대한 자원을 소모하는 트랜잭션을 피하기 위해 특정한 형태의 미터링(metering)이 필요하다.

가스는 이더 가치의 급격한 변화와 함께 발생할 수 있는 변동성으로부터 시스템을 보호하고, 가스가 지급하는 다양한 자원(즉, 계산, 메모리 및 저장)의 비용 사이의 중요하고 민감한 비율을 관리하기 위해 가스를 이더와 분리한다.

트랜잭션의 gasPrice 필드는 트랜잭션 생성자가 가스와 교환하여 지급할 가격을 설정할 수 있게 한다. 가격은 가스 단위당 웨이 단위로 측정된다. 예를 들어, 2장의 샘플 트랜잭션에서 지갑은 gasPrice를 3Gwei(3기가웨이 또는 300억 웨이)로 설정한다.

인기 있는 사이트 ETH 가스 충전소는 이더리움 메인 네트워크의 가스 및 기타 관련된 가스 측정 항목의 현재 가격에 대한 정보를 제공한다.

지갑은 신속한 트랜잭션 컨펌을 위해 gasPrice를 조정할 수 있다. gasPrice가 높을수록 트랜잭션이 더 빨리 컨펌될 것이다. 반대로, 우선순위가 낮은 트랜잭션은 낮은 가격을 설정해서 컨펌이 느려지게 할 수 있다. gasPrice가 설정될 수 있는 최솟값은 0이고, 이것은 수수료 없는 트랜잭션을 의미한다. 블록 공간에 대한 수요가 낮은 기간에는 수수료가 0인 트랜잭션들도 블록에 포함될 수 있다.

최소 허용 gasPrice는 0이다. 이는 지갑이 완전히 무료 트랜잭션을 생성할 수 있음을 의미한다. 용량에 따라 이들은 영원히 컨펌되지 않을 수도 있지만, 프로토콜에 무료 트랜잭션을 금지하는 것은 없다. 이더리움 블록체인에 성공적으로 포함된 이와 같은 트랜잭션 사례들을 찾을 수 있다.

web3 인터페이스는 여러 블록에 걸친 중간 가격을 계산하여 gasPrice를 제안하는 기능을 제공한다(트러플 콘솔 또는 자바스크립트 web3 콘솔을 사용하여 이를 수행할 수 있다).

> web3.eth.getGasPrice(console.log)
> null BigNumber { s: 1, e: 10, c: [ 10000000000 ] }

가스와 관련된 두 번째 중요한 필드는 gasLimit이다. 간단히 말하면, gasLimit은 트랜잭션을 완료하기 위해 트랜잭션을 시도하는 사람이 기꺼이 구매할 수 있는 최대 가스 단위 수를 제공한다. 단순 지급의 경우, 하나의 EOA에서 다른 EOA로 이더를 전송하는 트랜잭션을 의미하며, 필요한 가스양은 21,000개의 가스 단위로 고정된다. 얼마나 많은 양의 이더가 소비되는지 계산하려면, 지급하고자 하는 gasPrice에 21,000을 곱하라. 예를 들면 다음과 같다.

> web3.eth.getGasPrice(function(err, res) {console.log(res*21000)} )
> 210000000000000

트랜잭션 목적지 주소가 컨트랙트인 경우, 필요한 가스양을 추정할 수는 있지만 정확하게 결정할 수는 없다. 이는 컨트랙트가 각기 다른 경로로 이어지는 조건을 가질 수 있어 총 가스 비용이 다를 수 있기 때문이다. 컨트랙트는 통제할 수 없거나 예측할 수 없는 조건들에 의해 단순한 계산으로 끝날 수도 있고, 더 복잡한 계산을 수행해야 할 수도 있다. 예를 한번 들어보자. 호출될 때마다 카운터를 증가시키고 호출 횟수와 같은 횟수만큼 특정 루프를 실행하는 스마트 컨트랙트를 작성할 수 있다. 100번째 호출에서 복권과 같은 특별한 상을 줄 수도 있지만, 그것을 계산하려면 추가적인 연산이 필요하다. 컨트랙트를 99번 호출하는 동안 결과가 있었고, 100번째 호출에서 매우 다른 일이 발생한다. 지급할 가스의 양은 트랜잭션이 블록에 포함되기 전에 얼마나 많은 다른 트랜잭션이 해당 기능을 호출했는지에 따라 다르다. 여러분의 추정치는 99번째 트랜잭션을 기반으로 하지만, 트랜잭션이 확인되기 직전에 다른 사람이 99번째 컨트랙트를 요청한다. 이제 여러분은 호출할 수 있는 100번째 트랜잭션이고 계산 작업(및 가스 비용)은 훨씬 더 높다.

이더리움에서 사용되는 일반적인 비유를 빌리자면 gasLimit를 자동차의 연료 탱크 용량으로 생각할 수 있다(자동차가 트랜잭션이다). 여행에 필요한 만큼의 가스를 탱크에 채운다(트랜잭션 유효성 확인에 필요한 계산). 금액을 어느 정도 예측할 수 있지만, 연료 소비를 증가시키는 우회(좀 더 복잡한 실행 경로)같은 예기치 않은 변경사항이 있을 수 있다.

그러나 연료 탱크의 비유는 오해의 소지가 있고 여행이 완료된 후에 실제 가스 사용량에 따라 요금을 지급하는 주유 회사의 신용 계정과 좀 더 비슷하다. 트랜잭션을 전송할 때 첫 번째 유효성 확인 단계 중 하나는 그것이 발생된 계정이 (가스 가격 x 가스 요금)을 지급할 만큼 충분한 이더를 갖고 있는지 확인하는 것이다. 그러나 트랜잭션이 완료될 때까지 여러분의 계좌에서 금액이 실제로 차감되지 않는다. 트랜잭션에 의해 실제로 소비된 가스의 요금만 청구되지만, 트랜잭션을 보내기 전에 지급할 의사가 있는 최대 금액만큼 충분한 잔액이 있어야 한다.

4. 트랜잭션 수신자

to 필드에 트랜잭션 수신자가 지정된다. 이것은 20바이트 이더리움 주소를 포함한다. 주소는 EOA 또는 컨트랙트 주소일 수 있다.

이더리움은 이 필드를 더는 검증하지 않는다. 모든 20바이트 값은 유효한 것으로 간주한다. 20바이트 값이 개인키가 없거나 상응하는 컨트랙트가 없는 주소의 경우에도 트랜잭션은 여전히 유효하다. 이더리움은 주소가 공개키(따라서 개인키)에서 올바르게 파생되었는지 여부를 알 수 있는 방법이 없다.

이더리움 프로토콜은 트랜잭션의 수신자 주소를 검증하지 않는다. 해당하는 개인키 또는 컨트랙트가 없는 주소로 보낼 수도 있다. 그러면 이더가 연소(burning)되어 영구적으로 사용할 수 없게 된다. 유효성 검사는 사용자 인터페이스 수준에서 수행되어야 한다.

트랜잭션을 잘못된 주소로 보내면, 대부분의 경우에 해당 주소에 대응하는 개인키도 알 수 없고 서명도 만들 수 없기 때문에, 보내진 이더는 다시 사용할 수 없는 상태가 되므로 영원히 소실된 것으로 간주할 수 있다. 주소 확인은 사용자 인터페이스 수준에서 처리해야 한다고 가정한다. 사실, 이더를 연소시키는 데는 여러 가지 정당한 이유가 있을 수 있다. 예를 들면, 지급 채널 및 기타 스마트 컨트랙트에서의 부정 행위를 저지하는 것이나, 이더의 양이 유한하므로 이더를 연소시키면 모든 이더 보유자에게 연소된 값을 효과적으로 분배한 것으로 이해할 수 있다(그들이 보유한 이더의 양에 비례하여).

5. 트랜잭션 값과 데이터

트랜잭션의 주요 '페이로드(payload)'는 값(value)과 데이터(data)라는 2개의 필드에 포함된다. 트랜잭션은 값과 데이터, 값만, 데이터만 또는 값이나 데이터를 모두 가지지 않는 네 가지 조합이 모두 유효하다.

값만 있는 트랜잭션은 지급(payment)이다. 데이터만 있는 트랜잭션은 호출(invocation)이다. 값과 데이터 모두를 사용한 트랜잭션은 지급과 호출이다. 값과 데이터가 모두 없는 트랜잭션은 단지 가스 낭비일 뿐이다. 그러나 가능하긴 하다.

이 모든 조합을 시도해 보자. 먼저 데모를 읽기 쉽게 만들기 위해 지갑에서 출발지와 도착지 주소를 설정한다.

src = web3.eth.accounts[0];
dst = web3.eth.accounts[1];

첫 번째 트랜잭션에는 값(지급)만 포함되며 데이터 페이로드는 없다.

web3.eth.sendTransaction({from: src, to: dst, \
	value: web3.toWei(0.01, "ether"), data: ""});

지갑은 [그림 6-1]과 같이 보낼 값을 나타내는 확인 화면을 표시한다.

[그림 6-1] 값은 있지만 데이터가 없는 트랜잭션을 보여주는 패리티 지갑

다음 예제는 값과 데이터 페이로드가 모두 있는 경우다.

web3.eth.sendTransaction({from: src, to: dst, \
	value: web3.toWei(0.01, "ether"), data: "0x1234"});

지갑은 [그림 6-2]와 같이 전송할 값과 데이터 페이로드를 나타내는 확인 화면을 보여준다.

[그림 6-2] 값과 데이터가 있는 트랜잭션을 보여주는 패리티 지갑

다음 트랜잭션은 데이터 페이로드를 포함하지만 값은 0을 지정한다.

web3.eth.sendTransaction({from: src, to: dst, value: 0, data: "0x1234"});

지갑은 [그림 6-3]과 같이 0 값과 데이터 페이로드를 나타내는 확인 화면을 보여준다.

[그림 6-3] 값이 없고 데이터만 있는 트랜잭션을 보여주는 패리티 지갑

마지막으로, 이 트랜잭션에는 보낼 값이나 데이터 페이로드가 포함되지 않는다.

web3.eth.sendTransaction({from: src, to: dst, value: 0, data: ""});

지갑은 [그림 6-4]와 같이 0 값을 나타내는 확인 화면을 보여준다.

[그림 6-4] 값이 없고 데이터가 없는 트랜잭션을 보여주는 패리티 지갑

5-1. EOA 및 컨트랙트에 값 전달

값을 포함하는 이더리움 트랜잭션을 구성하면 지급(payment)과 동일하다. 이러한 트랜잭션은 대상 주소가 컨트랙트인지 여부에 따라 다르게 작동한다.

EOA 주소의 경우 또는 블록체인의 컨트랙트로 표시되지 않는 주소의 경우 이더리움은 상태 변경을 기록하여 주소 잔액에 보낸 값을 추가한다. 이전에 주소가 표시되지 않은 경우, 클라이언트 내부 상태 표현에 추가되고 잔액은 지급 금액으로 초기화된다.

목적지 주소(to)가 컨트랙트라면 EVM은 컨트랙트를 실행하고 트랜잭션의 데이터 페이로드에 지정된 함수를 호출하려고 시도한다. 트랜잭션에 데이터가 없으면 EVM은 폴백(fallback) 함수를 호출하고, 해당 함수가 지급 가능하다면 다음에 수행할 작업을 결정하기 위해 함수를 실행한다. 폴백 함수가 없다면 트랜잭션의 효과는 지갑에 지급하는 것과 마찬가지로 컨트랙트의 잔액을 늘린다.

컨트랙트는 함수가 호출될 때 또는 함수에 코딩된 조건에 따라 즉시 예외를 발생시켜 입금을 거부할 수 있다. 함수가 예외 없이 성공적으로 끝나면, 컨트랙트의 상태가 이더 잔액이 증가했음을 반영하여 업데이트된다.

5-2. EOA 또는 컨트랙트에 데이터 페이로드 전달

트랜잭션에 데이터가 포함되어 있으면 받는 주소는 컨트랙트 주소가 될 가능성이 크다. 그렇다고 해서 이더리움 프로토콜에서 완전히 유효한 데이터 페이로드를 EOA에 보낼 수 없다는 뜻은 아니다. 그러나 이 경우 데이터 해석은 EOA에 접근하는 데 사용하는 지갑에 달려 있다. 그것은 이더리움 프로토콜에 의해 무시된다. 또한 대부분의 지갑은 자신이 제어하는 EOA에 대한 트랜잭션에서 수신된 모든 데이터를 무시한다.

미래에는 지갑이 컨트랙트 방식대로 데이터를 해석할 수 있도록 하는 표준이 등장할 수 있으므로 트랜잭션은 사용자 지갑 내부에서 실행되는 함수를 호출할 수 있다. 중요한 차이점은 EOA에 의한 데이터 페이로드의 해석은 컨트랙트 실행과 달리 이더리움의 합의 규칙의 적용을 받지 않는다는 것이다.

트랜잭션이 컨트랙트 주소로 데이터를 전달한다고 가정해 보자. 이 경우 데이터는 EVM에 의해 컨트랙트 호출(contract invocation)로서 해석된다. 대부분의 컨트랙트에서는 이 데이터를 함수 호출(function invocation)로 사용하며, 명명된 함수를 호출하고 인코딩된 인수를 함수에 전달한다.

ABI 호환 컨트랙트(모든 컨트랙트라고 가정할 수 있음)로 전송된 데이터 페이로드는 다음을 16진수로 시리얼라이즈한 인코딩이다.

함수 선택기(function selector)

  • 함수 선택기는 함수 프로토타입의 Keccak-256 해시의 처음 4바이트다. 이렇게 하면 컨트랙트에서 호출할 함수를 정확하게 식별할 수 있다.

함수 인수(function argument)

  • 함수의 인수는 ABI 사양에 정의된 다양한 기본 유형에 대한 규칙에 따라 인코딩된다.

chapter2 Faucet.sol 예제에서는 출금하는 기능을 다음과 같이 정의했다.

function withdraw(uint withdraw_amount) public {

함수의 프로토타입(prototype)은 함수의 이름을 포함하는 문자열로 정의되고, 각 인수의 데이터 유형이 괄호 안에 들어 있으며, 쉼표로 구분된다. 함수 이름은 withdraw이며, 여기에는 uint(uint256의 별칭)인 단일 인수가 사용되므로 withdraw의 프로토타입은 다음과 같다.

withdraw(uint256)

이 문자열의 Keccak-256 해시를 계산해 보자.

> web3.utils.sha3("withdraw(uint256)");
'0x2e1a7d4d13322e7b96f9a57413e1525c250fb7a9021cf91d1540d5b69f16a49f'

해시의 처음 4바이트는 0x2ela7d4d이다. 이것이 '함수 선택기' 값이다. 이 값은 우리가 원하는 함수를 컨트랙트에게 알린다.

다음으로, withdraw_amount 인수로 전달할 값을 계산해 보자. 우리는 0.01이더를 출금하고 싶다. 이를 16진수로 시리얼라이즈된 부호 없는 빅엔디안 256비트 정수로 인코딩하여 웨이로 표현해 보자.

> withdraw_amount = web3.utils.toWei(0.01, "ether");
'10000000000000000'
> withdraw_amount_hex = web3.utils.toHex(withdraw_amount);
'0x2386f26fc10000'

이제 함수 선택기를 양(amount)에 추가한다(32바이트로 채워짐).

2e1a7d4d000000000000000000000000000000000000000000000000002386f26fc10000

이것이 우리 트랜잭션의 데이터 페이로드이며, withdraw 함수를 호출하고 0.01이더를 withdraw_amount로 요청한다.

6. 특별 트랜잭션: 컨트랙트 생성

우리가 언급해야 할 특별한 경우 중 하나는 블록체인에 새로운 컨트랙트를 만들어 향후 사용을 위해 배포하는 트랜잭션이다. 컨트랙트 생성 트랜잭션은 제로 어드레스라고 하는 특수 대상 주소로 전송된다. 컨트랙트 등록 트랜잭션의 to 필드는 0x0 주소를 포함한다. 이 주소는 EOA(해당하는 개인키-공개키 쌍이 없음)나 컨트랙트를 나타내지 않는다. 결코 이더를 소비하거나 트랜잭션을 시작할 수 없다. 이 필드는 목적지로만 사용되며, '컨트랙트 작성'이라는 특별한 의미로 사용된다.

제로 어드레스는 컨트랙트 생성에만 사용하려는 의도로 만들어졌지만, 때로는 제로 어드레스를 목적지로 하는 다양한 트랜잭션이 있다. 이것에 대해서는 두 가지로 설명할 수 있다. 즉, 실수로 인한 이더 손실이나 의도적인 이더 연소(의도적으로 이더를 소비할 수 없는 주소로 보내서 이더를 파괴함)다. 그러나 이더의 의도적인 연소를 원한다면 네트워크에 의도를 분명히 하고 대신 지정된 주소를 사용해야 한다.

0x000000000000000000000000000000000000dEaD

지정된 연소 주소로 보내진 이더는 영원히 사라질 것이다.

컨트랙트 생성 트랜잭션은 컨트랙트를 생성할 컴파일된 바이트코드를 포함하는 데이터 페이로드만 포함하면 된다. 이 트랜잭션의 유일한 효과는 컨트랙트를 작성하는 것이다. 새 컨트랙트를 특정 잔액으로 설정해서 시작하려면 값 필드에 이더 금액을 포함할 수 있지만, 이는 전적으로 선택사항이다. 데이터 페이로드(컨트랙트 없음) 없이 컨트랙트 생성 주소에 값(이더)을 보내면, 그 효력은 이더를 연소 주소로 전송하는 것과 같다. 트랜잭션이 일어날 컨트랙트가 없기 때문에 이더를 잃게 된다.

예를 들어, 데이터 페이로드에서 컨트랙트를 사용하여 트랜잭션을 수동으로 생성해서 2장에서 사용되는 Faucet.sol 컨트랙트를 생성할 수 있다. 컨트랙트는 바이트코드 표현으로 컴파일 해야 한다. 이것은 솔리디티로 컴파일할 수 있다.

$ solc --bin Faucet.sol

Binary:
6060604052341561000f57600080fd5b60e58061001d6000396000f30060606040526004361060...

동일한 정보를 리믹스(Remix) 온라인 컴파일러에서도 얻을 수 있다.

이제 우리는 트랜잭션을 생성할 수 있다.

> src = web3.eth.accounts[0];
> faucet_code = \
  "0x6060604052341561000f57600080fd5b60e58061001d6000396000f300606...f0029";
> web3.eth.sendTransaction({from: src, to: 0, data: faucet_code, \
  gas: 113558, gasPrice: 200000000000});

"0x7bcc327ae5d369f75b98c0d59037eec41d44dfae75447fd753d9f2db9439124b"

실수로 0x0에 이더를 보내고 그것을 영원히 잃는 데 드는 비용이 너무 많기 때문에, 제로 어드레스 컨트랙트 생성 시에는 항상 to 파라미터를 지정하는 것이 좋다. gasPrice 및 gasLimit도 지정해야 한다.

컨트랙트가 채굴되면 [그림 6-5]처럼 이더스캔(Etherscan) 블록 탐색기에서 볼 수 있다.

[그림 6-5] 성공적으로 채굴된 컨트랙트를 보여주는 이더스캔

컨트랙트에 대한 정보를 얻기 위해 트랜잭션 영수증을 확인할 수 있다.

> web3.eth.getTransactionReceipt( \
  "0x7bcc327ae5d369f75b98c0d59037eec41d44dfae75447fd753d9f2db9439124b");

{
  blockHash: "0x6fa7d8bf982490de6246875deb2c21e5f3665b4422089c060138fc3907a95bb2",
  blockNumber: 3105256,
  contractAddress: "0xb226270965b43373e98ffc6e2c7693c17e2cf40b",
  cumulativeGasUsed: 113558,
  from: "0x2a966a87db5913c1b22a59b0d8a11cc51c167a89",
  gasUsed: 113558,
  logs: [],
  logsBloom: \
    "0x00000000000000000000000000000000000000000000000000...00000",
  status: "0x1",
  to: null,
  transactionHash: \
    "0x7bcc327ae5d369f75b98c0d59037eec41d44dfae75447fd753d9f2db9439124b",
  transactionIndex: 0
}

여기에는 컨트랙트 주소가 포함되어 있다. 컨트랙트 주소는 이전 절에서 설명한 대로 컨트랙트에 자금을 송금하고 자금을 받을 때 사용할 수 있다.

> contract_address = "0xb226270965b43373e98ffc6e2c7693c17e2cf40b"
> web3.eth.sendTransaction({from: src, to: contract_address, \
  value: web3.utils.toWei(0.1, "ether"), data: ""});

"0x6ebf2e1fe95cc9c1fe2e1a0dc45678ccd127d374fdf145c5c8e6cd4ea2e6ca9f"

> web3.eth.sendTransaction({from: src, to: contract_address, value: 0, data: \
  "0x2e1a7d4d000000000000000000000000000000000000000000000000002386f26fc10000"});

"0x59836029e7ce43e92daf84313816ca31420a76a9a571b69e31ec4bf4b37cd16e"

[그림 6-6]에서 볼 수 있듯이 두 트랜잭션은 이더스캔에서 볼 수 있다.

[그림 6-6] 자금 송금 및 수령 트랜잭션을 보여주는 이더스캔

7. 디지털 서명

여기서는 디지털 서명이 작동하는 방식과 디지털 서명이 개인키를 공개하지 않고 어떻게 개인키의 소유권을 증명하는지 살펴본다.

7-1. 타원 곡선 디지털 서명 알고리즘

이더리움에서 사용되는 디지털 서명 알고리즘은 ECDSA(Elliptic Curve Digital Signature Algorithm)이다. 타원 곡선의 개인키-공개키 쌍을 기반으로 한다.

디지털 서명은 이더리움에서 세 가지 용도로 사용된다. 첫째, 서명은 이더리움 계정과 개인키의 소유자가 이더 지출 또는 컨트랙트 이행을 승인했음을 증명한다. 둘째, 부인방지(non-repudiation)를 보장한다. 즉, 허가의 증거는 부인할 수 없다. 셋째, 서명은 트랜잭션이 서명된 후에는 트랜잭션 데이터가 수정되지 않았고 어느 누구도 트랜잭션 데이터를 수정할 수 없음을 증명한다.

위키피디아의 디지털 서명 정의

디지털 서명(digital signature)은 디지털 메시지나 문서의 진위를 표현하기 위한 수학적 기법이다. 유효한 디지털 서명은 메시지가 알려진 발신자(인증(authentication))에 의해 생성되었고, 보낸 사람이 메시지를 보내지 않았음을 부인할 수 없으며(부인 방지(non-repudiation)), 메시지가 전송 중에 변경되지 않았다고 믿을 수 있는 근거를 제공한다(무결성(integrity)).

출저 : https://en.wikipedia.org/wiki/Digital_signature

7-2. 디지털 서명 작동 방법

디지털 서명은 두 단계로 구성된 수학적 체계다. 첫 번째 부분은 메시지(여기서는 트랜잭션)에서 개인키(서명 키)를 사용하여 서명을 만드는 알고리즘이다. 두 번째 부분은 누구나 메시지와 공개키만 사용하여 서명을 검증할 수 있게 해주는 알고리즘이다.

디지털 서명 만들기

이더리움의 ECDSA 구현에서 서명된 '메시지'는 트랜잭션이다. 또는 좀 더 정확하게 말하자면, 트랜잭션의 RLP로 인코딩된 데이터의 Keccak-256 해시다. 서명 키는 EOA의 개인키다.

결과는 다음과 같다.

Sig = F sig(F keccak256(m), k)

여기서,

  • k는 서명 개인키다.
  • m은 RLP 인코딩된 트랜잭션이다.
  • F keccak256은 Keccak-256 해시 함수다.
  • F sig는 서명 알고리즘이다.
  • Sig는 결과 서명이다.

함수 F sig는 일반적으로 rs라고 하는 두 값으로 구성된 서명 Sig를 생성한다.

Sig = (r, s)

7-3. 서명 확인

서명을 확인하려면 서명(r및 s)과 시리얼라이즈된 트랜잭션, 그리고 서명을 만드는 데 사용된 개인키에 상응하는 공개키가 있어야 한다. 본질적으로, 서명 확인은 공개키를 생성한 개인키의 소유자만이 트랜잭션에서 서명을 생성할 수 있음을 의미한다.

서명 검증 알고리즘은 메시지(즉, 우리가 사용하기 위한 트랜잭션 해시), 서명자의 공개키 및 서명(r 및 s 값)을 가져와서 서명이 메시지와 공개키에 유효하면 true를 반환한다.

7-4. ECDSA 계산

앞서 언급했듯이, 서명은 2개의 값 r과 s로 구성된 서명을 생성하는 수학 함수 F sig에 의해 생성된다. 이 절에서는 함수 F sig를 좀 더 자세히 살펴보자.

서명 알고리즘은 처음에는 일시적인(임시) 개인키를 암호학적인 안전한 방법으로 생성한다. 이 임시 키는 이더리움 네트워크에서 서명된 트랜잭션을 보는 공격자가 발신자의 실제 개인키를 계산할 수 없도록 r 및 s 값을 계산하는 데 사용된다.

전에 '공개키' 절에서 봤듯이, 일시적인 개인키는 해당 공개키(임시)를 만드는 데 사용되므로 다음과 같은 이점이 있다.

  • 임시 개인키로 사용되는 암호학적 안전한 난수 q
  • q로부터 생성된 상응하는 임시 공개키 Q와 타원 곡선 생성자 점 G

디지털 서명의 r 값은 일시적인 공개키 Q의 x 좌표다.

여기서 알고리즘은 다음과 같이 서명의 s 값을 계산한다.

s ≡ q^-1(Keccak256(m) + r * k) (mod p)

여기서,

  • q는 일시적인 개인키다.
  • r은 일시적인 공개키의 x 좌표다.
  • k는 서명(EOA 소유자의) 개인키다.
  • m은 트랜잭션 데이터다.
  • p는 타원 곡선의 소수 차수다.

검증은 r 및 s 값과 보낸 사람의 공개키를 사용하여 타원 곡선(서명 생성에 사용되는 임시 공개키)의 한 지점인 값 Q를 계산하는, 서명 생성 함수의 반대 프로세스다. 단계는 다음과 같다.

  1. 모든 입력이 올바르게 구성되어 있는지 확인한다.
  2. w = ^s-1 mod p를 계산한다.
  3. u1 = Keccak256(m) * w mod p를 계산한다.
  4. u2 = r * w mod p를 계산한다.
  5. 마지막으로, 다음 타원 곡선을 계산한다. Q ≡ u1 * _G + u2 * K (mod p)

여기서,

  • rs는 서명 값이다.
  • K는 서명자의(EOA 소유자의) 공개키다.
  • m은 서명된 트랜잭션 데이터다.
  • G는 타원 곡선 생성자 점이다.
  • p는 타원 곡선의 소수 차수다.

계산된 포인트 Q의 x 좌표가 r과 같으면, 검증자는 서명이 유효하다고 결론을 내릴 수 있다.

서명을 검증할 때 개인키는 알려지지도 공개되지도 않는다.

ECDSA는 상당히 복잡한 수학으로, 자세한 설명은 이 책의 범위를 벗어난다. 많은 훌륭한 온라인 가이드가 단계별로 설명해 주고 있으니 'ECDSA 설명'을 검색하거나 링크를 참고하라.

7-5. 트랜잭션 서명 실습

유효한 트랜잭션을 생성하려면 발신자는 ECDSA를 사용하여 메시지에 디지털 서명을 해야한다. '트랜잭션에 서명하시오'라고 할 때 실제로는 'RLP 시리얼라이즈된 트랜잭션 데이터의 Keccak-256 해시에 서명하시오'라는 뜻이다. 다시 말해, 서명을 트랜잭션 자체가 아니라 트랜잭션 데이터의 해시에 적용된다.

발신자는 이더리움에서 트랜잭션을 발생하기 위해 반드시 다음 과정을 거쳐야 한다.

  1. nonce, gasPrice, gasLimit, to, value, data, chainID, 0, 0의 9개의 필드를 포함하는 트랜잭션 데이터 구조를 만든다.
  2. RLP로 인코딩된 트랜잭션 데이터 구조의 시리얼라이즈된 메시지를 생성한다.
  3. 이 시리얼라이즈된 메시지의 Keccak-256 해시를 계산한다.
  4. 원래 EOA의 개인키로 해시에 서명하여 ECDSA 서명을 계산한다.
  5. ECDSA 서명에 계산된 v, r, s 값을 트랜잭션에 추가한다.

특수 서명 변수 v는 두 가지를 나타내는데, ECDSArecover 함수가 서명을 확인하는 데 도움이 되는 복구 식별자와 체인 ID이다. v는 27 또는 28 중 하나로 계산되거나, 체인 ID의 두 배에 35 또는 36이 더해져 계산된다. 복구 식별자('구식' 서명의 27 또는 28, 전체 스퓨리어스 드리곤(Spurious Dragon) 유형 트랜잭션의 35 또는 36)는 공개키 y 구성요소의 패리티를 나타내는 데 사용된다.

블록 #2,675,000에서 이더리움은 '스퓨리어스 드래곤' 하드 포크를 구현했으며, 그 밖의 변경사항 중 하나로 트랜잭션 재생 방지(트랜잭션이 하나의 네트워크에서 다른 네트워크로 전파 및 재생되는 것을 방지)를 포함하는 새로운 서명 체계를 도입했다. 이 새로운 서명 체계는 EIP-155에 명시되어 있다. 이 변경은 트랜잭션의 형식과 서명에 영향을 미친다. 따라서 두 가지 형태 중 하나를 취하고 해싱되는 트랜잭션 메시지에 포함된 데이터 필드를 나타내는 세 가지 서명 변수 중 첫 번째 변수(즉, v)에 주의를 기울여야 한다.

7-6. 원시 트랜잭션 생성 및 서명

이 절에서는 원시 트랜잭션을 생성하고 ethereumjs-tx 라이브러리를 사용하여 서명한다. 이는 일반적으로 사용자를 대신해서 트랜잭션에 서명을 하는 지갑 또는 애플리케이션의 함수가 어떻게 작동하는지를 보여준다. 이 예제의 소스 코드는 책의 깃허브 저장소에 있는 raw_tx_demo.js 파일에 있다.

// 사전 요구사항 로드
//
// npm init
// npm install ethereumjs-tx
//
// $ node raw_tx_demo.js 실행
const ethTx = require('ethereumjs-tx').Transaction;

const txData = {
	nonce: '0x0',
	gasPrice: '0x09184e72a000',
	gasLimit: '0x30000',
	to: '0xb0920c523d582040f2bcb1bd7fb1c7c1ecebdb34',
	value: '0x00',
	data: '',
	v: "0x1c", // 이더리움 메인넷 체인ID
	r: 0,
	s: 0 
};

tx = new ethTx(txData);
console.log('RLP-Encoded Tx: 0x' + tx.serialize().toString('hex'))

txHash = tx.hash(); // 이 단계는 RLP로 인코딩하고 해시를 계산한다.
console.log('Tx Hash: 0x' + txHash.toString('hex'))

// 트랜잭션 서명
const privKey = Buffer.from('91c8360c4cb4b5fac45513a7213f31d4e4a7bfcb4630e9fbf074f42a203ac0b9', 'hex');
tx.sign(privKey);

serializedTx = tx.serialize();
rawTx = 'Signed Raw Transaction: 0x' + serializedTx.toString('hex');
console.log(rawTx)

예제 코드를 실행하면 다음과 같은 결과가 생성된다.

$ node raw_tx_demo.js
RLP-Encoded Tx: 0xe6808609184e72a0008303000094b0920c523d582040f2bcb1bd7fb1c7c1...
Tx Hash: 0xaa7f03f9f4e52fcf69f836a6d2bbc7706580adce0a068ff6525ba337218e6992
Signed Raw Transaction: 0xf866808609184e72a0008303000094b0920c523d582040f2bcb1...

7-7. EIP-155를 사용한 원시 트랜잭션 생성

EIP-155 '단순 재생 공격 방지(Simple Replay Attack Protection)' 표준은 서명하기 전에 트랜잭션 데이터 내부에 체인 식별자(chain identifier)를 포함하여 재생 공격 방지가 가능한 트랜잭션 인코딩을 지정한다. 이렇게 하면 하나의 블록체인(예: 이더리움 메인 네트워크)에 대해 생성된 트랜잭션이 다른 블록체인(예: 이더리움 클래식 또는 롭스텐 테스트 네트워크)에서 유효하지 않다. 따라서 표준의 이름 그대로 한 네트워크에서 전파된 트랜잭션은 다른 네트워크에서 재생될 수 없다.

EIP-155는 트랜잭션 데이터 구조의 주요 6개 필드에 체인 식별자, 0, 0의 3개 필드를 추가한다. 이 세 필드는 인코딩되고 해싱되기 전에 트랜잭션 데이터에 추가된다. 따라서 트랜잭션의 해시가 변경되어 나중에 서명이 적용된다. 체인 식별자가 서명된 데이터에 포함됨으로써, 트랜잭션 서명은 체인 식별자가 수정되면 서명이 무효화되어 데이터 변경을 식별할 수 있다. 따라서 EIP-155는 서명의 유효성이 체인 식별자에 의존하기 때문에 다른 체인에서 트랜잭션을 재생할 수 없다.

체인 식별자 필드는 아래 표과 같이 트랜잭션이 의미하는 네트워크에 따라 값을 사용한다.

체인 식별자

체인체인ID
이더리움 메인넷1
모던(구식), 확장2
롭스텐3
린케비4
루트스톡 메인넷30
루트스톡 테스트넷31
코반42
이더리움 클래식 메인넷61
이더리움 클래식 테스트넷62
게스 사설 테스트넷1337

결과로 생성되는 트랜잭션 구조는 RLP로 인코딩되고 해싱되고 서명된다. 서명 알고리즘은 v 접두어에 체인 식별자를 인코딩하기 위해 약간 수정된다.

자세한 내용은 EIP-155 사양를 참고하자.

8. 서명 접두어 값(v) 및 공개키 복구

트랜잭션 메시지는 '발신자(from)'을 포함하지 않는다. 이는 발신자의 공개키가 ECDSA 서명을 통해 직접 계산될 수 있기 때문이다. 공개키가 있으면 쉽게 주소를 계산할 수 있다. 서명자의 공개키를 복구하는 프로세스를 공개키 복구(public key recovery)라고 한다.

'ECDSA 계산' 절에서 계산된 값 r과 s가 주어지면, 2개의 가능한 공개키를 계산할 수 있다.

먼저 서명에 있는 x 좌표인 r 값에서 2개의 타원 곡선 점 R과 R'를 계산한다. 타원 곡선은 x축에 대칭이므로, 어떤 값 x에 대해서도 곡선에 2개의 가능한 값이 있기 때문에 2개의 점이 있다.

r에서 우리는 또한 r의 곱셈 역함수인 r^-1을 계산한다.

마지막으로, 메시지 해시의 n 최하위 비트인 z를 계산한다. 여기서 n은 타원 곡선의 차수다.

가능한 2개의 공개키는 다음과 같다.

  • K1 = r^–1(sR – zG)

  • K2 = r^–1(sR' – zG)

여기서,

  • K1K2는 서명자의 두 가지 가능한 공개키다.
  • r^-1은 서명 r 값의 곱셈 역함수다.
  • s는 서명의 s 값이다.
  • RR'는 두 가지 가능한 일시적인 공개키 Q이다.
  • z는 메시지 해시의 n 최하위 비트다.
  • G는 타원 곡선 생성자 점이다.

좀 더 효율적으로 하기 위해, 트랜잭션 서명에는 2개의 가능한 R 값 중 임시 공개키가 무엇인지 알려주는 접두어 값 v가 포함된다. v가 짝수이면 R이 올바른 값이고, v가 홀수이면 R'이다. 그런 식으로 R에 대해 하나의 값만, K에 대해 하나의 값만 계산해야 한다.

9. 서명 및 전송 분리(오프라인 서명)

트랜잭션이 서명되면 트랜잭션은 이더리움 네트워크로 전송할 준비가 된다. 트랜잭션 생성, 서명, 브로드캐스트의 세 단계는 일반적으로 단일 작업(예: web3.eth.sendTransaction 사용)에 의해 처리된다. 그러나 '원시 트랜잭션 생성 및 서명' 절에서 봤듯이, 두 단계로 나누어 트랜잭션을 생성하고 서명할 수 있다. 서명된 트랜잭션이 있으면 web3.eth.sendSignedTransaction을 사용하여 트랜잭션을 16진수로 인코딩하고 서명해서 이더리움 네트워크에서 전송할 수 있다.

트랜잭션의 서명과 전송을 분리하려는 이유는 무엇인가? 가장 보편적인 이유는 보안이다. 트랜잭션에 서명하는 컴퓨터에는 잠금 해제된 개인키가 메모리에 로드되어 있어야 한다. 전송을 수행하는 컴퓨터는 인터넷에 연결되어 있어야 하며, 이더리움 클라이언트를 실행해야 한다. 이 두 기능이 하나의 컴퓨터에 있으면 온라인 시스템에 개인키가 있게 되며, 이는 매우 위험한 상황이 된다. 서명 및 전송 기능을 분리하여 각기 다른 시스템(오프라인 및 온라인 장치 각각)에서 수행하는 것을 오프라인 서명(offline signing)이라고 하며, 이는 일반적인 보안 방법이다.

[그림 6-7]은 이더리움 트랜잭션의 오프라인 서명 프로세스를 보여준다.

  1. 현재의 논스 및 사용 가능한 자금을 검색할 수 있는 계정에서 서명되지 않은 트랜잭션을 온라인 컴퓨터에 만든다.
  2. 서명되지 않은 트랜잭션을 QR 코드 또는 USB 플래시 드라이브를 통해 트랜잭션 서명을 위한 '에어 갭(air-gapped)' 오프라인 장치로 전송한다.
  3. 이더리움 블록체인에 브로드캐스트하기 위해, 서명된 트랜잭션을 QR 코드 또는 USB 플래시 드라이브를 통해 온라인 장치로 전송한다.

[그림 6-7] 이더리움 트랜잭션의 오프라인 서명

필요한 보안 수준에 따라, '오프라인 서명' 컴퓨터는 격리되고 방화벽이 있는 서브넷(온라인이지만 분리되어 있음)에서 에어 갭 시스템으로 알려진 완전히 오프라인인 시스템에 이르기까지 온라인 컴퓨터와 분리 정도를 다르게 할 수 있다. 에어 갭 시스템에서는 네트워크 연결이 전혀 없다. 즉, 컴퓨터는 '에어'갭으로 온라인 환경과 분리되어 있다. 트랜잭션에 서명하려면 데이터 저장 매체 또는 웹캠과 QR 코드를 사용하여 에어 갭 컴퓨터와 주고받는 트랜잭션을 생성한다. 물론 이것은 서명하고자 하는 모든 트랜잭션을 수동으로 전송해야 한다는 뜻이며, 스케일링을 할 수도 없다.

많은 경우에 완전 에어 갭 시스템을 활용할 수는 없지만, 약간의 격리로도 상당한 보안 이점을 얻을 수 있다. 예를 들어, 메시지 대기열 프로토콜만 허용하는 방화벽이 있는 격리된 서브넷은 온라인 시스템이 서명하는 것보다 공격할 부분을 줄어들게 하고 훨씬 높은 보안을 제공할 수 있다. 많은 기업이 ZeroMQ(0MQ) 같은 프로토콜을 이 용도로 사용한다. 이와 같은 설정으로, 서명을 위해 트랜잭션이 시리얼라이즈되고 대기한다. 대기열 전송 프로토콜은 TCP 소켓과 비슷한 방식으로 시리얼라이즈된 메시지를 서명 컴퓨터로 전송한다. 서명 컴퓨터는 대기열에서 시리얼라이즈된 트랜잭션을 신중하게 읽고, 적절한 키와 함께 서명을 적용한 후에 보내는 대기열에 배치한다. 보내는 대기열은 서명된 트랜잭션을 대기열을 해소하고 전송하는 역할을 하는 이더리움 클라이언트가 있는 컴퓨터로 전송한다.

10. 트랜잭션 전파

이더리움 네트워크는 '플러드 라우팅(flood routing)' 프로토콜을 사용한다. 각 이더리움 클라이언트는 (이상적으로) 메시(mesh) 네트워크를 형성하는 피어투피어(P2P) 네트워크에서 노드(node) 역할을 한다. 어떠한 네트워크 노드도 특별하진 않다. 모두 동등한 역할을 한다. 우리는 '노드'라는 용어를 사용하며, 노드는 'P2P' 네트워크에 연결되어 참여하는 이더리움 클라이언트를 지칭한다.

트랜잭션 전파는 서명된 트랜잭션을 생성(또는 오프라인에서 수신)한 이더리움 노드에서 시작한다. 트랜잭션은 검증된 후에 트랜잭션을 생성한 '직접' 연결된 다른 모든 이더리움 노드로 전송된다. 평균적으로 각 이더리움 노드는 이웃(neighbor)이라고 불리는, 적어도 13개의 다른 노드에 대한 연결을 유지한다. 각 이웃 노드는 트랜잭션을 수신하자마자 즉시 유효성을 검사한다. 그들이 그것을 타당하다는 데 동의하면, 그들은 사본을 저장하고 모든 이웃에게 전파한다(출처를 제외하고). 결과적으로 트랜잭션은 네트워크의 모든 노드가 트랜잭션 사본을 가질 때까지 원래 노드에서 바깥쪽으로 '물결치며 퍼진다(flooding)'. 노드는 전달하는 메시지를 필터링할 수 있지만, 기본 규칙은 전달받은 모든 유효한 트랜잭션 메시지를 전파하는 것이다.

몇 초 내에 이더리움 트랜잭션이 전 세계의 모든 이더리움 노드로 전파된다. 각 노드의 관점에서 보면 트랜잭션의 출처를 식별할 수 없다. 노드로 전송한 이웃 노드는 트랜잭션의 생성자이거나 인접 노드 중 하나로부터 트랜잭션을 수신했을 수 있다. 트랜잭션의 출처를 추적하거나 전파를 방해하기 위해 공격자는 모든 노드 중 상당 부분을 제어한다. 이것은 P2P 네트워크의 보안 및 개인 정보 보호 설계의 일부이며, 특히 블록체인 네트워크에 적용된다.

11. 블록체인에 기록하기

이더리움의 모든 노드는 동등한 피어이지만, 일부는 채굴을 하며 고성능 그래픽 처리 장치(Graphics Processing Unit, GPU)가 장작된 컴퓨터인 채굴 팜(mining farm)에 트랜잭션 및 블록을 제공한다. 채굴 컴퓨터는 트랜잭션을 후보 블록에 추가하고 후보 블록을 유효하게 만드는 작업증명을 찾으려고 시도한다. (14장에서 더 자세히 논의할 것이다.)

너무 자세한 이야기는 생략하고, 유효한 트랜잭션이 결국 트랜잭션 블록에 포함되어 이더리움 블록체인에 기록된다. 트랜잭션이 블록으로 채워지면 계정의 잔액을 수정하거나(단순 결제의 경우) 내부 상태를 변경하는 컨트랙트를 호출하여 트랜잭션은 이더리움 싱글톤 상태를 수정한다. 이러한 변경사항은 이벤트가 포함될 수 있는 트랜잭션 영수증(receipt) 형식으로 트랜잭션과 함께 기록된다. (13장에서 이 모든 내용을 자세히 살펴볼 것이다.)

생성에서 EOA에 의한 서명, 전파, 그리고 마지막으로 채굴까지 완료된 트랜잭션은 싱글톤의 상태를 변경하고 블록체인에서 지울 수 없는 기록을 남긴다.

12. 다중 서명 트랜잭션

비트코인의 스크립팅 기능에 익숙하다면, 여러 당사자가 트랜잭션에 서명할 때만 자금을 사용할 수 있는 비트코인 다중 서명(multiple-signature, multisig) 계정을 만들 수 있음을 알고 있을 것이다(예: 2개 개인키 중 2개 서명 또는 4개 개인키 중 3개 서명). 이더리움의 기본 EOA 값 트랜잭션에는 다중 서명 조항이 없다. 그러나 이더와 토큰을 전송하는 어떤 조건들도 처리할 수 있는 스마트 컨트랙트를 사용해 임의의 서명 제한룰을 적용할 수 있다.

이 기능을 이용하려면 이더를 다중 서명 요구사항이나 지출 한도(또는 둘의 조합)와 같이 원하는 지출 규칙으로 프로그래밍한 '지갑 컨트랙트'로 보내야 한다. 그리고서 지갑 컨트랙트는 지출 조건이 충족되면 승인된 EOA의 요청에 따라 자금을 보낸다. 예를 들어, 다중 상태에서 이더를 보호하려면 이더를 다중 서명 컨트랙트로 전송하라. 다른 계정으로 자금을 보낼 때마다 모든 사용자는 일반 지갑 애플리케이션을 사용하여 컨트랙트에 트랜잭션을 보내야 하며, 계약이 최종 트랜잭션을 수행할 수 있도록 효과적으로 승인해야 한다.

이러한 컨트랙트는 내부 코드를 실행하거나 다른 컨트랙트를 실행하기 전에 다중 서명을 요구하도록 설계될 수도 있다. 이와 같은 체계에서의 보안은 궁극적으로 다중 서명 컨트랙트 코드에 의해 결정된다.

스마트 컨트랙트로 다중 서명 트랜잭션을 구현하는 기능은 이더리움의 유연성을 입증한다. 그러나 이러한 유연성으로 인해 다중 서명 체계의 보안을 약화시키는 버그가 발생할 수 있으므로 양날의 검이다. 실제로 간단한 'M-of-N' 다중 서명 구성을 스마트 컨트랙트르 이용하지 않고 EVM에서 직접 다중 서명 명령을 처리하게 하자는 제안이 많다. 이는 핵심 합의 규칙의 일부이며, 강력하고 안전한 것으로 입증된 비트코인의 다중 서명 시스템과 동일하다.

13. 결론

트랜잭션은 이더리움 시스템의 모든 활동의 시작점이다. 트랜잭션은 EVM이 컨트랙트를 평가하고, 잔액을 업데이트하며, 일반적으로 이더리움 블록체인의 상태를 수정하는 '입력'이다. 다음 장에서는 스마트 컨트랙트를 훨씬 더 자세하게 다루고 솔리디티 컨트랙트 지향 언어로 프로그래밍하는 방법을 배워보겠다.

profile
블록체인 개발 어때요

0개의 댓글