트랜잭션은 이더리움에 상태 변화를 요청하는 메시지입니다.
예를 들어 ETH를 보내거나, 스마트 컨트랙트를 호출하거나, 새 컨트랙트를 배포하려면 트랜잭션을 만들어 네트워크에 제출해야 합니다.
중요한 점은 트랜잭션이 그냥 “데이터”가 아니라는 것입니다. 트랜잭션은 계정이 서명해서 보낸 요청입니다.
계정
-> 트랜잭션 생성
-> 서명
-> 네트워크에 전파
-> 블록에 포함
-> 실행
-> 상태 변경
트랜잭션은 무엇을 바꾸나
이더리움은 상태를 가지고 있습니다.
상태에는 계정의 잔액, nonce, 컨트랙트 코드, 컨트랙트 저장소 같은 정보가 포함됩니다.
트랜잭션은 이 상태를 바꾸는 입력입니다.
현재 상태
+ 트랜잭션
= 새로운 상태
예를 들어 A가 B에게 1 ETH를 보내면 상태가 바뀝니다.
A의 balance 감소
B의 balance 증가
컨트랙트를 호출하면 더 복잡한 상태 변화가 생길 수 있습니다.
컨트랙트에 저장된 상태값 변경
event log 생성
다른 컨트랙트 호출
그래서 트랜잭션은 “블록에 들어가는 데이터”이면서 동시에 “상태를 바꾸는 명령”이라고 볼 수 있습니다.
기본 구성 요소
Ethereum.org와 JSON-RPC 문서에서 볼 수 있는 트랜잭션 정보에는 이런 값들이 있습니다.
from: 트랜잭션을 보낸 주소to: 받을 주소 또는 호출할 컨트랙트 주소value: 함께 보낼 ETH 양input: 컨트랙트 호출이나 배포에 필요한 데이터nonce: 보낸 계정이 이전에 보낸 트랜잭션 수gas: 트랜잭션 실행에 사용할 수 있는 gas 한도hash: 트랜잭션을 식별하는 해시v,r,s: 서명과 관련된 값
처음에는 이렇게 단순하게 생각해도 됩니다.
누가 보냈는가: from
어디로 보내는가: to
얼마를 보내는가: value
무슨 데이터를 함께 보내는가: input
몇 번째 요청인가: nonce
얼마까지 실행 비용을 허용하는가: gas
정말 보낸 사람이 서명했는가: signature
서명은 왜 필요할까
트랜잭션은 계정의 자산이나 상태를 바꾸는 요청입니다. 그래서 아무나 남의 계정에서 트랜잭션을 보낼 수 있으면 안 됩니다.
이더리움에서는 사용자가 개인키로 관리하는 계정이 트랜잭션에 서명합니다.
이 계정을 정확히는 EOA라고 부르지만, 아직은 용어를 외울 필요는 없습니다. 뒤의 Account 모델 글에서 EOA와 Contract Account를 따로 정리합니다.
검증자는 서명을 이용해 이 트랜잭션이 실제로 해당 계정에서 나온 것인지 확인할 수 있습니다.
트랜잭션 내용
+ 서명
-> 보낸 주소 확인
그래서 트랜잭션에는 보통 from이 표시되지만, 실제로 중요한 것은 서명입니다. 서명을 검증하면 어떤 계정이 이 트랜잭션을 승인했는지 알 수 있습니다.
nonce는 왜 필요할까
nonce는 계정이 보낸 트랜잭션의 순서를 나타내는 값입니다.
Ethereum.org의 accounts 문서는 nonce를 계정이 보낸 트랜잭션 수를 나타내는 counter로 설명합니다. 같은 계정에서 같은 nonce를 가진 트랜잭션은 하나만 실행될 수 있습니다.
여기서 헷갈리기 쉬운 점이 있습니다. 계정의 현재 nonce는 “다음에 사용할 번호”처럼 보면 됩니다.
nonce는 0부터 시작합니다.
1번째 트랜잭션 -> nonce 0
2번째 트랜잭션 -> nonce 1
3번째 트랜잭션 -> nonce 2
...
8번째 트랜잭션 -> nonce 7
그래서 A 계정의 현재 nonce가 7이라는 말은, 이미 성공한 트랜잭션이 7개 있고 다음 트랜잭션이 nonce 7을 사용한다는 뜻입니다.
A의 현재 nonce: 7
다음 트랜잭션 nonce: 7
트랜잭션 실행 후 A의 nonce: 8
이 값은 같은 트랜잭션이 반복 실행되는 것을 막는 데 중요합니다.
예를 들어 A가 B에게 1 ETH를 보내는 트랜잭션이 있다고 해보겠습니다. 누군가 이 서명된 트랜잭션을 복사해서 계속 네트워크에 다시 보낼 수 있다면 문제가 됩니다.
하지만 nonce가 있으면 이미 실행된 nonce는 다시 실행될 수 없습니다.
nonce 7 트랜잭션 실행됨
-> A의 nonce는 8이 됨
-> 같은 nonce 7 트랜잭션은 다시 실행될 수 없음
그래서 nonce는 트랜잭션 순서를 정하고, 같은 서명 요청이 반복 실행되는 것을 막는 역할을 합니다.
to, value, input
트랜잭션이 무엇을 하는지는 to, value, input을 보면 대략 알 수 있습니다.
단순 ETH 전송은 보통 이런 모양입니다.
to: 받는 주소
value: 보낼 ETH
input: 비어 있음
컨트랙트 호출은 보통 input에 데이터가 들어갑니다.
to: 컨트랙트 주소
value: 함께 보낼 ETH, 없을 수도 있음
input: 호출할 함수와 인자 데이터
컨트랙트 배포는 to가 비어 있는 형태로 나타날 수 있습니다. JSON-RPC 문서도 contract creation transaction에서는 to가 null일 수 있다고 설명합니다.
to: null
input: 배포할 컨트랙트 코드와 생성자 데이터
처음에는 이렇게 구분하면 충분합니다.
ETH 전송: value가 중요
컨트랙트 호출: input이 중요
컨트랙트 배포: to가 비어 있고 input에 코드가 들어감
gas는 실행 비용의 한도다
트랜잭션을 실행하려면 gas가 필요합니다.
Ethereum.org의 트랜잭션 문서는 단순 ETH 전송에 21,000 gas가 필요하다고 설명합니다.
여기서 중요한 것은 gas를 “수수료”라고만 외우는 것이 아니라, 실행에 사용할 수 있는 계산 자원의 한도로 보는 것입니다.
gas
= 이 트랜잭션 실행에 사용할 수 있는 계산량의 한도
컨트랙트 호출은 단순 ETH 전송보다 더 많은 연산을 할 수 있으므로 gas가 더 많이 필요할 수 있습니다.
gas와 수수료 계산은 별도 글에서 자세히 다루는 것이 좋습니다. 여기서는 트랜잭션이 실행 비용 한도를 함께 담고 있다는 점만 기억합니다.
트랜잭션이 블록에 포함되면
트랜잭션은 네트워크에 전파된 뒤 바로 확정되는 것이 아닙니다.
대략 이런 과정을 거칩니다.
트랜잭션 생성
-> 서명
-> 네트워크 전파
-> 블록에 포함
-> 실행
-> receipt 생성
-> 상태 변경 결과가 stateRoot에 반영
블록 헤더 글에서 본 것처럼, 블록 헤더에는 여러 root가 들어 있습니다.
트랜잭션은 transactionsRoot와 연결됩니다.
트랜잭션 목록
-> transactionsRoot
트랜잭션 실행 결과는 receiptsRoot와 연결됩니다.
receipt 목록
-> receiptsRoot
트랜잭션을 실행한 뒤의 상태는 stateRoot와 연결됩니다.
상태 변화 결과
-> stateRoot
그래서 트랜잭션은 블록 body에 들어가는 데이터이면서, 블록 헤더의 여러 root를 계산하는 출발점이 됩니다.
정리
트랜잭션은 이더리움 상태를 바꾸기 위해 계정이 서명해서 보내는 요청입니다.
오늘 기준으로는 이렇게 정리합니다.
트랜잭션은 누가, 어디에, 어떤 값과 데이터를 보내며, 얼마의 실행 비용을 허용하는지 담은 서명된 상태 변경 요청이다.
참고 자료
- Ethereum.org, Transactions: https://ethereum.org/developers/docs/transactions/
- Ethereum.org, Accounts: https://ethereum.org/developers/docs/accounts/
- Ethereum.org, JSON-RPC API: https://ethereum.org/developers/docs/apis/json-rpc/