brunch

You can make anything
by writing

C.S.Lewis

by 김훈일 Jan 02. 2019

트랜잭션 해시(TXID)에 대한 오해

트랜잭션 해시는 블록체인 노드를 통해서만 얻을 수 있는 값이 아니다.

목차

1. 트랜잭션 해시라는 용어에 대해서

2. 트랜잭션 해시가 만들어지는 원리

3. 마치며


1. 트랜잭션 해시라는 용어에 대해서

블록체인에는, 트랜잭션 해시(Transaction hash) 라는 용어가 존재합니다. 이 용어는 거래소에서도 TXID(Transaction ID)라는 이름으로도 사용되기 때문에, 거래소를 이용하면서 암호화폐를 입금하거나 출금을 해보셨다면 한 번쯤은 접해보셨을 수도 있습니다.


이 트랜잭션 해시라는 것은 해당 트랜잭션의 고유 ID 입니다. 거래소의 예를 들어 설명해보자면, 고객 A가 거래소의 지갑으로 100 ETH를 보냈는데, 거래소에서 반영이 안될 때에, 고객 A는 거래소에 전화해서 자신이 보낸 트랜잭션의 고유 ID를 말해주면서 문의전화를 할 수 있습니다. 거래소 측에서는 마찬가지로 이 트랜잭션의 고유 ID가 실제 블록체인 네트워크 상에 존재하는지 확인하고, confirmation 횟수를 확인하여 언제쯤 입금처리가 되는지에 대해서 말해 줄 수 있습니다.


좀 더 절차적으로 설명드리자면 이렇습니다.

1) 고객이 트랜잭션 ID를 제시한다.

"제 트랜잭션 해시는 이것입니다. : 0x4b8d5180d63282bf9697147cf4d74f3a89e405930297fa3012b7236a762b513b "


2) 거래소가 트랜잭션 ID를 블록체인 네트워크에서 확인해본다.

https://etherscan.io/tx/0x4b8d5180d63282bf9697147cf4d74f3a89e405930297fa3012b7236a762b513b 


3) 해당 트랜잭션의 Confirmation 횟수를 확인 후, 고객에게 답변해준다.  

"우리 거래소에서는 confirmation이 40이 넘었을 때에 입금으로 처리하기 때문에 5분 정도 더 기다리시면 거래소에서 반영이 됩니다."


즉, 앞서 말씀드린 예에서 봤듯이 트랜잭션 해시라는 것은 어떤 트랜잭션을 고유하게 식별할 수 있게 해주는 ID와 같은 것입니다.


위에서 말씀드린 "어떤 트랜잭션 (트랜잭션 정보)"이란 다음과 같은 형태를 띄고 있습니다.

{

   from: '내 이더리움 지갑 주소'

   to: '내 거래소 이더리움 지갑 주소'

   value: 100,

   gasPrice: ...,

   gas: 21000

}

(트랜잭션 정보)


위와 같이 트랜잭션 정보는 아래와 같이 송신자(from), 수신자(to) 등등의 정보를 담고 있고, 이 트랜잭션 정보의 고유한 ID 값이 바로 트랜잭션 해시(TXID) 입니다.


2. 트랜잭션 해시가 만들어지는 원리

그렇다면, 트랜잭션 해시는 어떻게 만들어지는걸까요? 그냥 랜덤한 값으로 누군가가 정해주는 걸까요? 누군가라면 블록체인을 구성하고있는 컴퓨터 중 하나(노드)가 정해주는 걸까요?


사실 트랜잭션 해시가 만들어지는 원리는 간단합니다. 트랜잭션 해시를 만들기 위해서는 딱 두 가지의 재료가 필요합니다.

a. 트랜잭션 정보

b. 프라이빗 키

(프라이빗 키가 생소하시다면 https://brunch.co.kr/@nujabes403/11 와 https://brunch.co.kr/@nujabes403/13 포스트를 참조해주세요.)


a. 트랜잭션 정보

트랜잭션 정보는 아까 만들었습니다. "어느 지갑으로 얼마를 보내고 수수료는 얼만큼 지불할 것인지" 에 대한 정보가 바로 트랜잭션 정보이고, 보통 거래소를 이용하신다면 아래 스크린샷과 같이 유저에게 친숙한 인터페이스로 이런 정보들을 입력할 수 있게됩니다.

트랜잭션 정보를 입력하기 위한 친숙한 UI, 이런 정보들을 통해 트랜잭션 정보를 만든다.


b. 개인 키

그 다음 필요한 것은 개인 키(private key)입니다. 이 개인 키가 있어야만 트랜잭션 정보에 '서명'이라는 것을 할 수 있습니다.


cf) 왜 서명이 필요할까?

만일, 트랜잭션 정보의 from(송신자)라는 입력란에 아무 것이나 쓸 수 있고 그 트랜잭션을 전송할 수 있다면 돈 많은 사람의 지갑에서 내 지갑으로 코인을 보내는 것이 가능해지겠죠? 그것을 막기 위해, "이 트랜잭션은 내가 보낸 것이 확실합니다." 라는 보증을 해주는 것이 필요합니다. 이러한 보증을 '서명' 이라고 합니다.

(서명에 대해 생소하시다면 https://brunch.co.kr/@nujabes403/13 포스트를 참조해주세요.)


개인 키를 통해서 트랜잭션에 서명이 끝났다면 우리는 트랜잭션 정보와 서명을 블록체인 네트워크에 보낼 수 있습니다. 이더리움에서는 트랜잭션 정보와 서명을 합쳐서 보냅니다. (트랜잭션 정보와 서명을 RLP encoding하는 것을 의미합니다.)


이 트랜잭션 정보와 서명을 합친 정보를 블록체인 네트워크를 구성하는 한 컴퓨터 (노드)에서 받게되면, 해당 트랜잭션의 고유 값, 즉 트랜잭션 해시를 주게 됩니다. 즉, 트랜잭션을 보낸 사람은 그 트랜잭션 해시를 이용해서 블록체인 네트워크에서의 해당 트랜잭션 상태를 볼 수 있게 됩니다.


종합해보면, 트랜잭션 정보와 서명을 전달하면 노드로부터 트랜잭션 해시를 받을 수 있다는 것입니다. 트랜잭션 정보와 서명을 합친(encoding) 것을 우리는 rawTransaction 이라고 부릅니다. rawTransaction은 이렇게 생겼습니다.


'0xf86c028501dcd6500082520894fe001e973e1cfce58e4c9ef073ca10a7dbbbaa7488c5f6453c3870846e801ba026948f5dd25456ada3c9df777455d695aee77bf1554b377aa3e2a6fb0d4084c1a07e92a2b0b131f1f1631bfcd0d534c25d6bb57008d5d9465fa7e3784ca8ce96be'

(rawTransaction의 생김새)


이렇게 0x로 시작하는 16진수 기반의 값을 hex 값이라고 하는데, 읽기 굉장히 어렵지만, 컴퓨터는 이 rawTransaction을 해석(decode)해서 트랜잭션 정보가 무엇인지, 서명이 어떤 것인지 알아낼 수 있습니다.


다음은 rawTransaction을 해석(decode)한 값입니다.

[ '0x02',

  '0x01dcd65000',

  '0x5208',

  '0xfe001e973e1cfce58e4c9ef073ca10a7dbbbaa74',

  '0xc5f6453c3870846e',

  '0x',

  '0x1b',

  '0x26948f5dd25456ada3c9df777455d695aee77bf1554b377aa3e2a6fb0d4084c1',

  '0x7e92a2b0b131f1f1631bfcd0d534c25d6bb57008d5d9465fa7e3784ca8ce96be' ]

(rawTransaction의 decode 값)


아까는 굉장히 긴, 하나의 문자열로 구성되어있었는데 이번에는 조금씩 잘린 형태로 출력됩니다. 여전히 읽기 힘든 것은 마찬가지지만, 순서대로 값을 해석해보면 다음과 같습니다.


1. nonce (0x02) = 논스가 2인 트랜잭션입니다.

2. gasPrice (0x01dcd65000) = 가스 수수료가 8 gwei인 트랜잭션입니다.

3. gas (0x5208) = 가스리밋이 21000인 트랜잭션입니다.

4. to (0xfe001e973e1cfce58e4c9ef073ca10a7dbbbaa74) = 받는 사람의 지갑주소입니다.

5. value (0xc5f6453c3870846e) = 보내는 양이 14.264664994689877102 Ether 에 해당하는 트랜잭션입니다.

6. data (0x) = 추가적으로 트랜잭션에 실어보내는 데이터는 없습니다.

7. v (0x1b) = 서명과 관련된 값입니다.

8. r (0x26948f5dd25456ada3c9df777455d695aee77bf1554b377aa3e2a6fb0d4084c1) = 서명과 관련된 값 입니다.

9. s (0x7e92a2b0b131f1f1631bfcd0d534c25d6bb57008d5d9465fa7e3784ca8ce96be) = 서명과 관련된 값입니다.


위에 있는 값들을 각각 복사, 붙여넣기 해서 검색해보시면 아까 보여드렸던 굉장히 긴 문자열안에 하나하나씩 들어가있는 것을 알 수 있습니다.


'0xf86c028501dcd6500082520894fe001e973e1cfce58e4c9ef073ca10a7dbbbaa7488c5f6453c3870846e801ba026948f5dd25456ada3c9df777455d695aee77bf1554b377aa3e2a6fb0d4084c1a07e92a2b0b131f1f1631bfcd0d534c25d6bb57008d5d9465fa7e3784ca8ce96be'

(rawTransaction에 nonce, gasPrice, gas, to, value, data, v, r, s 값이 모두 들어가있음을 알 수 있다.)


자 그럼, rawTransaction에 대한 정체가 트랜잭션 정보와 서명을 합친 정보의 덩어리인 것을 알았고, 이 덩어리를 이더리움 노드가 받아서 트랜잭션 해시를 만들어준다는 것까지 자세하게 알아본 것 같습니다. 그럼 원래의 질문으로 다시 돌아가서, "그래서 트랜잭션 해시를 어떻게 만드는 걸까요?"


트랜잭션 해시는 사실, 위에 나와있는 rawTransaction에 keccak256 이라고 하는 해시함수를 사용해서 나오는 결과값입니다.


그럼 실제로 위의 값에 keccak256 해시함수를 사용해보겠습니다.

rawTransaction에 keccak256을 돌린 결과값. (https://leventozturk.com/engineering/sha3/)

위의 Message에 넣은 값은 아까 보여드렸던 rawTransaction 입니다.

0xf86c028501dcd6500082520894fe001e973e1cfce58e4c9ef073ca10a7dbbbaa7488c5f6453c3870846e801ba026948f5dd25456ada3c9df777455d695aee77bf1554b377aa3e2a6fb0d4084c1a07e92a2b0b131f1f1631bfcd0d534c25d6bb57008d5d9465fa7e3784ca8ce96be

이 값에서 맨 앞에 붙은 '0x'라는 값은 16진수를 표현하기 위해서만 사용되는 값이기 때문에 지운 후에 넣어주었습니다.


아래에 Hash output 이라고 나온 값이 바로 위의 rawTransaction을 인자로 하여 돌린 해시함수 keccak256의 결과값입니다.


4b8d5180d63282bf9697147cf4d74f3a89e405930297fa3012b7236a762b513b

라는 값이 나왔는데요. 이더스캔에서 한 번 검색해보겠습니다.

https://etherscan.io/tx/0x4b8d5180d63282bf9697147cf4d74f3a89e405930297fa3012b7236a762b513b 



논스가 2이고, 가스리밋이 21000, 수신자가 0xfe001e973e1cfce58e4c9ef073ca10a7dbbbaa74, 보내는 이더리움 개수 14.2646... 모든 값이 일치합니다. 확실하게 보고싶으면 https://etherscan.io/getRawTx?tx=0x4b8d5180d63282bf9697147cf4d74f3a89e405930297fa3012b7236a762b513b 

링크를 통해서 들어가서 확인할 수 있습니다.


0x4b8d5180d63282bf9697147cf4d74f3a89e405930297fa3012b7236a762b513b 트랜잭션 해시를

가진 rawTransaction은 0xf86c028501dcd6500082520894fe001e973e1cfce58e4c9ef073ca10a7dbbbaa7488c5f6453c3870846e801ba026948f5dd25456ada3c9df777455d695aee77bf1554b377aa3e2a6fb0d4084c1a07e92a2b0b131f1f1631bfcd0d534c25d6bb57008d5d9465fa7e3784ca8ce96be 임을 알 수 있습니다. 우리가 앞에서 봤던 rawTransaction의 값과 완전히 일치합니다.


그럼 한번 더 생각해보겠습니다. 트랜잭션 해시는 단순히 rawTransaction에 keccak256 해시함수를 돌려 얻을 수 있는 값입니다. 그렇다는 것은, rawTransaction을 꼭 이더리움 블록체인에 전송하지않아도 이 rawTransaction의 트랜잭션 해시가 어떨 것이라는 것을 미리 알 수 있다는 것입니다.


보통 web3와 같은 이더리움 RPC 라이브러리를 사용하다보면, 트랜잭션 전송에 대한 예제 코드로 아래와 같은 문법을 사용합니다.

web3 1.0 공식문서에서 설명하고 있는 sendTransaction 사용 문법

web3.eth.sendTransaction({

 from: ...,

 to: ...,

 value: ...,

 data: ...,

 nonce: ...,

})

.on('transactionHash', ...)

.on('receipt', ...)

.on('error', ...)


코드를 모르는 분께도 설명드리자면 간단합니다.

1) 트랜잭션 정보를 { ... } 안에 담습니다.

2) 트랜잭션의 상태에 따라 처리 로직을 만들어줍니다.

예를 들어 .on('transactionHash') 는 "트랜잭션 해시가 노드로부터 도착하면 ~를 해라" 라는 뜻 입니다.

마찬가지로 .on('receipt')"트랜잭션이 블록에 완전히 들어가면 ~를 해라" 라는 뜻이고,

마지막으로 .on('error')"트랜잭션 전송에 에러가 발생하면 ~를 해라"라는 뜻입니다.


여기서 개발자분들이 대부분 오해하시는 지점이 바로 .on('transactionHash') 코드 입니다. 이러한 코드를 보면 마치 transaction hash라는 값은 이더리움 노드만 생성할 수 있는 값인 것 같고, 클라이언트 단에서 만들 수 있는 값이 아니라고 생각합니다. 하지만, 그렇지 않습니다. 트랜잭션 정보와 서명을 담은 rawTransaction 값만 알고 있으면 여기에 keccak256 해시 함수만 돌리면 transactionHash를 알 수 있습니다.


이와 비슷한 코드로 web3.eth.accounts.signTransaction 라는 코드가 있습니다.

이 코드는 트랜잭션 정보와 개인 키(private key)를 이용해 rawTransaction을 만들어주는 함수입니다.


web3.eth.accounts.signTransaction

이 함수를 통해서, 이더리움 네트워크에 트랜잭션을 전송하기도 전에 벌써 rawTransaction을 만들 수 있고 따라서 transactionHash도 알 수 있습니다. (물론, 실제로 트랜잭션을 전송하지 않았다면 etherscan 같은 블록 익스플로러에서 해당 트랜잭션을 찾을 수는 없을 것입니다.)


이것을 이용하면 내가 아직 블록체인에 전송하지는 않았지만, 한 3일 뒤? 정도에 보낼 트랜잭션의 해시 값을 정확하게 알 수 있습니다. 조금 억지스럽지만, 친구에게 3일 뒤에 돈을 보내는 시나리오를 예로 들어서 설명드려보겠습니다.


A: "B야, 내가 너한테 갚을 돈 3 ETH가 있는데 지금 당장은 내가 못 보내고 한 3 일 뒤에 보낼 수 있을 것 같아. 3일 뒤에 보내게 되면, 그 트랜잭션 해시 값이 '0x4b8d5180d63282bf9697147cf4d74f3a89e405930297fa3012b7236a762b513b' 일 거야. 그러니까 3일 뒤에 저 트랜잭션해시로 조회해봐."


B: "알겠어, 3일 뒤에 저 트랜잭션 해시로 조회해볼게."


여기서 A는 아직 트랜잭션을 보내지 않았습니다. 하지만, 트랜잭션 정보로

{

송신자: A

수신자: B

보낼 돈: 3 ETH

...

}

을 알고 있고, A의 개인키로 해당 트랜잭션의 서명을 만들면,

트랜잭션 정보 + 서명을 담은 rawTransaction을 만들 수 있고,

keccak256 해쉬함수를 이용하여 트랜잭션 해시도 만들 수 있습니다.


결론적으로, 트랜잭션 해시는 트랜잭션을 전송하기전에도 미리 알 수 있는 값입니다.


3. 마치며

블록체인과 상대적으로 친숙한 dApp 개발자들도 따로 이더리움의 코어를 담당하는  go-ethereum 코드를 보지 않고, web3나 metamask, ethereum-js와 같은 라이브러리를 사용하다보면, 그 라이브러리의 usage를 통해서만 "블록체인 코어 코드가 이럴 것이다~" 라고 생각하게 되는 경우가 많은 것 같습니다.


특히, 위에 올렸던 web3의 .on('transactionHash') 코드의 경우는 제가 실제로 "무조건 노드에서만 트랜잭션 해시를 받아오는 건가보구나" 라는 오해를 가졌었습니다.


아무래도 타원곡선, ECDSA, secp256k1와 같은 생소한 암호학적인 요소가 들어가다보니까 이더리움 노드의 역할이 어디까지인지 모호해서 짐작하게 되는 경우가 있어서인 듯 합니다.


다음 글에서는 실제 go-ethereum에서 트랜잭션을 처리하는 로직에 대해서 단계별로 살펴보도록 하겠습니다.

긴 글 읽어주셔서 감사합니다 :)

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari