‘마스터링 이더리움’ 세미나 4 – 6장. 트랜잭션
이번 세미나에서는 ‘6장. 트랜잭션’을 다룹니다. 논스 관련 부분은 주의해서 읽습니다.
아래 인용한 본문 내용을 다시 한 번 읽어 봅니다.
- 트랜잭션은 외부 소유 계정(EOA)에 의해 서명된 메시지 인데, 이더리움 네트워크에 의해 전송되고 이더리움 블록체인에 기록된다.
- 트랜잭션은 EVM에서 상태 변경을 유발하거나 컨트랙트를 실행할 수 있는 유일한 방법이다.
- 이더리움은 글로벌 싱글톤 상태 머신이며, 트랜잭션은 이 상태 머신을 움직여서 상태를 변경할 수 있도록 만든다.
- 컨트랙트는 독자적으로 실행되지 않는다. 또한 이더리움도 자율적으로 실행되지 않는다. 모든 것은 트랜잭션으로부터 시작된다.
트랜잭션 구조
트랜잭션 구성요소들을 기억합니다.
- 트랜잭션은 다음 데이터를 포함하는 RLP(Recursive Lenght Prefix) 인코딩 체계를 사용해서 직렬화된 바이너리 메시지다. 이더리움의 모든 숫자는 8비트 배수 길이의 빅엔디안 정수로 인코딩 된다.
- 논스(nonce)
- A sequence number, issued by the originating EOA, used to prevent message replay
- 가스 가격(gas price)
- 가스 한도(gas limit)
- 수신자(recipient)
- 값(value)
- The amount of ether to send to the destination
- 데이터(data)
- The variable-length binary data payload
- v, r, s
- The three components of an ECDSA digital signature of the originating EOA
- 논스(nonce)
이더를 전송하려면 ‘누가’ ‘누구에게’ ‘얼마 만큼의’ 이더를 전송하는지를 명시해야 합니다. 누가는 디지털 서명으로, 누구에게는 수신자로 얼마 만큼은 값으로 표현됩니다. 이더리움에서는 디지털 서명을 위해 v, r, s 세 개의 값을 사용합니다. 트랜잭션 구성요소들에 누가에 해당하는 것이 없음을 알 수 있습니다. v, r, s를 사용해서 누가에 해당하는 발신자 주소를 알아낼 수 있습니다.
컨트랙트를 실행하려면 어떤 컨트랙트의 어떤 함수를 어떤 인수들을 가지고 실행할 것인지를 표현해야 합니다. 데이터가 이 역할을 합니다.
상식적으로 생각해 보면 이더만 보내는 트랜잭션이라면 데이터 값이 필요 없고, 컨트랙트만 실행하는 트랜잭션이라면 이더 값이 필요 없을 것입니다. 하지만 특별한 경우에는 컨트랙트 실행 시에 이더를 보내는 경우도 있습니다.
이더리움은 가스라는 개념으로 탈중앙 환경에서 튜링 완전이 갖는 문제를 해결한다고 했습니다. 구체적으로는 가스 가격과 가스 한도를 사용하는 것입니다.
가스량은 트랜잭션이 요구하는 EVM 실행에 따라 결정됩니다. 가스 가격과 가스 한도는 트랜잭션 전송자가 정합니다. 가스 가격은 웨이로 작성되고, 가스 한도 내에서 트랜잭션이 실행됩니다. 트랜잭션 수수료는 전송 시에는 ‘가스 한도 * 가스 가격’으로 정해지지만, 실제 지불되는 수수료는 ‘사용된 가스량 * 가스 가격’이 됩니다. 가스 한도를 높게 설정했다고 해서 걱정할 필요는 없다는 거지요. 실제적으로 지불되지 않는다고 해도 가스 한도는 적당히 잡아야 합니다. 이더를 보낼 때 지갑은 가스 한도를 기준으로 수수료를 계산하기 때문에 지갑에 있는 잔고는 ‘전송할 이더 수량 + 수수료’ 이상 이어야 합니다. 가스 한도를 너무 높게 책정하면 수수료가 커져서 보낼 이더를 더했을 때 잔고보다 커져서 트랜잭션을 전송할 수 없는 경우가 발생할 수 있습니다.
트랜잭션 논스
The nonce is one of the most important and least understood components of a transaction.
논스 중요한데 제대로 이해하지 못한 사람들이 많습니다. 이번 기회에 논스에 대해 확실하게 이해하고 넘어갑시다.
nonce: A scalar value equal to the number of transactions sent from this address or, in the case of accounts with associated code, the number of contract-creations made by this account.
논스는 어떤 계정(주소)이 전송한 트랜잭션 개수입니다. 위의 논스 정의 문장에서 뒷 부분 때문에 헷갈릴 수 있는데, 컨트랙트 생성도 트랜잭션을 전송해서 하는 것이기 때문에 컨트랙트 개수가 트랜잭션 개수에 해당한다는 것입니다. 한 주소로 이더 전송과 컨트랙트 생성을 같이 했다면 이들을 위해 전송한 트랜잭션 개수가 논스가 됩니다.
아래 인용한 문장을 다시 한 번 읽고 이더리움이 비트코인과 달리 계정 기반이고, 계정 기반 프로토콜의 경우 ‘논스’와 같은 개념이 필수적으로 사용될 수 밖에 없음을 이해하도록 합니다. 비트코인은 UTXO를 가지고 잔고를 관리하지만 이더리움은 계정에 직접 잔고를 두고 관리합니다.
It is important to note that the use of the nonce is actually vital for an account-based protocol, in contrast to the “Unspent Transaction Output” (UTXO) mechanism of the Bitcoin protocol.
비트코인에서는 비트코인 잔고가 UTXO set으로 관리됩니다. 비트코인 전송에 사용된 UTXO들은 트랜잭션이 처리되고 나면 UTXO set에서 제거되어 더 이상 사용할 수 없게 됩니다. 같은 트랜잭션을 복제해서 실행한다고 해도 입력이 참조한 UTXO가 더 이상 UTXO가 아니기 때문에 유효하지 않은 트랜잭션이 됩니다.
이더리움의 경우 잔고는 잔고 값으로 관리됩니다. 트랜잭션이 실행되면 잔고 값이 바뀌는 것입니다. 잔고 값만 바꿀 수 있다면 잔고를 조작하는 것이 가능하다는 것입니다. 같은 트랜잭션을 반복한다면 잔고가 있는 한 반복해서 이더를 인출할 수 있게 됩니다. 이러한 반복 실행을 막기 위해 이더리움이 선택한 방법이 논스입니다. ‘논스’라는 용어는 채굴에서도 같은 용어를 사용하기 때문에 블록의 논스와 트랜잭션 논스를 구분하 수 있어야 합니다.
논스 추적
In practical terms, the nonce is an up-to-date count of the number of confirmed (i.e., on-chain) transactions that have originated from an account. 번역서에서 ‘트랜잭션 건수에 대한 최신 통계다’라고 하고 있는데 원서의 어디에 통계라는 단어가 있는지 모르겠습니다.
논스는 0부터 시작합니다. 논스는 블록체인 계정 상태로 저장되지 않기 때문에 논스를 구하려면 해당 주소로 전송되어 확정된 트랜잭션 개수를 세어서 구해야 합니다. 트랜잭션 개수는 web3.eth.getTransactionCount과 같이 web3를 사용해서 구할 수 있습니다.
지갑을 개발할 때, 이번 장에서 다루고 있는 수준으로 논스를 이해하지 않고 getTransactionCount로 단순하게 논스를 결정한다면 낭패를 볼 수 있습니다. getTransactionCount가 확정된 트랜잭션 개수만 세고 보류(pending) 중인 트랜잭션 개수는 세지 않는 다는 점에 주의를 기울여야 합니다. 이렇게 만든 지갑은 사용자가 트랜잭션을 전송하고 확정될 때 까지 여유롭게 이더를 전송하는 경우 문제가 되지 않을 것입니다. 하지만 사용자가 연속해서 매우 빠르게 이더를 전송하게 되면 중복된 논스가 생겨 원하지 않는 결과를 얻게 될 것입니다. 예를 들어 3개의 트랜잭션을 전송했는데 1개는 확정되었고 2개가 보류중이라면 getTransactionCount는 1을 리턴할 거고, 새로 생성한 트랜잭션 논스는 1이 되어, 먼저 보낸 보류중인 트랜잭션들 중 하나와 논스 값이 같게 됩니다.
패리티는 빠르게 연속적으로 트랜잭션을 만들어 전송하더라도 논스를 올바르게 계산하는 parity_nextNonce 함수를 제공합니다. 어떻게 그럴 수 있는지 소스코드를 분석해 보도록 합시다.
논스의 간격, 중복 논스 및 확인
아래 인용한 본문 내용을 주의 깊게 다시 한 번 읽어 봅니다.
이더리움 네트워크는 논스에 따라 트랜잭션을 순차적으로 처리한다.
여러 트랜잭션을 순서대로 생성하고 그중 하나가 공식적으로 모든 블록에 포함되지 않으면 이후의 모든 트랜잭션이 ‘멈추고’ 누락된 논스를 기다린다. 유효하지 않거나 가스가 모자란 트랜잭션은 논스 시퀀스에 의도치 않게 ‘갭’을 만들 수 있다. 다시 트랜잭션이 계속되게 하려면 누락된 논스가 있는 유효한 트랜잭션을 전송해야 한다. ‘누락’된 논스가 있는 트랜잭션이 네트워크에 의해 유효성이 검증되면, 이후의 논스가 있는 모든 브로드캐스트된 트랜잭션이 차례대로 유효해진다는 점도 똑같이 염두에 두어야 한다. 트랜잭션을 회수(recall)하는 것은 불가능하다!
낮은 가스비로 트랜잭션을 전송한 경우 계속 보류중으로 남을 수 있습니다. 이런 경우 어떤 지갑들은 ‘가속화’라는 기능으로 해당 트랜잭션을 처리하는 경우가 있습니다. 논스 개념을 사용해서 ‘가속화’ 기능은 어떻게 구현하면 될지 생각해 보고 의견을 나눕니다. 보류중인 트랜잭션을 ‘취소’하는 것과 같은 효과를 볼 수 있도록 구현하는 방법도 가능할지 생각해 보고 의견을 나눕니다.
동시 실행, 트랜잭션 생성 및 논스
트랜잭션 생성을 병렬 처리할 경우 논스와 관련된 문제가 발생할 수 있습니다. 어떤 해결책들이 있을지 생각해 보고 의견을 나눠봅니다. 본문 내용을 충실히 반영합니다.
아래 인용한 해결책을 주의를 기울여 다시 한 번 살펴봅니다.
트랜잭션을 생성하고 논스를 할당하지 않는 것이다. 그런 다음, 이 서명되지 않은 트랜잭션들을 한 노드의 대기열에 올려서 이 노드가 트랜잭션을 서명하고 논스를 관리할 수 있게 하는 것이다. 물론, 이것이 프로세스상 병목 지점이 될 수는 있다. 서명하고 논스를 관리하는 작업은 시스템 부하가 늘어남에 따라 혼잡해질 수 있지만, 서명되지 않은 트랜잭션들을 생성하는 작업은 병렬 처리 문제를 고민하지 않아도 된다. 여전히 동시 실행 문제가 남아 있기는 하지만, 크리티컬한 프로세스 부분에서는 더 이상 존재하지 않게 된다.
대부분의 구현 솔루션들이 동시 실행을 피하고 거래소에서 출금 트랜잭션을 처리하는 단일 프로세스를 만드는 것처럼 병목 지점을 어쩔 수 없이 받아들이거나, 독립적으로 작동하는 다수의 출금 담당 핫 월렛을 설치하고 중간중간에 각 지갑의 밸런스를 다시 채워주는 형식으로 해결하게끔 만든다.
마지막 문장이 좀 헷갈릴 수 있는데, 여러 개의 계정을 사용한다는 것, 이 계정에서 출금을 해야 하니 이 계정에 중간 중간 이더를 채워줘야 한다는 것입니다.
트랜잭션 가스
아래 인용한 본문 내용을 다시 한 번 읽습니다.
- 가스는 이더리움의 연료다. 가스는 이더가 아니다. 이더에 대한 자체 환율을 가진 별도의 가상의 화폐다. 이더리움은 가스를 사용하여 트랜잭션이 사용할 수 있는 자원의 양을 제어한다.
- 가상(virtual)의 화폐입니다. 이더 처럼 실제하는 것은 아니고 내부적으로만 사용되는 단위입니다. 이더로 계산되기 위해서는 가스 비용이 설정되어야 하는데 가스 비용은 트랜잭션 발신자가 정합니다.
- 가스는 이더 가치의 급격한 변화와 함께 발생할 수 있는 변동성으로부터 시스템을 보호하고, 가스가 지급하는 다양한 자원(즉, 계산, 메모리 및 저장)의 비용 사이의 중요하고 민감한 비율을 관리하기 위해 가스를 이더와 분리한다.
- 같은 일이라면 사용되는 가스 수량은 동일합니다. 이더의 가격이 높아지면 가스 비를 좀 낮춰 설정하면 되고, 이더 가격이 낮아지면 가스 비를 조금 높여 설정하면 됩니다.
- 가스를 이더와 분리함으로 이더 가격의 변동성으로 부터 시스템을 보호할 수는 있지만, 처리해야 하는 트랜잭션 수가 늘어나고 발신자들이 자신의 트랜잭션을 먼저 처리해야 하는 이유가 분명할 때는 가스 비가 비 상식적으로 높아지기 때문에 가스를 이더와 분리하는 것만으로 시스템을 보호하기는 어렵습니다.
- 트랜잭션 목적지 주소가 컨트랙트인 경우 필요한 가스양을 추정할 수는 있지만 정확하게 결정할 수는 없다.
- 조건문이나 반복문이 있는 경우는 실행해 봐야 실행 경로를 정확히 알 수 있기 때문에 가스비를 정확하게 측정할 수는 없습니다.
ETH Gas Station 에서 전송 속도에 따른 가스 가격을 확인해 봅니다.
이더리움은 가스 가격을 0으로 설정할 수 있습니다. 즉 수수료가 없는 트랜잭션을 만들 수 있다는 것입니다. web3는 여러 블록에 걸친 중간 가격을 계산해서 제공하는 getPrice 함수를 제공합니다.
단순 지급은 하나의 EOA에서 다른 EOA로 이더를 전송하는 트랜잭션을 의미하며, 필요한 가스양은 21,000개의 가스 단위로 고정되어 있습니다.
가스 한도 내에서 트랜잭션은 실행되어야 합니다. 가스 한도가 넘어서면 트랜잭션은 실패하게 됩니다. 실패 해도 수수료는 지불 됩니다. 수수료에서 사용하고 남은 부분은 발신자에게 돌려줍니다.
트랜잭션 수신자
아래 요약한 정도는 기억하고 넘어갑니다.
- to 필드에 트랜잭션 수신자를 지정
- EOA 또는 컨트랙트 주소, 20바이트
- 이더리움 프로토콜은 트랜잭션의 수신자 주소를 검증하지 않음
- 주소 확인은 사용자 인터페이스 수준에서 처리해야 한다고 가정
- EIP-55를 지원하는 주소를 사용하면 유효하지 않은 주소를 검증할 수는 있습니다. 하지만 주소를 원하는 주소가 아닌 다른 주소로 작성했다면(은행 계좌를 다른 계좌로 보내는 것과 같은) 대응할 방법이 없습니다. 블록체인과 같은 탈중앙 시스템에서는 누구도 이에 대해 통제하지 않기 때문에 누구도 책임져 주지 않습니다.
트랜잭션 값과 데이터
아래 인용한 본문 내용을 다시 한 번 읽어 봅니다.
- 트랜잭션의 주요 페이로드는 값과 데이터라는 2개의 필드에 포함된다.
- 페이로드는 실제로 전달되어야 하는 것을 의미한다고 보면 됩니다.
- 이더리움 트랜잭션에서는 실제로 전달해야 하는 것은 이더이거나 컨트랙트 실행을 위한 함수 이름과 인자들입니다. 함수 이름과 인자들은 data라는 이름으로 전달됩니다.
- 페이로드는 실제로 전달되어야 하는 것을 의미한다고 보면 됩니다.
- 값만 있는 트랜잭션은 지급(payment)이다. 데이터만 있는 트랜잭션은 호출(invocation)이다. 값과 데이터 모두를 사용한 트랜잭션은 지급과 호출이다. 값과 데이터가 모두 없는 트랜잭션은 단지 가스 낭비일 뿐이다. 그러나 가능하긴 하다.
목적지 주소(to)로 시작하는 부분의 번역은 이상합니다. 아래 원문 내용을 확인합니다. 원문도 좀 이상하네요. 그래서 번역이 좀 이상해 졌나 봅니다.
- If the destination address (to) is a contract, then the EVM will execute the contract and will attempt to call the function named in the data payload of your transaction. If there is no data in your transaction, the EVM will call a fallback function and, if that function is payable, will execute it to determine what to do next. If there is no code in fallback function, then the effect of the transaction will be to increase the balance of the contract, exactly like a payment to a wallet. If there is no fallback function or non-payable fallback function, then transaction will be reverted.
- 수신 주소가 컨트랙트이면 컨트랙트가 실행될 것이고 data로 작성된 함수가 호출될 것입니다. 트랜잭션에 data가 없으면 fallback 함수가 호출될 것입니다. fallback 함수가 payable이면 이더를 받을 수 있는데 전송된 이더는 컨트랙트의 잔고에 자동적으로(코딩하지 않아도) 더해 집니다. 그렇기 때문에 함수 본문이 없는 fallback 함수를 payable를 작성해 두고 컨트랙트를 수신자로 이더 값을 설정해서 트랜잭션을 보내면, 컨트랙트에 이더를 전송하는 것과 같은 효과를 얻을 수 있습니다.
목적지 주소가 EOA이고, 트랜잭션에 데이터가 포함된 경우 데이터는 어떻게 처리될 것 같은지를 생각해 보고 의견을 나눠봅니다.
아래 요약한 내용을 기억합니다.
- ABI 호환 컨트랙트로 전송된 데이터 페이로드는 다음을 16진수로 시리얼라이즈한 인코딩(32바이트)이다.
- 함수 선택기
- 함수 프로토타입(signature – 함수 이름과 매개변수 타입으로 구성)의 keccak-256 해시의 처음 4바이트, 이 값으로 컨트랙트에서 호출할 함수를 식별함
- 함수 인수
- ABI 사양에 정의된 규칙에 따라 인코딩
- 함수 선택기
특별 트랜잭션: 컨트랙트 생성
아래 인용한 내용을 다시 한 번 읽습니다.
- 컨트랙트 생성 트랜잭션은 제로 어드레스라고 하는 특수 대상 주소로 전송된다. 트랜잭션 to 필드는 0x0 주소를 포함한다. 이더 연소를 원한다면 이 주소를 사용하지 말고 다음 주소를 사용하라. 0x000000000000000000000000000000000000dEaD
- 컨트랙트 생성 트랜잭션은 컨트랙트를 생성할 컴파일된 바이트코드를 포함하는 데이터 페이로드만 포함하면 된다. 새 컨트랙트를 특정 잔액으로 설정해서 시작하려면 값 필드에 이더 금액을 포함할 수 있지만, 이는 전적으로 선택사항이다.
디지털 서명
타원곡선 디지털 서명 알고리즘은 Programming Bitcoin 세미나에서 자세히 다룬 내용으로 복습한다고 생각하면 됩니다. 사실 본문 내용만 보고는 ‘왜’와 ‘어떻게’에 대한 수긍과 이해가 되지 않을 것입니다. 많이 부족합니다.
아래 요약한 내용을 주의 깊게 다시 한 번 읽어 봅니다.
- 서명은 다음과 같이 생성한다.
- Sig = Fsig(Fkeccak256(m), k)
- m은 RLP 인코딩된 트랜잭션, k는 서명 개인 키
- Sig = (r, s)
- Sig = Fsig(Fkeccak256(m), k)
아래 인용한 본문 내용을 주의 깊게 읽어 봅니다. 서명 생성 절차는 기억해둡니다.
- 트랜잭션에 서명하시오라고 할 때 실제로는 RLP 시리얼라이즈된 트랜잭션 데이터의 keccak-256 해시에 서명하시오라는 뜻이다. 다시 말해, 서명은 트랜잭션 자체가 아니라 트랜잭션 데이터의 해시에 적용된다.
- 트랜잭션에 서명하기 위해서 발신자는 다음과 같은 과정을 거쳐야 한다.
- nonce, gasPrice, gasLimit, to, value, data, chainID, 0, 0의 9개 필드를 포함하는 트랜잭션 데이터 구조를 만든다.
- 트랜잭션 데이터 구조의 RLP로 인코딩된 시리얼라이즈된 메시지를 만든다.
- 메시지의 keccak-256 해시를 구한다.
- EOA의 개인키로 해시에 서명하여 ECDSA 서명을 구한다.
- ECDSA 서명의 계산된 v, r, s 값을 트랜잭션에 추가한다.
- v는 27 또는 28 중 하나로 계산되거나, 체인 ID의 두 배에 35 또는 36이 더해져 계산된다.
서명 v의 두 가지 용도를 본문 내용을 충실히 반영해서 설명해 봅니다.
서명 및 전송 분리(오프라인 서명)
트랜잭션 서명과 전송을 분리하는 방법을 설명하고, 이렇게 했을 때의 장점에 대해서 설명합니다. 본문 내용을 충실히 반영합니다.
이후 절 들은 아래 인용한 본문 내용을 다시 한 번 읽고 넘어갑니다.
- 평균적으로 각 이더리움 노드는 적어도 13개의 다른 노드에 대한 연결을 유지한다.
- 생성에서 EOA에 의한 서명, 전파, 채굴까지 완료된 트랜잭션은 싱글톤의 상태를 변경하고 블록체인에서 지울 수 없는 기록을 남긴다. 이러한 변경사항은 이벤트가 포함될 수 있는 트랜잭션 영수증(receipt) 형식으로 트랜잭션과 함께 기록된다.