[마스터링 이더리움] 6장 : 트랜잭션

728x90

 

트랜잭션은 외부 소유 계정 (EOA, Externally Owned Account)에 의해 서명된 메시지이고,

이더리움 네트워크에 의해 전송되고 이더리움 블록체인에 기록된다.

다른 관점에서 보면, 트랜잭션은 EVM에서 상태 변경을 유발하거나 컨트랙트를 실행할 수 있는 유일한 방법이라는 것이다.

 

이더리움은 글로벌 싱글톤 상태 머신이며,

트랜잭션은 이 상태 머신을 움직여서 상태를 변경할 수 있도록 만든다.

즉, 모든 것은 트랜잭션으로부터 시작된다. (컨트랙트나 이더리움은 자율적으로 실행되지 않음)

 

 

트랜잭션 구조

트랜잭션은 기본적으로 이더리움 네트워크에서 시리얼라이즈되어 전송된다.

시리얼라이즈된 트랜잭션을 수신하는 클라이언트와 어플리케이션은 자체 내부 데이터 구조를 사용하여 트랜잭션을 메모리에 저장하고,

이 때 네트워크에서 시리얼라이즈된 트랜잭션 자체에는 존재하지 않는 메타데이터가 포함된다.

네트워크 시리얼라이제이션은 트랜잭션의 유일한 표준 형식이다.

  • 트랜잭션은 다음 데이터를 포함하는 시리얼라이즈된 바이너리 메시지이다.
    • 논스(Nonce) : 발신 EOA에 의해 발행되어 메시지 재사용을 방지하는 데 사용되는 일련번호
    • 가스 가격(gas price) : 발신자가 지급하는 가스의 가격 (wei 단위)
    • 가스 한도(gas limit) : 이 트랜잭션을 위해 구입할 가스의 최대량
    • 수신자(recipient) : 목적지 이더리움 주소
    • 값(value) : 목적지에 보낼 이더의 양
    • 데이터(data) : 가변 길이 바이너리 데이터 페이로드
    • v, r, s : EOS의 ECDSA 디지털 서명의 세 가지 구성요소

트랜잭션 메시지의 구조는 이더리움에서 간단하고 완벽한 바이트 시리얼라이제이션을 위해

특별히 만들어진 RLP(Recursive Length Prefix) 인코딩 체계를 사용하여 시리얼라이즈된다.

 

이더리움의 모든 숫자는 8비트 배수 길이의 빅엔디안 정수로 인코딩된다.

 

 

트랜잭션 논스

✅ 논스란?

해당 주소에서 보낸 트랜잭션 건수 또는 연결된 코드가 있는 계정의 경우,

이 계정에서 만든 컨트랙트 생성 건수를 나타내는 스칼라 값으로 0부터 시작하는 카운터

 

발신 주소의 속성이며 단지 발신 주소의 컨텍스트 안에서만 의미를 갖음

명시적으로 블록체인 계정 상태에 저장되지 않고, 해당 주소에서 발생한 확인된 트랜잭션 건수를 세어서 동적으로 계산

 

💡 왜 논스가 중요한가?
  1. 트랜잭션 생성 순서대로 포함된다는 점 (사용성)
  2. 복제 방지 기능

 

⏩️ 비트코인 프로토콜의 UTXO메커니즘과 달리, 계정 기반 프로토콜은 논스를 사용하는 것이 필수적

 

 

1번에 대한 예시)

만약에 두 가지 트랜잭션을 생성한다고 가정해보자.

a 트랜잭션 : 6이더를 지급하는 트랜잭션

b 트랜잭션 : 8이더를 지급하는 트랜잭션

이 때 중요도는 a > b 이다.

현재 내 지갑에는 10이더밖에 있지 않아 하나의 트랜잭션만 성공한다.

a가 더 중요하기 때문에 a를 먼저 서명하고 전파한 뒤, b를 서명하고 전파한다.

하지만 P2P 네트워크 특성상, 노드는 어떤 순서로든 트랜잭션을 수신할 수 있다.

즉, a를 먼저 보내도 네트워크 상황에 따라 b가 먼저 올 수도 있는 상황이라는 것.

이럴 때, 논스로 생성 순서를 표시할 수 있다는 것이다

a가 갖는 논스값은 3이라고 하면, b가 갖는 논스값은 4가 된다.

따라서 트랜잭션 b는 0부터 3까지의 논스가 있는 트랜잭션이 처리될 때까지 무시된다.

 

 

2번에 대한 예시)

내 계정에 100이더가 있다고 가정해보자.

내가 사고 싶은 물건이 2이더이고, 나는 판매자에게 2이더 지급을 하면 물건을 구입할 수 있다.

내 계정에서 판매자의 계정으로 2이더를 전송하는 트랜잭션에 서명을 한 다음,

트랜잭션을 이더리움 네트워크로 전파하여 검증한 후에 블록체인에 저장한다.

이 때, 논스값이 없다면 향후 2이더를 판매자의 계정에서 내 계정으로 보내는 트랜잭션은 방금 보낸 트랜잭션과 정확히 일치할 것이다.

즉, 이더리움 네트워크에서 해당 트랜잭션을 복사하고 붙여넣고 네트워크로 다시 보내는 방식을 통해 내 지갑에 이더가 소진될 때까지 계속 트랜잭션을 반복해서 ‘재실행’할 수 있다.

하지만 트랜잭션 데이터에 포함된 논스 값을 활용하면

동일한 수신자 주소에 동일한 양의 이더를 보내더라도 각각은 개별 트랜잭션으로 고유하다.

이렇게 트랜잭션의 일부로 논스를 증가함으로써 복제 행위를 막을 수 있다.

 

 

 

 

 

논스 추적

논스란?

각 계정에서 발생한 확인된(즉, 체인상에 있는) 트랜잭션 건수에 대한 최신 통계

 

우리의 지갑은 이 지갑에서 관리하는 각 주소에 대한 논스를 추적

❗️ web3.eth.getTransactionCount 사용 시
대기 중인 트랜잭션 건수 계산을 위해 getTransactionCount함수를 사용할 때,
여러 개의 트랜잭션을 연속해서 보내는 경우 그 결과가 제대로 반환되지 않을 수 있음

✅ 대기 중인 트랜잭션 개수와 확인된 트랜잭션 개수(미해결 트랜잭션이 모두 확인됨)가 동일할 때만 신뢰 가능


✅ Parity의 JSON RPC 인터페이스의 parity_nextNonce 함수는
트랜잭션에서 사용해야 하는 다음 논스를 반환해주기 때문에 빠르게 연속적으로 만들더라도 올바른 논스 값을 계산해서 제공해줌

 

 

논스의 간격, 중복 논스 및 확인

이더리움 네트워크는 논스에 따라 트랜잭션을 순차적으로 처리한다.

순서에 맞지 않는 논스값을 가진 트랜잭션은 어떤 블록에도 포함되지 않고,

누락된 논스가 나타날 때까지 기다리는 동안 해당 트랜잭션은 멤풀(mempool)에 저장된다.

이 후, 누락된 논스의 트랜잭션을 전송하면, 두 트랜잭션이 처리되고 블록에 포함된다.

 

✅  즉, 여러 트랜잭션을 순서대로 생성하고 그 중 하나가 공식적으로 모든 블록에 포함되지 않으면

이후의 모든 트랜잭션이 '멈추고' 누락된 논스를 기다린다.

 

트랜잭션을 ‘회수(recall)’하는 것은 불가능하다.

 

반면, 예를 들어 논스가 같지만 수신자나 값이 다른 2개의 트랜잭션을 전송하는 것과 같은

논스의 중복이 일어나면,

그 중 하나가 확정되고 하나는 거부된다.

어떤 트랜잭션이 확정되는 지는 그 트랜잭션이 첫 유효 노드에 도달하는 순서에 따라 결정된다.

즉, 이는 무작위적이다.

 

 

동시 실행(Concurrency), 트랜잭션 생성 및 논스

이더리움같은 탈중앙화 분산시스템에서 concurrency는 특히 복잡하고 중요한 문제이다.

 

✏️ Concurrency 문제 : 멀티스레딩, 멀티프로세싱, 분산 시스템 내에서 발생할 수 있음

이더리움은 정의상 작업(노드, 클라이언트, 댑)의 동시 실행을 허용하지만 합의를 통해 싱글톤 상태를 강제하는 시스템

 

< 트랜잭션 생성 방법 > 어떻게 병렬처리를 하는 가?

 

1️⃣ 단일 컴퓨터를 사용하는 경우 - single point of failure 발생

 

단일 실패 지점(single point failure)란,

분산시스템에서 한 대가 모든 수행을 할 때, 해당 컴퓨터가 오류가 나면 단일 실패 지점이 되어 모든 수행이 멈추게 됨

(특히, 논스에서 아까와 같은 오류가 생겼을 때, 후속 트랜잭션이 모두 중단된다)

 

2️⃣ 트랜잭션을 생성하고 논스를 할당하는 않는 것

 

(이렇게하면 트랜잭션은 서명되지 않는 상태로 남는다.

논스는 트랜잭션 데이터의 필수 부분이므로 트랜잭션을 인증하는 디지털 서명에 포함시켜야 한다.)

그런 다음, 이 서명되지 않은 트랜잭션들을 한 노드의 대기열에 올려서 이 노드가 트랜잭션을 서명하고 논스를 관리할 수 있게 하는 것

이 지점이 병목 지점이 될 순 있지만 병렬 처리 문제를 고민하지 않아도 됨

여전히 동시실행 문제가 존재하긴 하지만, 크리티컬한 프로세스 부분에서는 더 이상 존재하지 않음

 

 

✅ 대부분의 구현 솔루션들이 동시 실행을 피하고 거래소에서 출금 트랜잭션을 처리하는 단일 프로세스를 만드는 것처럼

병목 지점을 어쩔 수 없이 받아들이거나,

독립적으로 작동하는 다수의 출금 담당 핫 월렛을 설치하고 중간중간에 각 지갑의 밸런스를 다시 채워주는 형식으로 해결

 

 

트랜잭션 가스

가스는 이더리움의 연료로, 이더리움은 가스를 이용해 트랜잭션이 사용할 수 있는 자원의 양을 제어

튜링완전 계산 모델은 DoS 공격이나 실수로 막대한 자원을 소모하는 트랜잭션을 피하기 위해

특정한 형태의 metering이 필요하다.

*이더리움은 튜링완전

 

가스는

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

가스와 이더를 분리

 

 

ethgasstation에서 이더리움 메인 네트워크의 가스 및 기타 관련된 가스 측정 항목의 현재 가격에 대한 정보를 제공했으나

2023년 7월 1일부로 서비스 종료

 

✅ gasPrice

지갑은 신속한 트랜잭션 컨펌을 위해 gasPrice를 조정할 수 있다.

gasPrice가 높을 수록 트랜잭션이 더 빨리 컨펌되고, 낮을 수록 트랜잭션의 우선순위가 낮아진다.

 

web3 인터페이스는 여러 블록에 걸친 중간 가격을 계산해 getPrice를 제안하는 기능을 제공

(web3.eth.getGasPrice)

 

 

✅ gasLimit

트랜잭션을 완료하기 위해 트랜잭션을 전송하는 사람이 기꺼이 사용할 수 있는(구매할 수 있는) 최대 가스 단위 수

일반적으로, 단순 지급의 경우, EOA에서 EOA로 이더를 전송하는 트랜잭션을 의미하는 데,

이 경우 필요한 가스양은 21000개의 가스 단위로 고정된다.

즉, 최종적으로 내가 소비하는 이더의 양은 gasPrice*21000 가 되는 것이다.

 

 

트랜잭션 목적지 주소가 CA인 경우,

필요한 가스양을 추정할 수 있지만 정확하게 결정할 수는 없다.

이는 컨트랙트가 각기 다른 경로로 이어지는 조건을 가질 수 있어 총 가스 비용이 다를 수 있기 때문이다.

즉, 컨트랙트가 통제할 수 없는 조건에 의해 단순 계산으로 끝날 수도 있고, 더 복잡한 계산을 수행해야 할 수도 있다.

 

트랜잭션을 전송할 때 첫 번째 유효성 확인 단계 중 하나는

그것이 발생된 계정이 (가스 가격*가스 요금)을 지급할 만큼 충분한 이더를 갖고 있는 지 확인하는 것 !

 

 

트랜잭션 수신자

to 필드에 지정되는 트랜잭션 수신자는 20바이트 이더리움 주소를 포함하고, 이는 EOA와 CA 둘 다 가능

이더리움은 이 필드를 검증하지 않고 모든 20바이트 값은 유효한 것으로 간주한다.

(개인키가 없거나, 상응하는 컨트랙트가 없는 주소의 경우에도 유효한 것으로 간주)

⏩️ 유효성 검사는 사용자 인터페이스 수준에서 수행되어야 한다. (구리다)

 

 

이더를 일부러 연소시키기도 하는 데 이에는 나름 정당한 이유가 있을 수 있다

  1. 지급 채널 및 기타 스마트 컨트랙트에서의 부정 행위를 저지하는 것
  2. 이더의 양이 유한하므로 이더를 연소시키면서 모든 이더 보유자에게 연소된 값을 효과적으로 분배하는 것

 

트랜잭션 값과 데이터

트랜잭션의 주요 ‘페이로드(payload)’는 값(value)과 데이터(data)라는 2개의 필드에 포함된다.

 

트랜잭션은 값과 페이로드 중 둘 다 있거나, 하나만 있거나 아예 없어도 유효하다.

즉, 아래와 같은 경우 4가지로 분류된다.

 

1️⃣ 값(value)만 있는 트랜잭션: 지급(payment)

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

 

2️⃣ 데이터(data)만 있는 트랜잭션 : 호출(invocation)

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

 

3️⃣ 값(value)과 데이터(data) 모두를 사용한 트랜잭션: 지급과 호출

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

4️⃣ 모두 없는 트랜잭션: 가스 낭비

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

 

 

 

EOA 및 컨트랙트(CA)에 값 전달

값(data)을 포함하는 이더리움 트랜잭션을 구성하면 지급과 동일하다.

이러한 트랜잭션은 대상 주소가 컨트랙트인지 아닌 지 여부에 따라 다르게 작동한다.

 

 

1️⃣ EOA나 블록체인의 컨트랙트에 표시되지 않은 주소의 경우,

이더리움은 상태 변경을 기록하여 주소 잔액에 보낸 값을 추가한다.

이전에 주소가 표시되지 않은 경우, 클라이언트 내부 상태 표현에 추가되고 잔액은 지급 금액으로 초기화된다.

 

 

2️⃣ 목적지 주소(to)가 컨트랙트라면

EVM은 컨트랙트를 실행하고 트랜잭션의 데이터(data) 페이로드에 지정된 함수를 호출하려고 시도한다.

 

만약에 지정된 함수가 없다면(즉, 트랜잭션에 data 값이 없다면)?

EVM은 폴백(fallback) 함수를 호출하고,

해당 함수가 지급 가능하다면 다음에 수행할 작업을 결정하기 위해 함수를 실행한다.

만약 컨트랙트에 폴백 함수가 없다면 컨트랙트의 잔액을 늘린다. (컨트랙트를 하나의 지갑처럼 생각)

컨트랙트는 함수가 호출될 때 또는 코딩된 조건에 따라 예외를 발생시켜 입금을 거부할 수 있다.

 

 

 

EOA 또는 컨트랙트에 데이터 페이로드 전달

트랜잭션에 데이터가 포함되어 있으면 받는 주소는 CA가 될 가능성이 크다.

대부분 EOA에 접근하는 데 사용하는 지갑에 data를 어떻게 사용할 것인가가 달려있는데

대부분 지갑은 자신이 제어하는 EOA에 대한 트랜잭션에서 수신된 모든 데이터를 무시한다.

트랜잭션은 사용자 지갑 내부에서 실행되는 함수를 호출할 수 있다.

 

 

중요한 차이점은 컨트랙트 실행과 달리

EOA에 의한 데이터 페이로드의 해석은 이더리움의 합의 규칙의 적용을 받지 않는다는 것

 

 

트랜잭션이 컨트랙트 주소로 데이터를 전달한다고 가정

→ 이 경우, 데이터는 EVM에 의해 컨트랙트 호출로서 해석

대부분의 컨트랙트에서는 이 데이터를 함수 호출로 사용하며,

명명된 함수를 호출하고 인코딩된 인수를 함수에 전달

 

ABI 호환 컨트랙트(모든 컨트랙트)로 전송된 data payload는

 

1️⃣ 함수 선택기(function selector)

함수 프로토타입의 Keccak-256 해시의 처음 4바이트로 컨트랙트에서 호출할 함수를 정확하게 식별 가능

 

2️⃣ 함수 인수(function argument)

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

 

이렇게 두 개를 16진수로 시리얼라이즈한 인코딩이다.

 

 

💡 함수 프로토타입(prototype)이란?

       함수의 이름을 포함하는 문자열로 정의하며 parameter의 type을 괄호와 함께 전달하는 형식

 

function withdraw(uint withdraw_amount) public {

이 함수를 예시로 들어보면, withdraw(uint256)이 함수의 프로토타입이 된다.

 

해당 프로토타입을 Keccak-256을 하면 다음과 같다.

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

함수 선택기(function selector)는 위 해시의 처음 4바이트인 0x2e1a7d4d 가 된다.

이 값을 통해 우리가 원하는 함수를 컨트랙트에 전달한다.

 

우리가 0.01ETH를 출금하고 싶다고 가정했을 때,

0.01이더를 16진수로 시리얼라이즈된 부호 없는 빅엔디안 256비트 정수로 인코딩하여 웨이로 표현한 값은 0x2386f26fc10000이다.

 

위 두값을 합쳐서 32바이트로 만들면 2e1a7d4d000000000000000000000000000000000000000000000000002386f26fc10000

과 같은 값이 만들어지고,

이것이 우리 트랜잭션의 data payload로, withdraw함수를 호출하고 0.01이더를 withdraw_amount로 요청한다.

 

 

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

블록체인에 새로운 컨트랙트를 만들어 향후 사용을 위해 배포하는 트랜잭션이 있다.

 

컨트랙트 생성 트랜잭션은 제로 어드레스라고 하는 특수 대상 주소로 전송된다.

= 컨트랙트 등록 트랜잭션의 to 필드는 0x0 주소를 포함

 

💡제로어드레스 (0x0) 란?

- 이 주소는 EOA나 CA를 나타내지 않는다.
  (이더를 소비하거나 트랜잭션을 시작할 수 없음을 의미)
- 목적지로만 사용되며,
- ‘컨트랙트 작성’이라는 특별한 의미로 사용된다.

 

[ 제로 어드레스의 사용 ]

  1. 컨트랙트 생성
  2. 실수로 인한 이더 손실
  3. 의도적인 이더 연소 (의도적으로 이더를 소비할 수 없는 주소로 보내서 파괴함)
    ➡️ 이 경우는 0x000000000000000000000000000000000000dEaD 지정된 이 주소로 보내야함

 

 

to field가 zero address인 생성 트랜잭션의 data payload & value 경우

data payload에 들어갈 것: 컨트랙트를 생성할 컴파일된 바이트코드 (바이트코드로 컴파일 필수)

value에 들어갈 것: 이더 값

  1. data payload만 포함된 경우 - 그냥 컨트랙트를 작성하는 것
  2. data payload & value 포함된 경우 - 새 컨트랙트를 특정 잔액으로 시작
  3. value 만 포함된 경우 - 이더를 연소 주소로 전송하는 것과 같다.
  4. 둘 다 포함하지 않는 경우 ?

 

컨트랙트는 솔리디티를 통해 바이트코드 표현으로 컴파일 할 수 있다.

 

 

컨트랙트 생성하는 트랜잭션 생성 예시)

$ solc --bin Faucet.sol

Binary:6060604052341561000f57600080fd5b60e58061001d6000396000f30060606040526004361060...

 

트랜잭션을 보내는 코드

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

 

 

위 코드 실행 후 생성된 컨트랙트에 대한 정보를 얻기 위해 트랜잭션 영수증 조회

> 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
}

 

 

 

디지털 서명

아래 두 가지에 대해서 알아볼 것이다.

  1. 디지털 서명이 작동하는 방식
  2. 개인키를 공개하지 않고 어떻게 개인키의 소유권을 증명하는 지

 

타원 곡선 디지털 서명 알고리즘

이더리움에서 사용되는 디지털 서명 알고리즘은 ECDSA(Elliptic Curve Digital Signature Algorithm)이다.

타원 곡선의 개인키-공개키 쌍을 기반으로 한다.

💡 디지털 서명이란?
디지털 메시지나 문서의 진위를 표현하기 위한 수학적 기법

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

 

 

디지털 서명의 3가지 용도

  1. 이더리움 계정과 개인키의 소유자가 이더 지출 또는 컨트랙트 이행을 승인했음을 증명
  2. 부인 방지(non-repudiation)를 보장 (즉, 허가의 증거는 부인할 수 없다)
  3. 트랜잭션이 서명된 후에는 트랜잭션 데이터가 수정되지 않았고 어느 누구도 트랜잭션 데이터를 수정할 수 없음을 증명
    (무결성 보장-integrity)

 

디지털 서명 작동 방법

디지털 수명은 두 단계로 구성된 수학적 체계이다.

첫째, 메시지(여기서는 트랜잭션)에서 개인키(서명 키)를 사용하여 서명을 만드는 알고리즘

둘째, 누구나 메시지와 공개키만 사용하여 서명을 검증할 수 있는 알고리즘

 

 

[ 디지털 서명 만들기 ]

 

이더리움의 ECDSA 구현에서 서명된 ‘메시지’는 트랜잭션이다.

좀 더 정확하게 트랜잭션의 RLP로 인코딩된 데이터의 Keccak-256 해시다. 서명 키는 EOA의 개인키다.

✔︎ k : 서명 개인키

✔︎ m : RLP 인코딩된 트랜잭션

✔︎ F(Keccak-256) : Keccak-256 해시 함수

✔︎ F(sig) : 서명 알고리즘
    함수 F (sig)는 일반적으로 r 및 s라고 하는 두 값으로 구성된 서명 Sig를 생성한다.
    → Sig= (r,s)

✔︎ Sig : 결과 서명

 

 

서명 확인

서명을 확인하려면 서명(r & s)시리얼라이즈된 트랜잭션,

그리고 서명을 만드는 데 사용된 개인키에 상응하는 공개키가 있어야 한다.

본질적으로, 서명 확인은 공개키를 생성한 개인키의 소유자만이 트랜잭션에서 서명을 생성할 수 있음을 의미한다.

 

서명 검증 알고리즘은 메시지(우리가 사용하기 위한 트랜잭션 해시),

서명자의 공개키 및 서명(r &s값)을 가져와서 서명이 메시지와 공개키에 유효하면 true를 반환한다.

 

 

ECDSA 계산

서명 알고리즘은 처음에는 임시 개인키를 암호학적인 안전한 방법으로 생성한다.

이 임시 키는 이더리움 네트워크에서 서명된 트랜잭션을 보는 공격자가 발신자의 실제 개인키를 계산할 수 없도록

r 및 s 값을 계산하는 데 사용된다.

즉, 단방향 계산만 가능한 서명 알고리즘을 통해 공개키로 개인키를 유추할 수 없게 함.

 

 

4장 공개키부분에서 봤듯이, 임시 개인키는 임시 공개키를 만드는 데 사용되므로 아래와 같은 이점이 있다.

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

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

 

 

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

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

검증은 r 와 s 값, 보낸 사람의 공개키를 사용하여 타원 곡선(서명 생성에 사용되는 임시 공개키)의 한 지점인 값 Q를 계산하는,

서명 생성 함수의 반대 프로세스다.

 

단계는 다음과 같다.

 

1️⃣ 모든 입력이 올바르게 구성되어 있는 지 확인한다.

2️⃣ 다음 식을 통해 w를 계산한다.

3️⃣ u1을 계산한다.

4️⃣ u2를 계산한다.

5️⃣ 마지막으로, 다음 타원 곡선을 계산한다.

  • r, s : 서명 값
  • K : 서명자의(EOA 소유자의) 공개키다.
  • m : 서명된 트랜잭션 데이터다.
  • G : 타원 곡선 생성자 점이다.
  • p : 타원 곡선의 소수 차수다.
  • Q : 임시 공개키

위 계산을 마친 뒤, 계산된 포인트 Q의 x 좌표가 r과 같으면,

검증자는 서명이 유효하다고 결론을 내릴 수 있다.

이 때, 서명을 검증함에도 개인키는 알려지지도 공개되지도 않는다.

 

🔽 ECDSA에 대해서 더 공부하고 싶다면 아래 글 참고

Understanding How ECDSA Protects Your Data.

 

 

 

트랜잭션 서명 실습

유효한 트랜잭션을 생성하려면 발신자는 ECDSA를 사용하여 메시지에 디지털 서명을 해야한다.

‘트랜잭션에 서명하시오’ = ‘RLP serialized된 transaction data의 Keccak-256 해시에 서명하시오’

즉, 서명은 트랜잭션 자체가 아니라 트랜잭션 데이터의 해시에 적용된다.

 

 

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

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

특수 서명 변수 v는 두 가지를 나타내는데,

ECDSArecover 함수가 서명을 확인하는 데 도움이 되는 (1) 복구 식별자(2) 체인 ID이다.

(1) 의 경우, 27이나 28 중 하나로 계산되고,

(2)의 경우 체인 ID의 두 배에 35 또는 36이 더해져 계산된다.

 

체인 ID에 대한 자세한 내용은 바로 밑 ‘EIP-155를 사용한 원시 트랜잭션 생성’에서 다룬다.

*복구 식별자는 공개키의 y 구성요소의 패리티를 나타내는 데 사용된다.

*’구식’ 서명의 27 또는 28, 전체 스퓨리어스 드래곤 유형 트랜잭션의 35 또는 36

 

 

원시 트랜잭션 생성 및 서명

이 절에서는 원시 트랜잭션을 생성하고 ethereumjs-tx 라이브러리를 사용하여 서명할 건데,

이는 일반적으로 사용자를 대신해서 트랜잭션에 서명을 하는 지갑 또는 어플리케이션 함수가 어떻게 작동하는 지를 보여준다.

 

소스코드 깃헙

https://github.com/ethereumbook/ethereumbook/blob/develop/code/web3js/raw_tx/raw_tx_demo.js

// Load requirements first:
//
// npm init
// npm install ethereumjs-tx
//
// Run with: $ node raw_tx_demo.js

const ethTx = require('ethereumjs-tx');
const txData = {
	nonce: '0x0',
	gasPrice: '0x09184e72a000',
	gasLimit: '0x30000',
	to: '0xb0920c523d582040f2bcb1bd7fb1c7c1ecebdb34', 
	value: '0x00',
	data: '',
	v: "0x1c", // Ethereum mainnet chainID
	r: 0,
	s: 0
};

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

txHash = tx.hash(); // This step encodes into RLP and calculates the hash
console.log('Tx Hash: 0x' + txHash.toString('hex'))

// Sign transaction
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...

 

 

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

EIP-155 : 단순 재생 공격 방지 표준 (Simple Replay Attack Protection)

⏩️ 이름 그대로 한 네트워크에서 전파된 트랜잭션은 다른 네트워크에서 재생될 수 없다.

 

EIP-155은 서명하기 전에 트랜잭션 데이터 내부에 체인 식별자(chain identifier)를 포함하여

재생 공격 방지가 가능한 트랜잭션 인코딩을 지정한다.

이렇게 되면, 하나의 블록체인에 대해 생성된 트랜잭션이 다른 블록체인에서 유효하지 않다.

(예를 들면, 이더리움 메인넷에서 생성된 것은 테스트 넷에서는 유효하지 않음)

 

[ EIP-155 트랜잭션 데이터 구조 ]

기본적인 트랜잭션 데이터 구조의 주요 6개 필드에 체인 식별자, 0, 0의 3개 필드를 추가한다.

이 3개의 필드는 인코딩되고 해싱되기 전에 트랜잭션 데이터에 추가된다.

즉, 체인 식별자가 서명된 데이터에 포함됨으로써, 트랜잭션 서명은 체인 식별자가 수정되면 서명이 무효화되어 변경된 데이터를 식별할 수 있다.

⏩️ EIP-155는 서명의 유효성이 체인 식별자에 의존하기 때문에 다른 체인에서 트랜잭션을 재생할 수 없다.

 

 

체인 식별자는 다음과 같이 지정되어 있고, 인터넷 검색을 통해서 확인 가능.

결과로 생성되는 트랜잭션 구조는 기존과 같이 RLP로 인코딩되고 해싱되고 서명된다.

이 때, 서명 알고리즘은 v 접두어에 체인 식별자를 인코딩하기 위해 약간 수정된다.

 

🔽 자세한 내용은 아래 참고

https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md

 

 

 

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

트랜잭션 메시지는 ‘발신자(from)’ 필드를 포함하지 않는다.

이는 발신자의 공개키가 ECDSA 서명을 통해 직접 계산될 수 있기 때문이다.

공개키가 있으면 쉽게 주소를 계산할 수 있다.

서명자의 공개키를 복구하는 프로세스를 공개키 복구(public key recovery)라고 한다.

 

아까 봤던 ECDSA 계산 반대 프로세스에서, 계산된 r과 s가 주어지면, 2개의 가능한 공개키를 계산할 수 있다.

1️⃣ 먼저 서명에 있는 x좌표인 r 값에서 2개의 타원 곡선 점 R과 R’를 계산한다.

타원 곡선은 x축 대칭이라서, 어떤 x값에 대해서도 2개의 y값, 즉 2개의 점이 존재한다.

 

2️⃣ r에서 우리는 r의 곱셈 역합수의 r^(-1)을 계산한다.

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

 

 

이렇게 계산을 하고 나면 가능한 2개의 공개키는 다음과 같다.

  • K1과 K2 : 서명자의 두 가지 가능한 공개키
  • r^(-1) : 서명 r 값의 곱셈 역함수
  • s : 서명의 s 값
  • R과 R’ : 두 가지 가능한 일시적인 공개키 Q
  • z : 메시지 해시의 n 최하위 비트
  • G : 타원 곡선 생성자 점

 

효율성을 위해, 트랜잭션 서명에는 2개의 가능한 R 값 중 임시 공개키가 무엇인지 알려주는 접두어 v가 포함된다.

  • v가 짝수 : R
  • v가 홀수 : R’

이런 식으로 R에 대해 하나의 값만, K에 대해 하나의 값만 계산해야 한다.

 

 

 

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

트랜잭션이 서명되면 트랜잭션은 이더리움 네트워크로 전송할 준비가 된다.

 

일반적으로,

  • 트랜잭션 생성
  • 서명
  • 브로드캐스트

이 세 단계는 단일 작업 (예: web3.eth.sendTransaction)에 의해 처리된다.

 

 

그러나, 두 단계로 나누어 트랜잭션을 생성하고 서명할 수 있다.

서명된 트랜잭션이 있다면 web.eth.sendSignedTransaction을 사용해

트랜잭션을 16진수로 인코딩하고 서명해서 이더리움 네트워크에 전송할 수 있다.

 

 

그렇다면 왜 트랜잭션의 서명과 전송을 분리할까?

가장 큰 이유는 보안이다.

트랜잭션에 서명하는 컴퓨터에는 잠금 해제된 개인키가 메모리에 로드되어 있어야 한다.

전송을 수행하는 컴퓨터는 인터넷에 연결되어 있어야 하며, 이더리움 클라이언트를 실행해야 한다.

즉, 두 기능이 하나의 컴퓨터에 있다는 것은 온라인 시스템에 개인키가 있게 되는 것이고, 매우 위험하다.

 

 

[ 오프라인 서명 (offline signing) ]

서명 및 전송 기능을 분리하여 각기 다른 시스템(오프라인 및 온라인 장치 각각)에서 수행하는 것

이더리움 트랜잭션의 오프라인 서명

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

 

필요한 보안 수준에 따라 ‘오프라인 서명’ 컴퓨터는 방화벽이 있는 서브넷(온라인이지만 분리되어 있음)에서

완전히 격리되기까지(에어갭) 컴퓨터와 분리 정도를 다르게 할 수 있다.

트랜잭션에 서명하려면 데이터 저장 매체 또는 웹캠과 QR코드를 사용하여 에어 갭 컴퓨터와 주고받는 트랜잭션을 생성한다.

이는 서명하고자 하는 모든 트랜잭션을 수동으로 전송해야 한다는 뜻이고, 스케일링 할 수도 없다.

 

완전 에어 갭 시스템을 활용할 순 없지만, 약간의 격리로도 상당한 보안 이점을 얻을 수 있다.

예) 메시지 대기열 프로토콜만 허용하는 방화벽이 있는 격리된 서브넷

 

많은 기업이 ZeroMQ(0MQ) 같은 프로토콜을 위 예시 용도로 사용한다.

위 설정에서는 서명을 위해 트랜잭션이 시리얼라이즈되고 대기한다.

 

트랜잭션이 시리얼라이즈되는 순서는 아래와 같다.

  1. 대기열 전송 프로토콜은 TCP 소켓과 비슷한 방식으로 시리얼라이즈된 메시지를 서명 컴퓨터로 전송한다.
  2. 서명 컴퓨터는 대기열에서 시리얼라이즈된 트랜잭션을 신중하게 읽고, 적절한 키와 함께 서명을 적용한 후에 보내는 대기열에 배치한다.
  3. 보내는 대기열은 서명된 트랜잭션을 대기열을 처리하고 전송하는 역할을 하는 이더리움 클라이언트가 있는 컴퓨터로 전송한다.

 

미천하게 그림(?)으로 표현해본 위 MQ의 구조

 (트랜잭션 생성 컴퓨터)  → [M] [M] → (MQ 프로토콜) → [M] [M] → (서명 컴퓨터)

(브로드캐스트할 컴퓨터) ← [M] [M] ← (MQ 프로토콜) → [M] [M] ← (서명 컴퓨터)

 

 

트랜잭션 전파

이더리움 네트워크는 Flood routing 프로토콜을 사용한다.

💡 Flood routing

 

각 이더리움 클라이언트는 메시(mesh) 네트워크를 형성하는 P2P 네트워크에서 모두 동등한 노드 역할을 한다.

 

< 용어정리 >

클라이언트: 이더리움 황서(소프트웨어 사양)을 구현해놓은 소프트웨어

노드: P2P 네트워크에 연결되어 참여하는 이더리움 클라이언트

 

 

트랜잭션 전파는 서명된 트랜잭션을 생성(또는 오프라인에서 수신)한 이더리움 노드에서 시작한다.

트랜잭션은 검증된 후에 트랜잭션을 생성한 노드의 이웃노드로 전파가 시작된다.

평균적으로 각 이더리움 노드는 적어도 13개의 이웃(neighbor)이 있다.

 

각 이웃 노드는 트랜잭션을 수신하자마자 즉시 유효성 검사를 한다.

만일, 유효성 검사에서 타당하다고 생각되면 그 사본을 저장하고 모든 이웃에게 전파한다. (본인이 TX를 전달받은 이웃은 제외)

이런 과정을 거쳐 몇 초 내에 이더리움 트랜잭션이 전 노드로 전파된다.

 

이 때, 각 노드의 관점에서 보면 트랜잭션의 출처를 식별할 수 없다.

트랜잭션의 출처를 추적하기 위해서는 많은 노드를 제어해야하기 때문이다.

이것은 P2P 네트워크의 보안 및 개인 정보 보호 설계의 일부이며, 특히 블록체인 네트워크에 적용된다.

 

 

 

블록체인에 기록하기

이더리움의 모든 노드는 동등한 피어이지만,

일부는 채굴을 하며 고성능 그래픽 처리 장치(GPU)가 장착된 컴퓨터인 채굴 팜(mining farm)에 트랜잭션 및 블록을 제공한다.

 

채굴 컴퓨터는 트랜잭션을 후보 블록에 추가하고 후보 블록을 유효하게 만드는 작업증명(PoW)을 찾으려고 시도한다.

트랜잭션이 블록에 채워지면 계정의 잔액을 수정하거나 내부 상태를 변경하는 컨트랙트를 호출하여

트랜잭션은 이더리움 싱글톤 상태를 수정한다.

이러한 변경사항은 이벤트가 포함될 수 있는 트랜잭션 영수증(receipt) 형식으로 트랜잭션과 함께 기록된다.

 

EOA에 의한 서명, 전파, 그리고 마지막으로 채굴까지 완료된 트랜잭션

싱글톤의 상태를 변경하고 블록체인에서 지울 수 없는 기록을 남긴다.

 

 

다중 서명 트랜잭션

비트코인과 달리, 이더리움의 기본 EOA 값 트랜잭션에는 다중 서명 조항이 없다.

그러나 이더와 토큰을 전송하는 어떤 조건들도 처리할 수 있는 스마트 컨트랙트를 사용해 임의의 서명 제한룰을 적용할 수 있다.

 

이 기능을 이용하려면 이더를 다중 서명 요구사항이나 지출 한도와 같이

원하는 지출 규칙으로 프로그래밍한 ‘지갑 컨트랙트’로 보내야 한다.

그리고 지갑 컨트랙트는 지출 조건이 충족되면 승인된 EOA의 요청에 따라 자금을 보낸다.

 

예를 들어, 멀티 시그를 이용하기 위해서 다중 서명 컨트랙트로 이더를 전송하고,

다른 계정으로 출금을 하고 싶을 때마다 모든 사용자는 일반 지갑 어플리케이션을 사용하여 컨트랙트에 트랜잭션을 보내야 하며,

계약이 최종 트랜잭션을 수행할 수 있도록 효과적으로 승인해야 한다.

 

이러한 컨트랙트는 내부 코드를 실행하거나 다른 컨트랙트를 실행하기 전에 다중 서명을 요구하도록 설계될 수도 있다.

스마트 컨트랙트로 다중 서명 트랜잭션을 구현하는 기능을 통해 이더리움의 유연성을 입증한다.

하지만 이는 양날의 검이 되기도 하는 데, 다중 서명 체계의 보안을 약화시키는 버그가 발생할 수도 있기 때문이다.

 

 

결론

트랜잭션은 이더리움 시스템의 모든 활동의 시작점이다.

트랜잭션은 EVM이 컨트랙트를 평가하고, 잔액을 업데이트하며, 일반적으로 이더리움 블록체인의 상태를 수정하는 ‘입력’이 되는 것이다.