Ethereum Bootnode 기능 확인

2023. 6. 14. 10:07Blockchain/Ethereum

728x90
반응형

※ 원글 작성 : 22년 5월 4일

Bootnode

부트노드는 이더리움에 참여하려는 노드가 처음으로 연결되는, 이더리움 code안에 주소가 박혀있는 노드이다. 참여 노드는 부트노드에 연결하여 다른 노드들을 find 하고 다른 이더리움 노드의 주소(enode 식별자)를 전달받아 해당 노드와 연결된다. 블록 동기화도 찾아낸 노드들로부터 정보를 얻어서 진행하기 때문에 부트노드의 연결은 이더리움 생태계 안에 들어가기 위한 시작점이라고 볼 수 있다.

부트노드가 새로 참여하는 노드에게 주변 live 노드 정보를 전달해 준다면, 개발자가 임의로 healthy한 노드를 찍어서 전달해 줄 수 있지 않을까? Geth 내에 부트노드 코드를 보며 어떠한 방식으로 노드간 연결이 진행되는지 분석해보고, 실제로 임의의 노드를 전달해본다.

Go-ethereum source

일반적으로 bootnode를 실행 시에

> bootnode
Fatal: Use -nodekey or -nodekeyhex to specify a private key

부트노드 키를 요청한다.

bootnode -genkey bootnode.key
bootnode -nodekey bootnode.key -verbosity 9

의 명령어를 통해 부트노드를 실행할 수 있다. 해당 명령어를 받는 source를 확인하면, geth 내에 cmd/bootnode/main.go 인것을 확인할 수 있다.

선언된 변수들을 먼저 확인해보면,

func main() {
    var (
        listenAddr  = flag.String("addr", ":30301", "listen address")
        genKey      = flag.String("genkey", "", "generate a node key")
        writeAddr   = flag.Bool("writeaddress", false, "write out the node's public key and quit")
        nodeKeyFile = flag.String("nodekey", "", "private key filename")
        nodeKeyHex  = flag.String("nodekeyhex", "", "private key as hex (for testing)")
        natdesc     = flag.String("nat", "none", "port mapping mechanism (any|none|upnp|pmp|extip:<IP>)")
        netrestrict = flag.String("netrestrict", "", "restrict network communication to the given IP networks (CIDR masks)")
        runv5       = flag.Bool("v5", false, "run a v5 topic discovery bootnode")
        verbosity   = flag.Int("verbosity", int(log.LvlInfo), "log verbosity (0-5)")
        vmodule     = flag.String("vmodule", "", "log verbosity pattern")

        nodeKey *ecdsa.PrivateKey
        err     error
    )

임을 알 수 있는데, 위에서 cli로 실행 시 사용한 flag인 genkey, nodekey, verbosity 등을 확인할 수 있다. 해당 source를 차례차례 봐보자

    natm, err := nat.Parse(*natdesc)
    if err != nil {
        utils.Fatalf("-nat: %v", err)
    }
    switch {
    case *genKey != "":
        nodeKey, err = crypto.GenerateKey()
        if err != nil {
            utils.Fatalf("could not generate key: %v", err)
        }
        if err = crypto.SaveECDSA(*genKey, nodeKey); err != nil {
            utils.Fatalf("%v", err)
        }
        if !*writeAddr {
            return
        }
    case *nodeKeyFile == "" && *nodeKeyHex == "":
        utils.Fatalf("Use -nodekey or -nodekeyhex to specify a private key")
    case *nodeKeyFile != "" && *nodeKeyHex != "":
        utils.Fatalf("Options -nodekey and -nodekeyhex are mutually exclusive")
    case *nodeKeyFile != "":
        if nodeKey, err = crypto.LoadECDSA(*nodeKeyFile); err != nil {
            utils.Fatalf("-nodekey: %v", err)
        }
    case *nodeKeyHex != "":
        if nodeKey, err = crypto.HexToECDSA(*nodeKeyHex); err != nil {
            utils.Fatalf("-nodekeyhex: %v", err)
        }
    }

import된 nat의 경우, p2p/nat에 존재한다. 네트워크를 아는 사람이면 들어본적 있는 그 NAT이며, NAT-PMP나 UPnP(Universal Plug and Play)등의 네트워킹 프로토콜 설정을 먼저 진행한다.

switch 문에서 cli에서 입력받은 bootnode flag를 분기처리 한다. 크게 두 부분으로 나눌 수 있는데, genkey를 원할 경우 bootnode key를 생성하고, 생성된 bootnode key를 통해 실행을 하는 경우 crypto/crypto.go 내에 있는 ECDSA 라이브러리로 secp256k1 curve를 사용한 개인키를 생성하는 단계로 넘어간다.

    if *writeAddr {
        fmt.Printf("%x\n", crypto.FromECDSAPub(&nodeKey.PublicKey)[1:])
        os.Exit(0)
    }

    var restrictList *netutil.Netlist
    if *netrestrict != "" {
        restrictList, err = netutil.ParseNetlist(*netrestrict)
        if err != nil {
            utils.Fatalf("-netrestrict: %v", err)
        }
    }

writeAddr은 fmt로 주소를 확인하기 위함이니 넘어간다. netrestict option은 IP 기반으로 필터링을 할 수 있어서 geth --netrestrict 192.168.0.1/24처럼 geth가 LAN만 연결을 할 수 있으며, comma를 통해 여러개의 network list를 선택해 지정된 IP 네트워크로만 통신을 제한할 수 있다.

    addr, err := net.ResolveUDPAddr("udp", *listenAddr)
    if err != nil {
        utils.Fatalf("-ResolveUDPAddr: %v", err)
    }
    conn, err := net.ListenUDP("udp", addr)
    if err != nil {
        utils.Fatalf("-ListenUDP: %v", err)
    }
    realaddr := conn.LocalAddr().(*net.UDPAddr)
    if natm != nil {
        if !realaddr.IP.IsLoopback() {
            go nat.Map(natm, nil, "udp", realaddr.Port, realaddr.Port, "ethereum discovery")
        }
        if ext, err := natm.ExternalIP(); err == nil {
            realaddr = &net.UDPAddr{IP: ext, Port: realaddr.Port}
        }
    }

기본 listening port는 30301이며(addr 옵션으로 port 변경 가능) port number까지 확정이 되었으면 해당 IP/port로 UDP listening을 진행한다.

UDP로 bootnode가 listen할 시 2가지 버전이 존재한다. (v4, v5) 여기서는 v5가 아닌 v4를 사용할 것이다.

    else {
        if _, err := discover.ListenUDP(conn, ln, cfg); err != nil {
            utils.Fatalf("%v", err)
        }
    }

discover.ListenUDP 기능 내에서 참여 노드가 근처 연결 노드를 물어볼 시 findnode를 핸들링하는 함수 내에 기능을 추가하여, bootnode가 전달하고 싶은 노드 정보를 임의로 변경하여 전달 가능하다.

Node 정보 전달 handling

// discover/v4_udp.go

func (t *UDPv4) handleFindnode(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, mac []byte) {

...
}

bootnode는 ping/pong 후 노드의 live를 확인하고 노드 정보를 findnode 함수를 통해 다른 노드 정보를 전달한다.

Private 등 블록체인 운영이 가능할 시에 위 함수를 수정해보면, bootnode 운영자가 원활한 통신 상태등을 고려해 healthy한 노드를 주기적으로 모니터링하여 연결을 추천해주는 방식으로 새로 참여하는 노드들에게 연결할 노드 정보를 전달해 줄 수 있을 것이다.

func (t *UDPv4) handleFindnode(h *packetHandlerV4, from *net.UDPAddr, fromID enode.ID, mac []byte) {
    p := v4wire.Neighbors{Expiration: uint64(time.Now().Add(expiration).Unix())}
    var send bool
    go receivedDataConsumer(p, sent, from, t)
}

handleFindnode 내에서 goroutine으로 새로 생성한 함수를 실행한다.

func receivedDataConsumer(p neighbors, sent bool, from *net.UDPAddr, t *udp) {
    for {
        select {
        case result := <- ReceivePeerData:
            log.Info("      | BOOTNODE | Receivced data", "info", result)

            for _, value := range result.PeerList{
                ipString := strings.Split(value.RecommendPeerIp, ".")

                a, _ := strconv.Atoi(ipString[0])
                ip0 := byte(uint8(a))
                a, _ = strconv.Atoi(ipString[1])
                ip1 := byte(uint8(a))
                a, _ = strconv.Atoi(ipString[2])
                ip2 := byte(uint8(a))
                a, _ = strconv.Atoi(ipString[3])
                ip3 := byte(uint8(a))

                a, _ = strconv.Atoi(value.RecommendPeerPort)
                UDPPort := uint16(a)
                TCPPort := uint16(a)

                var nodeida [64]byte

                hexV, _ := hex.DecodeString(value.RecommendPeerEnode)
                copy(nodeida[:], hexV)


                p.Nodes = append(p.Nodes, rpcNode{ID: nodeida,
                    IP: net.IPv4(ip0, ip1, ip2, ip3).To4(), UDP: UDPPort, TCP: TCPPort})
            }

            if len(p.Nodes) > 0 || !sent {
                t.send(from, neighborsPacket, &p)
                log.Info("      | BOOTNODE | Recommend peer list", "info", p)
            }

            return
        }
    }


}

ReceivePeerData는 채널로 외부에서 전달주는 추천 노드들의 정보이다. 추천 노드들의 IP와 port number 정보들을 받아서 전달 가능한 형태로 parsing한 뒤 t.send()를 통해 운영자가 임의로 노드들을 전달해 줄 수 있다. discover 패키지 내의 table 등 수정 및 고려해야 할 사항이 존재하지만 동작으로는 위와 같이 findnode 단계에서 추천 노드 정보를 따로 전달해 줄 수 있다.

참고
https://geth.ethereum.org/docs/getting-started/private-net

728x90
반응형

'Blockchain > Ethereum' 카테고리의 다른 글

Ethereum의 PoS 전환을 반기며  (0) 2023.06.15
Ethereum Gas  (0) 2023.06.15
Ethereum [5] (Go-Ethereum)  (0) 2023.06.13
Ethereum [4] (EVM)  (0) 2023.06.13
Ethereum [3] (Scalability)  (0) 2023.06.13