화폐의 본질을 생각해보면 교환이다. 지급(payment), 청산(clearing), 결제(settlement)로 나누어서 보기도 하는데, 결국 두 주체간의 가치 교환에 대한 내용이다. 그리고 이 교환을 표준화 및 수량화 시킨 것이 바로 "화폐"다.
이렇게 수량화된 화폐는 가만히 생각해보면 늘 남는 돈이 발생할 수 밖에 없다. 다자간 교환이 다수가 발생하면서 시차와 금액차가 있기 때문에, 돈이 남지 않게 하려면, 모든 사람이 동시에 같은 금액을 거래해서 수입지출이 0이 되게 하는 실질적으로 가능하지 않은, 비현실적인 방법 밖에 없기 떄문이다. 그래서 정부는 실제 교환에 유효한 충분한 양의 화폐를 공급해야만 그러한 교환이 원활하게, 즉 경제가 돌아가게 할 수 있다. 물론 불행히도 여러가지 목적에 의해서 화폐량을 늘릴 수 있는 가능성을 별도로 하고도 말이다.
여하튼, 또 다른 말로는, 가치 교환이 표준화 되는 순간 남는 돈의 존재가 필수적이 된다고 할 수 있다.
사람들은 늘 이익을 극대화하 위해 노력하게 되는데, 그렇다면 이 남는 돈을 거래할 수는 없을까? 그래서 곧바로 빚과 이자, 신용이 탄생하게 된다. 이익을 극대화하는 것은 시장의 효율성에도 한몫 하는데, 그래서 이 신용은 시장을 효율화하기도 한다. 그리고 그 효율성을 추구하다보면 신용의 탄생이 필연이다.
그런데 여기서 화폐란 어떤 특성을 지녀야 하는가? 이 때는 어찌보면, 그저 합의된 원장 역할의 기준만 되도록 해주면 된다. 그리고 그 원장의 가장 중요한 속성은 사라지지도 않고, 속일 수 없으면 된다. 당연히 가치가 보존되도록 발행량이 관리될 필요도 있다.
그래서 의심스러워 보이는 비트코인도 화폐역할을 할 수 있다. 그리고 앞의 글에도 소개했듯이 이 비트코인은 일반 화폐가 지니지 못하는 많은 전자적인 편리 기능을 가지고 있다. 더할 나위없이 진보한 화폐이다. 원격 송금이 가능하며, 그 긴 세월 아직까지 해킹되어 붕괴한 적도 없고, 누구도 부인할 수도 없고 훔쳐갈 수도 없다. 발행량은 통제되며, 옮기는데 힘들거나 보관하기 어렵지도 않다. 더군다나 꽤 많은 사람들에게 인지되고 참여자가 많은 화폐이다. 세상에 이런 편리한 화폐가 있어왔는가?
더군다나 비트코인에서 발전한 암호화폐는 이 신용에 대한 자체적인 기능을 일부 지원한다. 스테이킹이나 DeFi같은 스마트 컨트랙트 기능이 대표적이다. 이런 기능을 종합적으로 갖는 화폐가 기존에 있었던가? 결제는 또 얼마나 완벽한가. 비트코인의 발행량 통제는 너무나 잘 알려져 있고 갑자기 늘어날 일이 없다. 오히려 암호를 잊어 없어지는 화폐가 걱정일 정도다.
물론 이 비트코인도 완전하지는 않다. 언급했듯이 소유주가 개인키를 잊으면 분실되는 단점이 있다. 몇가지 속성때문에 화폐 주도권을 갖으려는 정부가 금지할 수도 있다. 비트코인은 개인키를 모르면 통제할 수가 없기 때문에 몰수도 안되고 상속도 되지 않는 부분이 있는 것이다. 그리고 또하나 비트코인의 경쟁상대는 더 편리하고 보편적인 인지를 확보하게될 다른 암호화폐가 되겠다. 금이나 은같은 물질과는 다르게 암호화폐는 더 편리한 다음 서비스로 진화할 수 있는데, 언제고 또 다른 더 수수료도 적고 거래속도도 빠른, 기능이 다양한 비트코인의 다음 버전이 나와서 어떤 계기로 잘 정착되면 사람들이 서비스를 갈아 탈 수도 있겠다. 이런 관점에서는 DeFi가 가능한 이더리움이 강한 힘을 갖고 있지 않나 생각했던 적이 있다. 다만, 지금은 무제한 발행히 가능한 이더리움과는 적절한 수준에서 공존하고 있는 셈이고, 오히려 비트코인은 디지털 금의 위치를 차지하고 있는게 아닌가 싶다.
오랜 친구들과 만나면 블록체인 기반 암호화폐 이야기를 해주는데, 비록 코인 매수를 추천하지는 않더라도(변동성이 너무 심하다) 암호화폐의 가치를 단지 쓸모없는 허상에 거품으로 치부하는 것은 1차원적인 판단이라는 말은 꼭 해준다.
이 무용론은 예컨데 비트코인이 복사가 쉽고 단지 숫자뿐인 쓸모없는 허상이고, 어딘가 쓸데가 있는 금과는 다르다는 것이 가장 전형적이다. 전혀 가치가 없다는 말이다.
그러나 이 관점은, 현대사회의 많은 여러가지 구성요소의 본질을 이해하는데 별로 도움이 되지 않는 전통적인 사고방식이다. 이 복잡한 현대 사회는 각 참여자의 이해관계에 의해서 돌아가는 생태계를 이해하는게 중요하다. 즉 그 참여자들이 물려 돌아가는 것을 제각기 이해할때만 제대로 보인다.
첫번째, 무언가가 교환가치를 갖는 화폐로서의 신뢰를 갖게 만드는 것은 매우 어려운 일인데, 기적적으로 비트코인이 어떤 수 이상의 신뢰 그룹을 확보했다. 2010년대를 거치면서 탈중앙화를 부르짖는 일련의 그룹과 기술을 신봉하는 이들에 의해 그리 되었다. 이것은 지금봐도 신비롭다.
두번째, 이 비트코인은 완전히 투명하게 운영되며 유통량이 적정 수준을 넘지 않는다는 것이 보증된다.
세번째, 이제 신뢰를 확보한 이 비트코인이 화폐로서 금과 비교해보면, 온라인 화폐 관점에서는 피쳐폰을 쓰다가 스마트폰을 쓰는 느낌이다. 글로벌 송금이 보장되며 금보다 보관도 쉽고, 나눠서 사용하기도 좋다. 아이폰이 나오고 나서야 기존 스마트폰이 불편했던 것을 알게 되는 것처럼, 비트코인은 이 금이 얼마나 불편한지 깨닫게 해주었다. 금은 아주 작게 자를 수도 없고 보관을 하려면 경비가 필요하다. 송금같은 것은 없고 누구에게 보내려면 들고 가야한다. 그리고 언제나 순도를 의심해야 한다. 그런데 비트코인은 이런 모든 단점이 없다. 물론 개인이 사용하기에 불편한 점이 있지만, 몇가지 툴을 사용하면 어느정도 금의 불편함에 비길바는 아니다.
네번째, 이 화폐에 대한 참여자가 늘고 연관 서비스는 계속 성장한다. 그 화폐를 지원하는 서비스가 늘어나게 된다. 화폐 수요가 증가한다
이 틀에서는 갑자기 암호화폐가 기존에 존재하던 법정화폐들과도 경쟁을 시작하게 된다. 의외로 사람들은 가치를 저장하기 위해서 혹은 직접 쓰지는 않지만 뭔가 필요에 의해서 화폐를 교환하고 쌓아둔다. 그것을 법정화폐로 할지 아니면 암호화폐로 할지 고민하는 사람이 생기기 시작한다. 그리고 후자를 택하는 사람들을 위주로 지속적으로 생태계가 작동하게 된다. 물론 현재는 투기냐 아니냐 논쟁이 거센 가상자산 거래소를 위주로 유통되지만, 이제 2세대 3세대 블록체인 기술들이 송금/결재 이상의 것을 지원하게 되고 더 다양한 생태계를 끌어안게 된다.
따라서 암호화폐의 가치는 참여자 유입이나 관련 생태계의 성장 관점에서 볼 일이다. 규제가 그것을 억누르기도 하지만, 그 생태계의 성장이 다시 규제를 비껴가고, 사람들은 편리하고 효율적인 것을 한번 쓰면 다시는 돌아갈 생각이 없어진다. 이것이 수많은 규제의 위협 속에서도 암호화폐가 작동하는 방식이다. 이 세상에는 정부가 싫어하지만 사람들의 수요로 여전히 존재하는 시장이 없지 않다. 사람들은 필요를 충족시켜주는 더 좋은 서비스면 어떻게든 찾아내서 소비하기도 한다.
이런 마당에서 비트코인이 단지 숫자에 불과해서 무용하다는 의견은 역시 금도 단지 의미없는 금속조각이 아니냐는 비판과 별로 다를바 없다. 그 유명한 짐바브웨 달러를 생각해보면 법정화폐도 휴지가 되지 않는 건 아니다. 화폐가 법정통화이냐 혹은 손으로 만질 수 있거나 장신구로 쓰이는 금이냐 같은 것은 상황따라 평가받게 되고 역사 속에서 굳어진 것일 뿐이다. 더 중요한 것은 그것을 둘러싼 생태계가 어떤 방식으로 작동하고, 그것이 기존 것에 비해 얼마나 더 사람들에게 편리하고 잘 사용되는가 이다.
과거에 기대에 이 생태계를 보지 못하면 그저 새로운 세상이 계속 낯설 뿐이다. 암호화폐가 사라지고 다시 예전 통화로 돌아갈까? 아니다, 이제 화폐는 암호화폐보다 더 나은 디지탈 화폐로 대체될 수 있을 뿐이다. 다시 옛날의 구태의연한 화폐로 돌아가지 않는다. 어느 시장도 스마트폰을 버리고 피쳐폰으로 돌아가지 않는다. 정부가 규제해도 어떻게든 구해서 쓰게 되지 않겠는가. 그런 세상에 우리는 놓여있는 셈이다. 그래서 온 힘을 다해 이 생태계를 이해하려고 노력해야 변화를 바라보고 제대로 앞으로 나아갈 수 있다.
이와 관련하여 이더리움 작업을 자동화하는 script화가 필요하고, 가장 쉬운 방법은 geth 자체의 명령을 geth attach 라는 형태로 script화 하는 것이다. 편의상 여기서는 linux의 bash shell을 통해 geth attach 기능을 사용해 보자.
우선 먼저 가정이 필요한데, script 실행하는 같은 서버에 geth가 최소한 lightmode로 작동하고 있다고 가정했다
(물론 full mode도 작동한다)
아래 처럼 nohup으로 geth를 light mode로 띄워놓자. nohup을 사용하면 다행스럽게도 terminal을 종료해도 geth가 계속 살아있다.(geth process를 만약 kill해야 한다면, ps -ef geth 명령으로 확인하여 kill PID 명령을 통해 죽이자)
eth.sendTransaction({from: fromacc, to: toacc, value: transferval, gas: gaslimit, gasPrice: gasprice});
...
특정 계정(fromacc)에서 특정 계정(toacc)으로 gas price, gas limit을 정하여 실행한다. 이때 parameter로 사용되는 gas, gas price, gas limit이라는 것을 알아보자.
ㅇgas는 gas limit을 의미한다. 해당 송금을 요청할때 사용자가 지불할 수 있는 최대 허용 gas를 정의한다.
대개 단순 송금은 21,000이다. 특이한 것은 조금더 올려 잡아도, 이더리움 송금이 최종 완료되고 나서 남으면 돌려준다. gas는 이더리움 송금 외에도 ERC-20 방식 등 별도의 이더리움 기반 코인 등 송금할때도 사용하기 때문에, 복잡한 transaction에는 높은 gas를 부과해서 처리한다.
ㅇgas price는 사용하는 gas에 곱하여 최종의 송금비용(transaction fee)를 ethereum으로 계산할 수 있다. gas price는 높이 부과해줄수록 더 빠른 송금이 가능하다. 예를들면 일반 이더리움 송금은 21,000 gas를 소모하는데, 대략 gas price는 35~60 gwei(giga wei) 정도 된다. 참고로 wei는 이더리움을 더 작은 단위로 부르는 값인데 아래와 같이 이해하면 된다.
10^18 wei = 1 Ether
0.000000000000000001 Ether = 1 wei
1 gwei = 1 giga wei = 1,000,000,000 wei
sendTransaction에서 이 gas(gas limit), gasPrice(gas price)를 생략하면 디폴트 값으로 요청되고 다소 해당 값이 작을 수도 있어서 가끔씩 오류가 발생할 수도 있다. 그리고 송금하려는 이더리움과 이 수수료가 모두 잔고에 남아있어야 송금이 성공된다. 잔고가 부족한 경우 잔고 부족 오류가 발생한다.
에러 예시> Error: Insufficient funds for gas * price + value - 송금지정금액+송금비가 잔액을 초과한 경우
gas required exceeds allowance - gas limit이 작아서, 더 큰 gas limit 지정이 필요한 경우
gas price는 시간이 오래 걸려도 상관없으면 1 gwei를 지정해서 처리하기도 한다. 그러면 지금 이더리움 네트워크의 대략의 gas price는 얼마인지 궁금한가? eth.gasprice 를 사용해도 되고, 사이트에서도 확인할 수 있다. etherscan.io 사이트에서 각 transaction에 사용된 gas price, gas등도 확인이 가능하다
이런 스크립트는 어떻게 작성할 수 있을까? 필자는 crontab과 geth attach script를 결합해보았다.
이를 테면 계좌를 반복적으로 조회하면서, 잔액이 일정액 이상 남아있으면 송금하는 작업을 반복할 수 있다. 다만, 구동시 에러(peer 접속 중단 등)가 나면 해당 geth attach가 Exception으로 중단되는데, 다시 작업을 이어주기 위해 crontab에 주기적으로 다시 띄워준다. 다만, 해당 script에는 동일한 geth attach script 명령이 기 동작 중이면 실행되지 않고 멈추도록 앞 부분에 "ps -ef"를 활용한 방어 코드를 넣어주었다.
# crontab registration - run every 10 minutes this script # 10,20,30,40,50 * * * * cd /work/geth;/bin/bash repeatedtrasfer_eth_from_knownaddr.sh # # 2021.08.05 by neibc #
# run this script only where there is no duplicated attach job # because it tries to run sendTransaction continuously if there is no error(disconnection or something) #
이것은 이더리움 계좌를 정의하기 위한 개인키/공개키 쌍에서, 공개키를 가지고 생성한다. 공개키가 주어지면 아주 빠르게 위 계좌주소를 만들 수 있다. 공개키는 어떻게 만드는가? 개인키가 주어지면 또 빠르게 공개키를 만들 수 있다. 그러나 반대는 어렵다. 그렇게 설계되어 있다. 그러면 처음부터 3가지 단계로 다시 설명해보자.
A. 개인키 선택(1~2^256 숫자 중의 한개를 임의로 고른다. 그 숫자값을 개인키라 한다)
이더리움 지갑주소는 가상자산 거래소에서도 대신해 만들어 주며, 이를 통해 외부 입금도 받을 수 있는데, 신기하게도 한 개의 개인키가 정해지면 저렇게 지갑주소가 곧바로 확정된다. 그리고 이 개인키만 알면, 계좌를 송금할때 필요한 서명(signing)을 남길 수 있으므로 계좌를 통제하게 된다. 이 상황은, 마치 암호를 바꿀 수 없는 은행 계좌와 같다고 볼 수 있다. 그래서 사실 저 개인키를 잘 보관해야 하며, 유출되면 바로 돈을 인출당하기 전에 다른 계좌로 송금하는 수 밖에 없다.
자, 그러면 이제 개인키가 정해지면 이더리움의 지갑주소가 생긴다는 것을 알았다. 그런데 지갑주소에서 개인키를 얻는 계산은 사실 만들어 내기 어렵게 만들어져있다. 그러면 어떻게 하는가? 오래 걸리는 것은 나중에 생각하더라도, 시도해볼 수 있는 것은, 개인키를 1부터 증가시켜 공개키/지갑주소를 계속 만들면서, 실제 존재하는 이더리움 지갑주소와 비교해보면 되지 않겠는가. 소위 brute force 알고리즘이다. 처음부터 끝까지 다 만들어서 대조해보는 것이다.
A. 개인키를 1부터 시작해서 증가
B. 공개키/계좌주소를 생성
C. 생성된 계좌 주소를 기존의 주소들과 비교. (이상 반복)
일단 개인키를 아래처럼 16진수(hex) 32 bytes 문자열로 넘겨주면 공개키, 지갑주소를 생성하는 python code를 확보해보자. 이후 계속 이 소스를 활용한다. 필요한 모든 것이 이 eachecker.py에 들어가있다. ( https://github.com/neibc/ethereum/blob/main/eachecker.py ) 대표적으로 get_addr_fromhexstr 함수를 살펴보자.
Private key: 0000000000000000000000000000000000000000000000000000000000000001 Public key: 79be667ef9dcbbac55a06295ce870b..(생략)..e1108a8fd17b448a68554199c47d08ffb10d4b8 Address: 0x7e5f4552091a69125d5dfcb7b8c2659029395bdf
이 함수를 쓰면, 개인키 문자열에서 Address(계좌주소)를 알아낼 수 있게 되었다. 자 그럼 여기서 잠깐. 개인키를 1로 삼는 계좌는 과연 실제 존재할까? 우리들의 개발덕들이 해보았을까?
그렇다. 존재한다.
그리고 이 계좌는 많은 사람이 개인키가 "1" 이라는 것을 알고 있으니, 돈을 넣어가면, 바로 빼내가는 것 같다. 아래 주소에서 지갑의 거래내역을 확인할 수 있다. 계속 내 계좌로 이체하는 트랜잭션을 날리는 프로그램을 짤 수도 있겠다 싶었다. 이 경쟁에서 승리하기가 쉽지 않지만, 실제로 그렇게 경쟁하는 이들이 있다. 정말 그렇다.
2. 이제 본격적으로 지갑주소를 생성해서 비교해볼, 실제 이더리움 계좌 목록을 확보해보자.
여러가지 방법이 있지만, 제일 빠른 것은 구글 클라우드의 빅쿼리에서 공개DB로 제공한 이더리움 원장 테이블을 사용하는 방법이다. 구글이 고마울 뿐이다. 빅쿼리의 테스트 사용 정도는, 요금도 없다. 구글 계정으로 구글 클라우드의 빅쿼리 콘솔로 이동해 아래와 같이 쿼리를 수행하자. (가입절차는 필요하겠다)
SELECT `address` FROM `bigquery-public-data.crypto_ethereum.balances` WHERE `eth_balance` > 1 ORDER BY `eth_balance` DESC LIMIT 4000000
대략 3백만개의 계좌를 export할 수 있었다. 이것을 etheracclist.csv 로 받아서 이후 활용한다 (당연히 파일 이름은 아래 프로그램의 소스파일에서 적당히 고쳐써도 좋다)
여기서 처럼 3백만 계좌를 python에서 dictionary로 처리하면서 탐색하고자 하면, 6.2gb가 조금 넘는 메모리가 필요하다. 속도를 위해 메모리로 올려서 처리하기 때문이다. 1천만계좌면 21gb정도 필요하다. 메모리가 부족하면 잔고가 적은 뒤쪽의 계좌들은 삭제해버리면 된다. 이더리움 잔고가 많은 계좌순으로 소팅해서 저장했기 때문이다. 기억해두자.
3. 그러면 이제 개인키 1~0x100,000 까지 모두 위 이더리움 계좌 목록과 대사해보자
다시 조금 전에 받은 eachecker.py를 살펴보자. 그대로 실행한다. 이 프로그램은 내부에 runmode = 1 이 되면 startn 변수에 명기된 숫자부터 endn으로 명기된 숫자까지 1씩 증가시키면서, 위 계좌목록에서 일치한 것을 찾으면 BINGO를 출력하고 로그 파일에도 해당 내역을 기록한다.
$ more eachecker.py
...
runmode = 1
...
print('\nrunmode 1 : full sequence searching from startn to endn...')
실행해보면, 예상은 했으나, 돈이 있는 계좌는, 위 숫자범위에서는 전혀 매칭이 되지 않는다는 것을 알 수 있다. 개인키가 노출된 일부 계좌는(위에 소개했던 계좌) 이미 모두 돈이 인출되어, 이더리움 잔고가 0이다. 빅쿼리로 추출한 위 대상 계좌들은 1이더리움 이상 잔고를 지니는 것을 조건으로 추출했다는 것을 기억하자.
이제 고백할 때가 되었다. 이 무작위 대입 비교(brute force attack) 작업을 비유하는 가장 인상깊었던 표현은
"1부터 2의 256제곱이라는 숫자는 그것을 단순히 세는 것에만 필요한 전력이 우주의 태양과 같은 별이 일평생을 타는 에너지보다 더 많이 필요하다"
라는 것이었다.
2의 256제곱(=2^256)은 대략 10^77쯤 된다. 로또 당첨확률이 대략 10^7 번 중 한 번인 것을 염두해보면, 이 개인키를 찾는 일은 로또를 11번 연속 당첨된 것과 같은 확률의 작업에 속한다. 천만분의 1의 확률을 11번 통과하는 셈이다.
시간으로 바꿔보면 이 작업을 하는데 대략 고사양 CPU로 1초에 1만건을 처리한다고 가정하자. 초당 10^4건이다. 그러면 PC 1대로 계산한다고 하면 10^74 초 걸리고, 1년을 1억초(10^8)로 후하게 잡아도 대략 10^66 년이 걸린다. 지구의 CPU core 전체일것 같은 1000억개(10^11)쯤 동원해도 10^55 년이 걸린다. 빅뱅이후 대략 120억년(1.2 * 10^10)이 지났다고 하니, 우주가 시작되고 지금까지 계산해도 10^45번중 1개(10^10 년)를 처리한 셈이다. 그렇게 우주의 시간 계산을 10^45번 반복해야 한다.
10^77이라는 숫자를 이렇게 10^45으로 줄이는데도(33제곱이 줄었다), 전 우주의 시간과 모든 리소스를 동원해야 하는 수준인 것이다. 아직 줄여할 숫자가 45제곱이 더 남았다. 10배씩 시간을 절감할때마다 이 제곱수를 1씩 줄일 수 있다.
(개인키와 계좌주소의 관계가 1:1이 아닌 것으로 조금더 따져볼 수는 있겠으나 상황은 비슷할테고, 어려워서 넘어갔다.)
그렇다. 결론적으로 이래가지고서는 가망이 없다. 그렇지만 이대로 포기할 수는 없다!
(참고로, 타원곡선암호화의 특성상 다수의 비밀키가 공개키와 매핑되므로 2^160 = 약 10^48 정도의 시도만으로 개인키를 알아낼 수 있다고 한다 https://horizon.kias.re.kr/23225/ 그래도 역시 저 48제곱을 33제곱만큼 줄이는데, 전 우주의 시간과 리소스가 동원되는것은 같다.)
4. 특별한 형태의 개인키가 있지 않을까?
앞서 개인키 1로 된 계좌에 거래 기록이 있다는 것을 알았다. 그러면 다른 특별한 계좌 형태가 있는 것이 아닐까? 예를들면 개인키 값을 pi를 갖는 계좌는 어떨까?
있다!
Private key: 3141592653589793238462643383279502884197169399375105820974944592 #소수점 점만 뺀 32자리수. 16진수 문자열이긴 한데 이해해주자. Public key: cf9bfbdca0c087fde..(생략)..4096f2e2959f9959c88 Address: 0x7357589f8e367c2c31f51242fb77b350a11830f3
잔고가 0.002 이더리움(약 $4)이 들어있다! 라고 놀래주고 싶으나, 사실 이건 필자가 오일러를 기념하는 마음으로, 송금해둔 이더리움이다. (이더리움 원장의 자연 상수 e로 된 개인키의 계좌를 발견하고 처음 입금한 것은 필자다. 영원히 이더리움 원장에 새겨둔 셈이다) 내 이더리움 계좌를 신규 생성한 후 개인키를 e 상수로 갖는 계좌와, 개인 휴대전화번호를 갖는 두 계좌에 소액 송금해두었다. 이렇게 영원히 새긴 셈이다. 이더리움이 충분하면 여러가지 개인키를 선점(?)할 수 있다. abcdef0 만 가지고 문장을 만들까도 잠깐 고민했다
여하튼 그렇다. 이런 형태의 특별한 개인키에 숨겨둔 이더리움이 있을지도 모르겠다. 가끔씩 필자같은 마음으로 기부를 하는 사람이 있다. 위 pi 계좌의 거래를 보라.
5. 그러면 단순한 조합의 개인키로 누가 생성하지는 않았을까?
다시 조금 전에 받은 eachecker.py로 돌아가보자. 이 프로그램은 내부에 runmode = 0 이 되면 특정 패턴을 반복하는 형태로 개인키를 만들어서 비교해준다. 소스를 수정해 runmode = 1을 runmode = 0으로 고치자. 이 합성 방식은 이를테면
A. 1자리수를 0x1 ~ 0xe 까지 32번 복사하고.. (32bytes로 만들어야 하니)
B. 2자리수를 0x01 ~ 0xfe 까지 16번 복사하고.. (32bytes로 만들어야 하니)
C. 4자리수를 0x0001 ~ 0xfffe 까지 8번 복사하고.. (32bytes로 만들어야 하니)
하는 방식이다. 일단 실행을 해보자(아래는 이해를 위해 실제 소스보다 로그를 좀 더 찍은 버전임은 참고하자)
$ python3 eachecker.py
runmode 0 : repeated pattern searching... 2021-07-09 01:17:34 INFO range : 1 15 2021-07-09 01:17:34 INFO 1111111111111111111111111111111111111111111111111111111111111111 2021-07-09 01:17:34 INFO 2222222222222222222222222222222222222222222222222222222222222222 2021-07-09 01:17:34 INFO 3333333333333333333333333333333333333333333333333333333333333333 2021-07-09 01:17:34 INFO 4444444444444444444444444444444444444444444444444444444444444444 2021-07-09 01:17:34 INFO 5555555555555555555555555555555555555555555555555555555555555555 2021-07-09 01:17:34 INFO 6666666666666666666666666666666666666666666666666666666666666666 2021-07-09 01:17:34 INFO 7777777777777777777777777777777777777777777777777777777777777777 2021-07-09 01:17:34 INFO 8888888888888888888888888888888888888888888888888888888888888888 2021-07-09 01:17:34 INFO 9999999999999999999999999999999999999999999999999999999999999999 2021-07-09 01:17:34 INFO aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 2021-07-09 01:17:34 INFO bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 2021-07-09 01:17:34 INFO cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc 2021-07-09 01:17:34 INFO dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd 2021-07-09 01:17:34 INFO eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee 2021-07-09 01:17:34 INFO range : 2 255 2021-07-09 01:17:34 INFO 0101010101010101010101010101010101010101010101010101010101010101 2021-07-09 01:17:34 INFO 0202020202020202020202020202020202020202020202020202020202020202 2021-07-09 01:17:34 INFO 0303030303030303030303030303030303030303030303030303030303030303 2021-07-09 01:17:34 INFO 0404040404040404040404040404040404040404040404040404040404040404 2021-07-09 01:17:34 INFO 0505050505050505050505050505050505050505050505050505050505050505 2021-07-09 01:17:34 INFO 0606060606060606060606060606060606060606060606060606060606060606 2021-07-09 01:17:34 INFO 0707070707070707070707070707070707070707070707070707070707070707 2021-07-09 01:17:34 INFO 0808080808080808080808080808080808080808080808080808080808080808 2021-07-09 01:17:34 INFO 0909090909090909090909090909090909090909090909090909090909090909 2021-07-09 01:17:34 INFO 0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a 2021-07-09 01:17:34 INFO 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b 2021-07-09 01:17:34 INFO 0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c 2021-07-09 01:17:34 INFO 0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d 2021-07-09 01:17:34 INFO 0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e 2021-07-09 15:17:34 INFO 0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f ...
하면 무언가 random으로 하지 않았을때 생길수 있는 개인키들을 그나마 조사할 수 있다. 사실상 random으로 하면 찾아낼 확률이 없지만, 만약에 해당 개인키를 생성하는 프로그램에서 어찌하다보니 저런 패턴에 의지해서 만들게 되었다면, 훨씬 빠른 속도로 탐지가 될 수 있다. 다만, 필자도 몇시간 구동해 본 바로는 찾아지지 않았다. 병렬로 처리하고 여러대의 서버로 돌리면 상황이 좀 나아질지 모르겠다. (그래서 이더리움 지갑 주소를 생성할때는 아예 시작과 끝 영역을 충분히 제외하고, 반드시 랜덤의 개인키로 만들어야 한다)
4. 기타
이 작업을 하면서 수학적으로 이렇게 개인키/공개키/지갑주소의 무작위처럼 보이는 매핑 관계를 정의해놓을 수 있다는 사실에 적잖이 감동 받았다. 암호학 분야를 조금 더 이해한 셈이다. 단지 어떤 논리적인 코드만으로 전 우주보다 큰 매핑 관계를 구성하고 역으로 알아낼 수 없게 되어 있는것 아닌가. 그리고 막연히 생각했던 것보다, 실제 찾아보면 전혀(!) 나오지 않았다. 자신의 실제 돈이 저장되는 계좌는 랜덤의 개인키로 생성하므로, 로또 7번과 우주의 어딘가에 감춰놓은 상황과 같다. 분명히 어딘가에는 존재하지만, 전 우주를 뒤질 수는 없는 노릇이고, 우연히 발견할 확률은 충분히 작다. 알쏭달쏭한 설계지만, 여하튼 찾을 수가 없다.
그러나 여전히 아직 이 탐구 과정에서 재미있는 계좌가 있다. 마지막으로 보고 가야할 계좌가 바로 이 녀석이다.
"0xdcc703c0E500B653Ca82273B7BFAd8045D85a470"
이 계좌는 개발자의 실수로 공개키에 아무것도 할당하지 않았을때 생성되는 지갑주소이다. 원래는 개인키에서 생성한 공개키로 주소를 만들어야 하는데, 변수를 잘못 넘기는 바람에, 아무값도 없는 변수를 넘기는 경우가 있고, 그때 생기는 지갑주소이다. 그런데 이 지갑주소에 무려 815 이더리움이 송금되어 있다. 거래소에서 송금 주소를 코딩할때 실수해서 넘어갔을 테다. 결국 이 계좌는 확실하게 아무도 개인키를 모르는 계좌이다. 어떤 개인키를 가지고 만들어야 공개키가 0이 되는지 모르기 때문이다. 이것만 알아내도 815 이더리움을 가질 수 있어서, 다른 문제보다는 쉽다. (다른 녀석들은 공개키도 알수가 없고, 계좌 주소만 알기 때문인데, 이건 반쯤은 왔다)
반복되는 개인키 검색으로 만약 이 지갑주소가 나오는 개인키를 찾아낸다면 아무런 양심의 가책없이 내 계좌로 송금하면 된다!
그리고 여기까지 오면서 필자가 깨달은 사실이 있다. 이더리움의 계좌 보안체계는 돈을 우주보다 넓은 광활한 어딘가에 숨겨놓는 개념이다. 일반적인 암호로 보관하는 것과는 다르다. 광활한 우주 어느 좌표(개인키)에 숨겨놓는 것이다. 위치를 모르면 찾는 방법은 그걸 하나하나 방문해보는 수 밖에 없다. 그래서 그 좌표를 들키면 아주 쉽게 꺼내갈 수 있다. 그런데 들키지만 않으면 분명히 존재하는데도, 마치 존재하지 않는 것처럼 숨겨둘 수 있다. 그리고 그 좌표를 나도 잊으면, 실질적으로 영원히 찾을 수 없다. 그 좌표(정보)를 알고 있는 사람이 그 돈의 주인다. 그게 이 지갑 체계의 비밀이다.
이런 단순하지만 효과적인 방법을 고안하다니. 처음 개발했을 사람에게 놀랄 따름이다. 물론 과거로부터 잘 알려진 사실을 사토시 나카모토가 응용한 것이지만 말이다.
5. 정말 개인키를 찾아내면 어떻게 하죠?
사실은 이 과정을 진행하면서, 개인키를 찾는 상상은 했지만, 실제로 찾아낸 후 어떻게 할지는 고민이 되었다. 암호화폐의 계좌 소유주라는 개념은 불친절하게도 시스템 입장에서는 개인키를 소유하고 있느냐로 판단하기 때문에, 실제로 도덕적인 문제가 좀 누그러지기도 했다. 그렇다고 계좌 주인에게 물어볼 방법도 없는게 사실이다(역시 실명화된 체계가 아니기 때문에, 알 수 없기 때문이다)
그러나 위에서 소개한 계좌처럼, 아무도 개인키를 모르는 혹은 누구도 괴롭힐 일이 없는 개인키가 잊혀진 계좌라고 가정해보자.
그러면 해당 알아낸 개인키로 계좌를 개설하여 내 계좌로 송금하면 된다. geth같은 이더리움 클라이언트를 설치해 이 과정을 수행할 수 있다. 이건 역시 조금은 복잡하기 때문에 링크로 안내하자. syncmode를 light로 실행하면 10분안에 필요한 동기화가 완료되고, 잔액 조회 및 송금 명령을 실행할 수 있다. ( https://infoengineer.tistory.com/52 ) geth를 main net에 붙이기 전에 우선 해당 발견한 개인키로 계좌는 생성해 두어야 한다. 아래 명령으로 개인키 32 bytes 16진수 문자열을 통해 가능하다.
i 16:54:29 ethminer Configured pool 127.0.0.1:8545 i 16:54:29 ethminer Selected pool 127.0.0.1:8545 i 16:54:29 ethminer Established connection to 127.0.0.1:8545 i 16:54:29 ethminer Spinning up miners... cu 16:54:29 cuda-0 Using Pci Id : 01:00.0 GeForce GTX 1070 (Compute 6.1) Memory : 6.90 GB X 16:54:29 ethminer Got code:-32000 message:no mining work available yet from 127.0.0.1:8545 m 16:54:34 ethminer 0:00 A0 0.00 h - cu0 0.00 ..
cu 17:00:51 cuda-0 Job: 2fe6ca74… Sol: 0x5e2c787b34bdc0a0 i 17:00:51 ethminer **Accepted 9 ms. 127.0.0.1:8545 i 17:00:51 ethminer **Accepted 29 ms. 127.0.0.1:8545 cu 17:00:51 cuda-0 Job: 2fe6ca74… Sol: 0x5e2c787b34d2809f cu 17:00:51 cuda-0 Job: 2fe6ca74… Sol: 0x5e2c787b34d451d2 i 17:00:51 ethminer **Accepted 2 ms. 127.0.0.1:8545 i 17:00:51 ethminer **Accepted 12 ms. 127.0.0.1:8545
..
이후 nvidia-smi같은 tool로 GPU가 실제 작동하고 있는지 확인한다. GPU로 채굴하는 것을 알 수 있다.
B. 다음에는 사설로 이더리움을 구성하기 위해 genesis파일을 작성하여 geth를 실행해보자.
ethereum 사설망은 PoW방식의 ethash모드와 좀 다른 형태(Proof of Authority)의 고속의 clique 방식이 있는데, 보통 ethereum에서는 clique을 추천한다. 하지만 여기서는 실제 사용되는 ethash모드로 진행하도록 하자.(대규모 개발 시험을 위해서는 clique로 하면 편하겠다. 상세 내용이나 파라메터에 대해서는 해당 공식 문서를 참조한다.(https://geth.ethereum.org/docs/interface/private-network)
ethash모드로 사설 이더리움망을 구성하려면 json파일을 하나 구성해야 하는데, genesis.json이라고 칭해보자.
그리고 아래와 같이 init을 해준다. 이렇게 하면 위 genesis파일에 기초하여 필요한 기초 파일을 생성해주고 초기 셋팅을 해준다. 초기 배정 금액 alloc/balance는 wei단위(10^18 wei = 1 이더리움)라서 100이더리움(100000000000000000000 wei)이 배정되었다
$ cd /work/geth-alltools-linux-amd64-1.10.4-aa637fd3
> primary = eth.accounts[0]; #여기서 만든 첫번째 계좌 "0x2d75914e826c023beaa98a4c05db1be808c4aaa3" > secondary = eth.accounts[1]; #여기서 만든 두번째 계좌 "0x2276b96e62c9394d76fbdc59f451688a55f99d1f"
> miner.setEtherbase(primary); #이체 채굴을 하면 첫번째 계좌로 돈이 들어간다.
> miner.start(); #채굴을 시작하자. 채굴을 하지 않으면 거래가 발생하지 않고 이더리움이 생기지도 않는다.
> personal.unlockAccount(primary); #송금을 하기 위해 첫번째 계좌의 암호를 입력해보자.
(하지만 정상적인 full mode sync를 위해서는 16gb이상, 500gb급 ssd가 필요하다. 사양이 떨어지면 light mode를 사용하자.)
이더리움 노드 서버 혹은 클라이언트인 geth를 다운로드 받아 구동하면 된다. 여기서는 geth 1.10.4 버전 ubuntu linux에서 작동시켰다. 다만, geth는 go언어로 만들어져서 거의 모든 OS를 지원하므로, 다른 OS에서도 아래와 유사한 방식으로 구동할 수 있을 것이다.
여기서 Geth 1.10.4 Linux 64bit 버전을 다운로드 받자(Tools가 같이 있는 버전을 받아도 좋다. 아래는 geth & tools를 기준으로 한다.) 필자는 Ubuntu 18.04기반으로 작업하였다. 다운로드를 받아 적절한 디렉토리에 압축을 풀면 go로 된 단순한 명령어들을 볼 수 있다.
아래는 /work/gethdata에 여러가지 데이터를 저장하고, /work/밑에 프로그램을 압축 푼다고 가정해보자.
$ mkdir /work/ #프로그램 설치 폴더 생성
$ mkdir /work/gethdata #데이터 폴더 생성, 각종 정보 파일과 계좌를 만들때의 정보 등을 담게 된다
$ cd work
$ tar xvfz geth-alltools-linux-amd64-1.10.4-aa637fd3.tar.gz
#sendTransaction의 nonce는 총 출금한 개수다. 처음에는 0, 그 다음에는 1을 붙이자.
다만 main net에 제대로 참여하기 위해서는 geth가 전체 blockchain의 block들이 전부 sync(다운로드)되어야 한다.
geth 홈페이지에 따르면 4 core / 16gb ram / 500gb 이상의 ssd를 추천사양으로 이야기한다. (필자가 100%정도 sync되었을때 용량이 360GB였다) 모두 sync하는 데에는 2~3일 정도 소요되었다. ssd를 추천하는 이유는 sync를 제대로 하기 위해서는 고속의 I/O가 필요하기 때문이라고 한다. sync여부는 위 geth console에서 아래와 같이 확인 가능하다
> eth.syncing
{
currentBlock: 12780722
highestBlock: 12780722
..
}
이렇게 currentBlock과 highestBlock이 같은 값이면 sync가 100%완료된 상태이다. 그런데 disk I/O가 충분히 빠르지 않으면 이것이 100%sync가 잘 되지 않는다.
geth는 내부적으로 fast mode, full mode, light mode 3가지가 존재하는데 디폴트는 fast mode이다. hardware사양이 낮고 적은 용량만 가진 서버에서는 light mode를 확인하여 해당 방식으로 구동하는 것이 가능하다. 10분에 기초 동기화가 되고 300mb정도의 용량만으로도 유지가 가능하다고 한다. 다만 이 light mode는 다른 노드의 도움을 받아야만 정상적인 처리가 가능하여 해당 노드에 의존하므로 처리 속도가 느리다고 알려져 있다. 채굴도 불가능하다. light mode는 geth실행시에 --syncmode "light" 만 추가하면 되어 소형 기기용으로 많이 추천되고 있는 모양이다.