1 Transaction
- 트랜잭션은 외부 소유 계정(EOA)에 의해 서명된 메시지 인데, 이더리움 네트워크에 의해 전송되고 이더리움 블록체인에 기록된다.
- 트랜잭션은 EVM에서 상태 변경을 유발하거나 컨트랙트를 실행할 수 있는 유일한 방법이다.
- 컨트랙트는 독자적으로 실행되지 않는다. 또한 이더리움도 자율적으로 실행되지 않는다. 모든 것은 트랜잭션으로부터 시작된다.
- 이더리움은 글로벌 싱글톤 상태 머신이며, 트랜잭션은 이 상태 머신을 움직여서 상태를 변경할 수 있도록 만든다.
2 Transaction의 구조
- 트랜잭션은 아래의 필드를 포함하는 RLP(Recursive Lenght Prefix) 인코딩 체계를 사용해서 직렬화된 바이너리 메시지다.
- 이더리움의 모든 숫자는 8비트 배수 길이의 빅엔디안 정수로 인코딩 된다.
2.1 Nonce
-
발신 EOA에 의해 발행되어 메시지 재사용을 방지하는 데 사용되는 일련번호
- 블록의 논스와 트랜잭션 논스는 다른 것
-
해당 주소에서 보낸 트랜잭션 건수와 동일한 스칼라 값
-
컨트랙트 생성 트랜잭션도 포함
-
EOA가 트랜잭션을 보내는 경우 트랜잭션의 nonce는 현재 EOA의 nonce값이 된다.
2.1.1 논스 추적
- 논스는 각 계정에서 발생한 확인된 트랜잭션 건수에 대한 최신 통계
- 논스는 0부터 시작한다.
- 논스는 블록체인 계정 상태로 저장되지 않기 때문에 논스를 구하려면 해당 주소로 전송되어 확정된 트랜잭션 개수를 세어서 구해야 한다.
- 트랜잭션 개수는
web3.eth.getTransactionCount
과 같이 web3를 사용해서 구할 수 있다
web3.eth.getTransactionCount 사용시 주의 사항
web3.eth.getTransactionCount
은 확정된 트랜잭션 개수만 세고 보류 중인 트랜잭션을 세지 않는다- 사용자가 빠르게 트랜잭션을 생성한다면 중복된 논스가 발생할 수 있다.
2.1.2 논스의 간격, 중복 논스 및 확인
- 이더리움 네트워크는 논스에 따라 트랜잭션을 순차적으로 처리한다.
- 여러 트랜잭션을 순서대로 생성하고 그중 하나가 공식적으로 모든 블록에 포함되지 않으면 이후의 모든 트랜잭션이 ‘멈추고’ 누락된 논스를 기다린다.
- 논스가 0인 트랜잭션을 전송한 다음 논스가 2인 트랜잭션을 전송하면 두 번째 트랜잭션은 어떤 블록에도 포함되지 않는다.
- 논스 1인 트랜잭션이 나타날 때 까지 논스 2인 트랜잭션은 멤풀에 저장된다.
- 논스가 같지만 수신자나 값이 다른 2개의 트랜잭션이 발생하면 하나는 확정되고 하나는 거부된다.
- 어떤 트랜잭션이 확정되는지는 첫 유효 노드에 도달하는 순서에 따라 결정된다.
- 유효하지 않거나 가스가 모자란 트랜잭션은 논스 시퀀스에 의 도치 않게 ‘갭’을 만들 수 있다.
- 다시 트랜잭션이 계속되게 하려면 누락된 논스가 있는 유효한 트랜잭션을 전송해야 한다.
- ‘누락’된 논스가 있는 트랜잭션이 네트워크에 의해 유효성이 검증되면, 이후의 논스가 있는 모든 브로드캐스트된 트랜잭션이 차례대로 유효해진다는 점도 똑같이 염두에 두어야 한다.
- 트랜잭션을 회수(recall)하는 것은 불가능하다!
2.1.3 동시 실행, 트랜잭션 생성 및 논스
- 동일한 주소에서 트랜잭션을 생성하는 여러 개의 독립적인 지갑 애플리케이션이 있을 때 동시에 출금을 수행하면 동시 실행 문제가 발생할 수 있다.
해결책
- 단일 컴퓨터를 사용하여 트랜잭션에 서명하는 컴퓨터에 선착순으로 논스를 할당하는 방법
- 이 단일 컴퓨터가 단일 실패 지점이 된다.
- 즉 이 컴퓨터가 장애가 나면 해당 주소에서 출금을 할 수 없다.
- 트랜잭션을 생성하고 논스를 할당하지 않는 방법
- 서명되지 않은 이 트랜잭션들을 한 노드의 대기열에 올려서 이 노드가 트랜잭션을 서명하고 논스를 관리할 수 있게 하는 것
- 이 노드가 단일 실패 지점이 된다.
- 서명되지 않은 트랜잭션 생성 작업을 병렬 처리할 수 있다.
구현 솔루션
- 대부분의 구현 솔루션들이 동시 실행을 피하고 거래소에서 출금 트랜잭션을 처리하는 단일 프로세스를 만드는 것처럼 병목 지점을 어쩔 수 없이 받아드린다.
- 또는 독립적으로 작동하는 다수의 출금 담당 핫 월렛을 설치하고 중간중간에 각 지갑의 밸런스를 다시 채워주는 형식으로 해결하게끔 만든다.
2.2 gasPrice
- 트랜잭션의 발신자가 지급하는 가스의 가격(웨이)
Gas
가스란 이더리움의 연료이다
- EVM의 각 명령어는 가스 단위로 미리 정해진 비용이 있다.
- 거래 데이터의 1바이트당 5가스의 수수료가 발생한다.
가스는 이더가 아니라 이더에 대한 자체 환율을 가진 가상 화폐이다
가스의 용도
- 가스는 트랜잭션이 사용할 수 있는 자원의 양을 제어한다.
- 튜링 완전 개발 모델이 DoS 공격이나 실수로 막대한 자원을 소모하는 트랜잭션을 피하기 위해 미터링이 필요하다.
- 이더 가치의 변동성으로 부터 시스템을 보호한다.
- 가스는 네트워크의 과부하를 막으며, 이더리움 플랫폼이 계속 운영되도록 하는 인센티브 역할을 한다
Gas Price 설정하기
- 트랜잭션 생성자는 원하는 가스 가격을 지정할 수 있다.
- 예를 들어 ‘가스 당 3Gwei를 지불할 용의가 있다.’라고 설정했을 때, 트랜잭션이 1,000,000가스를 소비하 고 가스 가격을 3Gwei로 설정하면 해당 트랜잭션에 대한 수수료로 3,000,000Gwei를 지불하게 된다.
- 대부분의 채굴자들은 가스 가격이 높은 트랜잭션을 선택하여 블록에 포함시키기 때문에 설정한 가스 가격이 높을수록 트랜잭션은 더 빨리 처리된다.
- 가격을 낮게 책정해도 결국 트랜잭션이 블록에 포함되기는 하지만 그 대기 시간이 상당히 길어질 가능성도 있다.
- 가스 가격은 일반 사람들에 의하여 무작위로 책정되지는 않고 가스 가격을 측정해주는 사이트가 있다.
- 이더리움 가스스테이션이라는 사이트인데, 이곳에서 적당한 가스 가격을 선택할 수 있다.
- 사용되는 가스의 평균값이라고 보면 된다.
- 빠른 처리를 원할 경우 여기 나온 금액보다 높은 가격을 측정하면 된다.
- web3는 여러 블록에 걸친 중간 가격을 계산해서 제공하는 getPrice 함수를 제공합니다.
2.3 gasLimit
- 이 트랜잭션을 위해 구입할 가스의 최대량
- 가스 한도는 작업 중단 시점을 보장함으로써 무제한으로 이더를 사용하는 것을 방지할 수 있다.
- 사용자는 트랜잭션을 실행하기 위해 사용할 가스의 최대 금액을 나타내는 가스 한도(Gas Limit)를 설정한다.
- 가스 한도는 요청하는 작업량의 추측이다. 하지만 추측은 쉬운 일이 아니다.
- 일반적으로 21,000가스의 한도는 대부분의 거래를 만족한다고 알려져 있다.
- 한도가 낮으면 작업이 완료되지 않고 거래는 실패하며 그 시점까지 사용된 이더가 손실된다.
- 한도를 너무 높게 설정하여 한도 전에 작업이 끝나더라도 작업에 사용되지 않은 모든 이더는 다시 되돌려 받을 수 있다
2.4 to(수신자)
- 목적지 이더리움 주소(20바이트)
- 주소는 EOA 또는 CA의 주소
- 이더리음은 이 필드를 검증하지 않는다
- 트랜잭션을 없는 주소로 보내면 이더가 연소되어 영구적으로 사용할 수 없다
- 이더 연소를 위한 주소:
0x000000000000000000000000000000000000dEaD
- 이더 연소를 위한 주소:
2.5 value(값)
- 목적지에 보낼 이더의 양
- 컨트랙트 생성의 경우 새로 생성되는 컨트랙트 계정의 잔액에 예치된다.
2.6 v, r, s
- EOA의 ECDSA 디지털 서명의 세 가지 구성요소
- 트랜잭션의 서명에 해당하는 값들이며
- 트랜잭션 필드에 트랜잭션의 발신자 정보가 없는데 EOA의 공개키를 ECDSA 서명의 v, r, s 구성요소로 부터 알아낼 수 있기 때문이다.
- 트랜잭션에 발신자 필드가 보인다면 소프트웨어에 의해 추가된 것
2.7 data
- 가변 길이 바이너리 데이터 페이로드
- 컨트랙트를 실행할 때 컨트랙트의 어떤 함수를 어떤 인수들을 가지고 실행할 것인지를 표현할 때 사용한다.
- 상식적으로 값(value)를 보내기위한 트랜잭션이라면 데이터 필드가 필요없다.
3 Transaction Value and Data
-
트랜잭션의 주요 페이로드는 값과 데이터라는 2개의 필드에 포함된다.
- 페이로드는 실제로 전달되어야 하는 것을 의미한다고 보면 된다.
-
값만 있는 트랜잭션은 지급이고 데이터만 있는 트랜잭션은 호출이다.
-
값과 데이터를 같이 사용하면 지급과 동시에 호출이다.
-
값과 데이터 둘다 사용하지 않는 것이 가능하나 가스 낭비다.
3.1 EOA 및 CA에 값 전달
- 값을 포함하는 트랜잭션을 구성하면 지급에 해당한다.
EOA에 값 전달
- 이러리움은 상태 변경을 기록하여 주소 잔액에 보낸 값을 추가한다
CA에 값 전달
- 목적지 주소(to)가 컨트랙트라면 EVM은 컨트랙트를 실행하고 트랜잭션 데이터 페이로드에 지정된 함수를 호출한다.
- 데이터가 없으면 EVM은 풀백 함수를 호출한다.
- 풀백 함수가 없다면 트랜잭션의 효과는 지갑에 지급하는 것과 마찬가지로 컨트랙트의 잔액을 늘린다.
Fallback Function
- 트랜잭션에 데이터가 없거나 선언된 함수 이름이 없는 경우 호출되는 컨트랙트의 기본 함수입니다.
- 컨트랙트는 한 개의 Fallback Function을 가질 수 있다.
- Fallback Function은 이름이 없고 파라미터와 반환 값이 없다.
contract Faucet {
function () public payable {}
// Give out ether to anyone who asks
function withdraw(uint withdraw_amount) public {
// Limit withdrawal amount
require(withdraw_amount <= 100000000000000000);
// Send the amount to the address that requested it
msg.sender.transfer(withdraw_amount);
}
}
3.1 EOA 및 CA에 데이터 페이로드 전달
- 트랜잭션에 데이터가 포함되어 있으면 받는 주소는 컨트랙트 주소일 가능성이 크다
- 데이터가 포함된 트랜잭션을 EOA에게 보낼 수 없는 것은 아니다
- 이러한 경우 데이터의 해석은 EOA에 접근하는데 사용하는 지갑에 달려있다
- 대부분의 지갑은 트랜잭션에서 수신된 모든 데이터를 무시한다
- EOA의 데이터 페이로드 해석은 이더리움 합의 규칙이 적용되지 않는다
CA에 데이터 페이로드 전달
- EVM에 의해 컨트랙트 호출로서 해석된다.
- 명명된 함수를 호출하고 인코딩된 인수를 함수에 전달한다
- 데이터 페이로드(32 바이트)는 16진수로 시리얼라이즈한 인코딩이다
- 페이로드는 함수 선택기와 함수 인수로 구성된다.
함수 선택기
- 함수 프로토타입의 Keccak-256해시의 처음 4바이트
function withdraw(uint withdraw_amount) public {}
- 이 함수의 함수 프로토타입은
withdraw(uint256)
이다. web3.sha3("withdraw(uint256)");
- 함수 프로토 타입의 Keccak-256해시 -> '0x2e1a7d4d13322e...'
- 따라서 함수 선택기는 해시의 처음 4바이트
0x2e1a7d4d
이다
- 이 함수의 함수 프로토타입은
함수 인수
- 함수 인수는 ABI 사양에 정의된 다양한 기본 유형에 대한 규칙에 따라 인코딩된다
- withdraw_amount 인수로 0.01을 전달해보자
withdraw_amount = web3.toWei(0.01, "ether");
- 16진수로 시리얼라이즈된 부호 없는 빅엔디안 256비트 정수로 인코딩하여 웨이로 표현한 것
- '10000000000000000'
withdraw_amount_hex = web3.toHex(withdraw_amount);
- '0x2386f26fc10000'
트랜잭션의 데이터 페이로드
- 이제 함수 선택기를 함수 인수에 추가한다
0x2e1a7d4d0000000000000...2386f26fc10000
(32바이트)- 이것이 트랜잭션의 데이터 페이로드이다
- withdraw함수를 호출하고 0.01 이더를 withdraw_amount로 요청함
4 컨트랙트 생성 트랜잭션
- 컨트랙트 생성 트랜잭션은의 to 필드가 0x0인 트랜잭션을 말한다.
- 0x0을 제로 어드레스라고 부르며 컨트랙트 작성이라는 특별한 의미로 사용된다
- 컨트랙트 생성 트랜잭션은 컨트랙트를 생성할 컴파일된 바이트 코드를 데이터 페이로드에 포함하면 된다.
4.1 컴파일
- 데이터에 담기위해 솔리디티로 작성한 컨트랙트를 컴파일한다.
$ solc --bin Faucet.sol
Binary:
6060604052341561000f57600080fd5b60e58061001d6000396000f30060606040526004361060...
4.2 컨트랙트 생성 트랜잭션 생성
- to: 0x0(트랜잭션 생성 주소)
- data: 컴파일된 코드
> src = web3.eth.accounts[0];
> faucet_code = \
"0x6060604052341561000f57600080fd5b60e58061001d6000396000f300606...f0029";
> web3.eth.sendTransaction({from: src, to: 0, data: faucet_code, \
gas: 113558, gasPrice: 200000000000});
"0x7bcc327ae5d369f75b98c0d59037eec41d44dfae75447fd753d9f2db9439124b"
4.3 receipt 확인
- 컨트랙트 생성 트랜잭션의 트랜잭션 해시로 receipt을 확인한다.
- receipt에는 생성된 컨트랙트의 주소가 포함되어 있다.
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
}