즐코

POA 기반 프라이빗 네트워크 구축 (feat. docker) 본문

BlockChain

POA 기반 프라이빗 네트워크 구축 (feat. docker)

YJLEE_KR 2022. 8. 13. 18:22

dApp store 프로젝트 시작 전 자유로운 테스트를 위해 프라이빗 네트워크를 구축해보았다. 

ganache나 다른 Rinkeby같은 testnet을 써도되지만 가나쉬는 서버를 끄면 계정정보 및 블록 정보가 초기화 되버리고, 다른 테스트넷 같은 경우 테스트용 이더를 받아와야하고, 거래 성사 시 시간이 조금 걸리는 단점이 있어 테스트하기엔 살짝 불편하다.

나중에 플젝 진행 시 거래, 판매 등의 테스트도 수월하게 하고, 도커도 처음 사용해볼겸,, 겸사겸사 만들어보았다.

 

합의 알고리즘

모든 노드가 동일한 하나의 체인을 가질 수 있게끔 해주기 위해서 합의 알고리즘을 선택해야 하는데 대표적으로 POW, POS, POA 등이 있다. 메인넷은 POW 작업증명 방식을 사용하는데, geth는 프라이빗 네트워크 구성 시 Clique라는 권위증명 알고리즘도 지원해준다.

이는 POW 보다 리소스 낭비가 적기 때문에 프라이빗 테스트넷에 적합하며 Rinkeby와 Goril 에서도 이 방식을 쓴다고 한다.

 

- Ethash : Geth의 POW 알고리즘

- Clique : POA 알고리즘 - 인증된 signers만이 새로운 블럭을 생성할 수 있다. 이 인증된 authorized signers는 제네시스 블럭 설정의 초기 세팅 값에 들어간다. (genesis.json의 extraData 속성) 투표 매커니즘을 통해 이 Signers들이 승인받기도 하며 signers의 역할이 취소되기도 한다. 즉 블록체인이 동작하는 중에도 이 signers들은 바뀔 수 있다고 한다.

 

도커의 기본 사용법은 현재 주제에서 엇나가는 내용이므로 해당 포스팅에선 자세히 다루진 않는다.

 

1. ethereum/client-go 도커이미지 가져오기

dockerhub에서 제공해주는 ethereum/client-go 라는 이미지를 pull해와서 사용했다.

예전에는 이 image가 없어서 각 컨테이너마다 ubuntu 이미지를 깔아서 geth 설치를 해줬어야 했다고 하니 참 다행이다.

$ docker pull ethereum/client-go

 

이제, private_net 디렉토리를 하나 만들고 작업을 시작한다!

디렉토리 구조는 아래와 같다. 프라이빗 네트워크는 서로가 연결되어있는 제한된 수의 이더리움 노드들로 이뤄져있다.

이 모든 각각의 노드들은 개별적인 data directory, 블록데이터를 보관하는 폴더 ( — datadir) 을 가지고 있어야 한다. 이 모든 노드들은 서로를 알아야하고, 정보를 교환하며 초기 상태와 공통의 합의 알고리즘을 공유해야함을 인지하고 작업을 시작한다.

 

2. 계정 생성 

본격적으로 제네시스 설정 파일을 만들기 전에 계정들을 생성해주려고 한다. 

왜냐하면 제네시스 파일을 만들 때 아래의 계정정보를 넣어줘야하기 때문이다.

 

- 이더를 선지급하고 시작할 계정들

 - signers 즉, 블록 검증 및 채굴을 담당할 노드 계정들

 

나는 각각의 노드 폴더 상에 들어가서 아래의 명령어로 계정들을 keystore라는 디렉토리 안에 생성해주었다.

$ geth account new --datadir ./keystore

 3. 제네시스 블럭을 위한 genesis.json 파일 만들어주기

같은 블록체인 네트워크에 있기 위해선 제네시스 블럭이 같아야 함을 알고 있다.

genesis.json을 puppeth를 사용하여 만들어준다. 복사해서 각 노드 폴더 안에 넣어줄 것이므로 그냥 디렉토리 루트내에다가 만들어줬다. 터미널 상에 puppeth 치고 network 이름 설정 후 아래 캡쳐대로 진행해준다. 

 

중요한건 두가지이다.

 

1/ 작업증명 알고리즘 (consensus engine) Clique 로 선택

2/ 블럭의 검증 및 채굴을 위한 계정 (sealer) 설정 부분 : 위에서 만들어준 계정번호를 넣어준다. 

     => 나는 노드가 총 4개지만 bootnode와 node1의 coinbase 계정만 넣어주었다. 

 

 

private_net 디렉토리 최상단에 testing이라는 폴더 내에 genesis.json 파일이 생겼다,

중요한건 extraData 속성이다. 아래 캡쳐처럼 제네시스 설정파일 만들 때 signer로 지정해준 계정 2개가 연속해서 들어가있음을 확인할 수 있다. 계정간의 특별한 구분자없이 그냥 연속으로 들어가는듯하다. 

 

genesis 파일 내용 중 수정해주거나 추가해줄 부분은 추가해주고 이 완성한 genesis.json 파일을 전부 bootnode 부터 node3 까지 복사해서 넣어준다. 

 

나는 처음에 이 ethereum/client-go 이미지를 가져와서 Dockerfile을 따로 만들어서 제네시스 파일을 init해주는 이미지를 따로 만들었었다. 근데 굳이 그렇게 할 필요없이 geth 이미지 그대로 컨테이너를 생성하는 게 더 편할 것 같아 컴포즈 파일을 아래와 같이 두개 만들어주었다. 

 

1/ docker-init.yml : genesis 설정 파일 기반으로 각 컨테이너(노드)별 초기화 작업

2/ docker-compose.yml : 각 컨테이너(노드)별로 geth 실행 작업

4. docker-init.yml 파일 작성

내 로컬 디렉토리들처럼 bootnode, node1, node2, node3, node3 컨테이너를 각각 만들어줄 예정이다.

컴포즈 파일 작성 시 중요한건 volumes 부분과 command 부분이다.

volumes 속성

내 로컬 디렉토리와 컨테이너 내의 디렉토리를 매칭, 동기화 시켜준다. 

[내 로컬 디렉토리 경로(현재 작업공간)] : [ 컨테이너 경로 ] 

geth에서 자동으로 생성해준 .ethereum 폴더내에 genesis 파일이 있어야하므로 아래 코드대로 작성해준다.

이렇게 하면 위에서 내 로컬 디렉토리에 미리 넣어둔 genesis파일이 동기화 되어 컨테이너 내의 /root/.ethereum 폴더 내에 들어가게 된다!

volumes : ./bootnode:/root/.ethereum

command 속성

컨테이너 생성 이후에 컨테이너 터미널 내에서 실행할 명령어이다.

genesis.json을 기준으로 제네시스 블럭 초기화 작업을 해준다. 원래는 geth init [제네시스 파일 경로]를 쳐줘야 하지만 기본적으로 ethereum/client-go 이미지를 pull해와서 작업하므로 geth 명령어를 생략해줘도된다. 

command: init /root/.ethereum/genesis.json

전체 코드

version: "3.7"

services:
  bootnode:
    image: ethereum/client-go:latest
    tty: true
    container_name: boot-node
    volumes:
      - ./bootnode:/root/.ethereum
    command: init /root/.ethereum/genesis.json

  node1:
    image: ethereum/client-go:latest
    tty: true
    container_name: node1
    volumes:
      - ./node1:/root/.ethereum
    command: init /root/.ethereum/genesis.json

  node2:
    image: ethereum/client-go:latest
    tty: true
    container_name: node2
    volumes:
      - ./node2:/root/.ethereum
    command: init /root/.ethereum/genesis.json

  node3:
    image: ethereum/client-go:latest
    tty: true
    container_name: node3
    volumes:
      - ./node3:/root/.ethereum
    command: init /root/.ethereum/genesis.json

도커 컴포즈 터미널 명령어 

$ docker compose -f docker-init.yml up

아래와 같이 각 컨테이너별로 Successfully wrote genesis state 가 콘솔창에 뜨고, 각 컨테이너와 내 로컬 디렉토리들이 매칭되어 geth 라는 폴더가 자동으로 생겼다!

 

5. bootnode 설정해주기!

노드 환경 설정 및 초기화를 하고나면 peer-to-peer 네트워크를 세팅해줘야한다. 이 때 boot node가 필요하다.

다른 노드들이 네트워크 참여 시 entry point (진입점)가 되어줄 노드를 부트 노드라고 한다.

p2p 네트워크를 위해 모든 참여 노드들이 연결될 수 있게끔 허브, 중간다리가 되어주는 것이다. 

$ bootnode -genkey boot.key
# 현재 디렉토리 상에 boot.key가 생성된다. 
$ cat boot.key
# cat 명령어 없이 위에서 생성된 boot.key 파일 내의 값을 복사해서 써도 된다.

 

 

6. docker-compose.yml 파일 작성

docker.init.yml 파일은 init해줄때만 실행해주는 것이므로 제네시스 파일을 변경해서 초기화가 필요하지 않는 이상 쓰지 않을 것이다. 이젠 각 컨테이너의 geth 실행을 도와줄 compose 파일을 작성한다.

 

docker-compose파일의 command 속성은 무조건 컨테이너에 만들어진 디렉토리에서 실행하는 명령어임을 인지하고 작성해야한다. 

그래야 경로 설정에 있어서 헷갈리지 않는다. 

 

각 컨테이너(노드)별로 커맨드 라인이 살짝 다르다. 

 

1/ bootnode 설정

 

우리의 p2p 연결을 도와줄 노드이다. 위에서 bootnode key를 괜히 만든게 아니다. 이 boot.key값을 통해 해당 노드의 주소값인 enode값을 고정시킬 수 있다. 이 고정된 부트노드의 enode값을 네트워크 내 모든 노드들이 바라볼 것이다. 

--nodekeyhex="위에서 만든 boot.key값"

signers 계정 unlock 설정

제네시스 설정 시 extraData에 들어간 signers 계정은 마이닝 작업을 해줘야 한다.

그렇기 때문에, 마이닝 시 일일이 마이너 계정 unlock을 시켜주기보단 geth를 실행하자마자 계정을 unlock 하면서 시작하는게 편하므로, --unlock="계정넘버"  옵션을 넣어준다.

이 때 계정을 만들때 넣어주었던 비번도 넣어줘야한다. --password 비번을 명령어 상에 다이렉트로 넣는 게 아니라 비번을 가지고 있는 파일의 경로를 넣어줘야 한다. 그렇기 때문에 unlock해줄 signer계정을 가진 노드별로 password 파일을 따로 만들어준다. 난 그냥 확장자명 없이 password란 파일 안에 각 계정 비번을 써주었다. 이때 password파일은 내 로컬컴퓨터 디렉토리 내에 만들어주었지만 command 명령어 상의 경로는 위에서 언급했듯이 내 로컬 컴퓨터 디렉토리 경로가 아닌 컨테이너 내의 경로를 넣어주는 것이다.

--unlock="0xa67520e2c4ffc1effca9ce927bf126a12a85fd87"
--password="/root/.ethereum/password"

나머지 속성들 설정 기본 port 설정 및 http port 설정 및 각 디테일 설정 (아래 전체 코드 참고)

아래 두 포트넘버가 헷갈리지 않도록 주의한다.

 

 --port :  p2p 네트워크 내의 각 노드의 연결포트

 --http.port :  http.rpc 통신을 위한 연결포트

 

이때 docker-compose 내의 속성 중에 port가 있는데, 네트워크 내 각 노드의 port는 tcp/udp 둘다 만들어줘야한다. 안그러면 geth 실행 시 처음만 실행되고 missing UPD port 에러를 던지면서 geth 실행이 종료된다. 그렇기 때문에 아래와 같이 방화벽은 포트 30303에서 UDP, TCP 둘 다 허용해줘야한다. 

    ports:
      - "30300:30300"
      - "30300:30300/udp"
      - "8545:8545"

 

이렇게 bootnode 컨테이너 부분까지만 작성해주고, compose up 해준다.

$ docker compose -f docker-compose.yml up

이러면 부트노드만 실행이 되고 docker dashboard의 container 터미널을 켜준다.

geth 콘솔창 실행해주고 enode값을 가져온다. 

$ geth attach
$ admin.nodeInfo.enode

이 때 포인트는 뒤 쪽에 @ 부터 컴포즈 파일 상에 적어둔 host-name과 port 넘버를 넣어서 복사해둔다.

자세히 말하면 아래처럼 바꾸란 뜻이다. 

enode://0ee3ee5993762d03be5aab9c15f6dc781fcd8e57fd36d20d3b219fd93962b752d75f9495652f229f8f5cd74f9a84e47b67bc5604725aa64ef776b68da4f40807@127.0.0.1:30300?discport=0 에서 @ 이후를 지워주고

enode://0ee3ee5993762d03be5aab9c15f6dc781fcd8e57fd36d20d3b219fd93962b752d75f9495652f229f8f5cd74f9a84e47b67bc5604725aa64ef776b68da4f40807@geth-bootnode:30300 으로 바꿔준다.

 

이 bootnode의 enode값을 복사해놓고 다시 로컬 터미널로 돌아와서 해당 compose파일을 내려서 우선 컨테이너를 삭제해준다. 

컴포즈 파일을 수정하고 다시 up시키기 전엔 해당 컴포즈 파일을 습관적으로 down시켜주고 시작하는 게 좋다.

$ docker compose -f docker-compose.yml down

 

2/ node1~node3 설정

 

bootnode와 연결하기 위해서 위에서 복사해둔 enode값을 가져와서 --bootnodes 속성값으로 넣어준다.

각 노드들은 command의 --port, --http.port 속성을 다 다르게 해준다. 즉, networkid만 통일시켜주는 것이다.

--bootnodes="enode://0ee3ee5993762d03be5aab9c15f6dc781fcd8e57fd36d25d3b219fd93962b752d75f9495652f229f8f5cd74f9a84e47b67bc5604725aa64ef776b68da4f40807@geth-bootnode:30300"

전체코드

version: "3.7"

services:
  bootnode:
    image: ethereum/client-go:latest
    container_name: boot-node
    hostname: geth-bootnode
    command: --datadir /root/.ethereum
      --allow-insecure-unlock
      --syncmode="full"
      --networkid=7979
      --nodekeyhex="291c6c817274d8ccf13ba4145d689fd9b845db136e155f2a68f3d404042f35a7"
      --nodiscover
      --http
      --http.addr="0.0.0.0"
      --http.api="eth,web3,net,txpool,admin,personal,miner"
      --http.corsdomain="*"
      --http.port=8545
      --port=30300
      --unlock="0xa67520e2c4ffc1effca9ce927bf126a12a85fd87"
      --password="/root/.ethereum/password"
    ports:
      - "30300:30300"
      - "30300:30300/udp"
      - "8545:8545"
    volumes:
      - ./bootnode:/root/.ethereum

  node1:
    image: ethereum/client-go:latest
    depends_on:
      - bootnode
    command: --datadir /root/.ethereum
      --networkid=7979
      --bootnodes="enode://0ee3ee5993762d03be5aab9c15f6dc781fcd8e57fd36d25d3b219fd93962b752d75f9495652f229f8f5cd74f9a84e47b67bc5604725aa64ef776b68da4f40807@geth-bootnode:30300"
      --allow-insecure-unlock
      --syncmode="full"
      --http
      --http.addr="0.0.0.0"
      --http.api="eth,web3,net,txpool,admin,personal,miner"
      --http.corsdomain="*"
      --http.port=8546
      --port=30301
      --unlock="0x6937479399d4532d0a7b6ead6601e55bf913dd75"
      --password="/root/.ethereum/password"
    ports:
      - "8546:8546"
      - "30301:30301"
      - "30301:30301/udp"
    volumes:
      - ./node1:/root/.ethereum

  node2:
    image: ethereum/client-go:latest
    depends_on:
      - bootnode
    command: --datadir /root/.ethereum
      --networkid=7979
      --bootnodes="enode://0ee3ee5993762d03be5aab9c15f6dc781fcd8e57fd36d25d3b219fd93962b752d75f9495652f229f8f5cd74f9a84e47b67bc5604725aa64ef776b68da4f40807@geth-bootnode:30300"
      --allow-insecure-unlock
      --syncmode="full"
      --http
      --http.addr="0.0.0.0"
      --http.api="eth,web3,net,txpool,admin,personal,miner"
      --http.corsdomain="*"
      --http.port=8547
      --port=30302
    ports:
      - "8547:8547"
      - "30302:30302"
      - "30302:30302/udp"
    volumes:
      - ./node2:/root/.ethereum

  node3:
    image: ethereum/client-go:latest
    depends_on:
      - bootnode
    command: --datadir /root/.ethereum
      --networkid=7979
      --bootnodes="enode://0ee3ee5993762d03be5aab9c15f6dc781fcd8e57fd36d25d3b219fd93962b752d75f9495652f229f8f5cd74f9a84e47b67bc5604725aa64ef776b68da4f40807@geth-bootnode:30300"
      --allow-insecure-unlock
      --syncmode="full"
      --http
      --http.addr="0.0.0.0"
      --http.api="eth,web3,net,txpool,admin,personal,miner"
      --http.corsdomain="*"
      --http.port=8548
      --port=30303
    ports:
      - "8548:8548"
      - "30303:30303"
      - "30303:30303/udp"
    volumes:
      - ./node3:/root/.ethereum

 

전체 코드 저장 후 다시 컴포즈 진행해주면, 이렇게 컨테이너가 4개 쫘라락 생긴다. 즉 한 블록체인 네트워크 상에 총 4개의 노드가 생긴 것이다!

 

이제 노드별로 터미널 창을 다 켜서 geth attach 를 실행시켜준다.

boot-node 의 게스 콘솔창에 admin.peers를 치면 node1 부터 node3까지의 3개 노드가 나온다!

굳이 admin.addPeer() 를 안해도 연결이 된 것이다.

 

더 자세한 연결 확인은 아래와 같이 할 수 있다. 캡쳐하기 귀찮아서 말로 설명한다..

 

1/ boot-node 콘솔 창에서 admin.peers로 확인

2/ signer 노드로 설정한 노드들만 miner.start(8) 해서 eth.blockNumber 모든 노드 콘솔 창에서 쳐서 블럭 넘버가 동일한지 확인

* 이 때 signer 노드들 중 하나만 mine을 진행하는 게 아닌 모두가 mining.start()를 해줘야 채굴이 된다.

3/ 특정 노드에서 transaction 발생시키고 각 노드들마다 txpool이 똑같이 업데이트되는지 확인

* 나는 첫번째 거래는 그걸 발생시킨 노드쪽에서만 txpool이 업데이트 되고 나머지 노드들의 txpool은 업데이트 되지않다가, 이때 채굴을 해주고 다시 두번째 거래를 일으키면 그땐 다른 노드들도 txpool이 업데이트 되는 이상한 오류가 발생함... 아직 해결하지 못한 상태지만 geth 실행 시 속성을 추가해주면 되지않을까 생각한다. 내일까지 마저 고쳐볼 예정

 

=> 해당 오류 수정은 signer 노드들 실행시에 --mine 옵션을 추가해줘서 geth 실행과 동시에 채굴이 일어나게끔 하는 것이다. 이렇게 하면 일일이 miner.start() + miner.stop() 으로 채굴을 시작하고 멈추고를 안해도 되는데, 생각해보니까 블록체인 네트워크는 채굴을 멈추지 않고 하는게 정상인데 나는 거래 발생 시 txpool이 모든 노드들과 sync가 되는지 확인해보려고 억지로 채굴 시작/중단을 컨트롤했기때문에 발생한 오류인 것 같다... 

 

Comments