레이블이 ethereum인 게시물을 표시합니다. 모든 게시물 표시
레이블이 ethereum인 게시물을 표시합니다. 모든 게시물 표시

2018-06-03

2018년 22번재 주 - P2P Network

이 포스팅은 그냥 지난 한 주간 읽었던 것들을 정리하는 포스트입니다. 그냥 예전에 봤던 글 중 나중에 필요한데 뭐였는지 기억 안 나는 글들이 있어서 쓰기 시작했습니다.
보통 하는 일과 관련된 글들이 올라오겠지만 딱히 정해둔 주제는 없고, 그때그때 관심 있었던 것을 읽었기 때문에 지난주에 쓰인 글일 수도 있고 몇 년 전에 쓰인 글일 수도 있습니다.

지난주에 이어 이번 주는 P2P를 주제로 발표했다. 이번에도 발표자료를 만들면서 참고한 자료를 공유하도록 하겠다.


Chord: a scalable peer-to-peer lookup protocol for internet applications

Tapestry: A Resilient Global-scale Overlay for Service Deployment

Pastry: Scalable, decentralized object location and routing for large-scale peer-to-peer systems

A Scalable Content-Addressable Network

P2P 네트워크 중에서도 네트워크 토폴로지가 특정한 규칙에 의해서 구성되는 네트워크를 structured 네트워크라고 말한다. Structured network는 unstructured network에 비해 특정 노드에 부하가 걸리거나 의존하는 것을 방지하면서 잘 분산된 네트워크를 구성할 수 있다. 위의 4 논문은 각기 Chord, Tapestry, Pastry, Content Addressable Network(a.k.a. CAN)라는 대표적인 structured 네트워크 오버레이를 제시하는 논문이다.

Kademlia: A Peer-to-Peer Information System Based on the XOR Metric

Kademlia는 위의 네 논문을 개선하는 네트워크 오버레이를 제시한다. BitTorrent, eDonkey, 이더리움 등 많은 P2P 시스템이 kademlia 방식을 이용한다. 실질적으로 P2P 네트워크에서 가장 많이 사용되는 방식이다.
Kademlia의 가장 큰 특징은 xor distance를 사용하는 것이다. Xor distance는 노드 아이디의 공통된 prefix가 길수록 가까운 노드로 판단하는 방식이다. 이 방식은 triangle inequality를 만족하며, 각 노드가 작은 라우팅 테이블만으로 전체 네트워크를 효율적으로 연결할 수 있다.
게다가 거리가 대칭적이기 때문에 TCP로 연결해야 하는 시스템에서도 사용하기 좋다.

Understanding churn in peer-to-peer networks

Churn이라는 것은 새 노드가 네트워크에 참여하거나 네트워크에서 노드가 빠져나가는 변동성을 말한다. 위 논문은 다양한 p2p 네트워크에서 네트워크 상태를 조사해서 각 네트워크가 어떤 churn을 가지는지 분석했다.
사실 churn 자체는 서비스의 종류마다 다 다를 것이다. 이 논문에서 주목할 점은 그 churn을 어떻게 분석했는지 그 방법론에 대한 것이라고 생각한다.

Low-Resource Eclipse Attacks on Ethereum’s Peer-to-Peer Network

이더리움에 가할 수 있는 eclipse attack에 관해 정리한 논문이다. 자세한 내용은 전에 쓴 적이 있으니 참고 바란다.

Eclipse Attacks on Bitcoin’s Peer-to-Peer Network

이 논문은 비트코인에서 할 수 있는 eclipse attack에 관해 정리한 논문이다. 이더리움은 2대의 머신으로 공격할 수 있었던 반면 비트코인에서는 약 400개의 머신이 필요하다. 아직 비트코인에서 이보다 적은 수로 공격할 수 있는 방법은 나오지 않았다.

BLAKE2: simpler, smaller, fast as MD5

Cryptographic hash function 중 하나인 BLAKE2를 소개하는 논문이다. SHA-3 finalist였던 blake의 개선판으로 blake보다 더 적은 메모리로 빠르게 돌아간다.

The BLAKE2 Cryptographic Hash and Message Authentication Code (MAC)

Codechain에서는 MD5SHA-1을 이용하는 일반적인 HMAC 대신에 BLAKE2를 HMAC으로 사용한다. 이와 관련하여 이론적 분석이 있을 것이라고 생각하고 보기 시작했는데, HMAC에 관한 내용은 별로 없고, BLAKE2의 알고리즘과 구현에 관한 내용이 대부분이었다.

O(1) Block Propagation

이건 내가 발표한 내용이 아니라 양준모 님의 발표에 있던 비트코인의 Graphene의 아이디어를 줬던 제안이다. 트랜잭션은 언제나 전파하는 비트코인의 특성상 대부분 트랜잭션이 이미 전파돼있을 거라고 가정하고, IBLT라는 자료구조를 이용해서 없는 트랜잭션을 빠르게 확인할 수 있다는 것이 메인 아이디어다.

Invertible Bloom Lookup Tables

이 논문이 위에서 말한 IBLT를 제시하는 논문이다.

2018-05-12

Secure Tree - state trie의 키가 256 bit인 이유

지난번 글에서 설명했듯이 ethereum의 상태는 modified Merkle Patricia Trie(a.k.a. MPT)에 저장된다. Ethererum에서 값은 nonce, balance 등 account의 상태고, 그 키는 account의 주소다. 이 Account의 주소는 160bit이기 때문에, MPT의 root에서부터 40 nibble의 경로를 타고 가면 account의 상태가 나와야 한다. 하지만 실제로 ethereum의 상태가 저장된 MPT에서 account의 주소를 키로 가지는 노드를 찾으면, leaf node가 아닌 branch nodeextension ndoe가 나온다.

이는 ethereum이 account를 MPT에 집어넣을 때, account의 주소를 바로 키로 사용하는 것이 아니라, 주소의 keccak-256 hash를 키로 사용하기 때문이다. 즉, 40 nibble의 account 주소를 따라가는 것이 아니라, 64 nibble의 hash를 따라가야 원하는 account를 찾을 수 있다. 이렇게 account의 주소를 바로 경로로 사용하는 것이 아니라, 주소의 hash를 경로로 사용하는 것을 ethereum은 secure tree라고 부른다.

Secure tree에 대해 자세히 설명하기 전에 ethereum이 사용하는 MPT에 대해서 더 알아야할 것이 있다. Ethereum에 있는 MPT는 state trie만이 아니다. Ethereum은 총 네 종류의 MPT가 있다.

첫 번째는 state trie다. 여기에는 ethereum의 account 정보가 저장된다. 여기서 account 정보는 account의 nonce, balance, storage root의 hash, code의 hash다. 만약 계정이 smart contract라면 storage root에는 smart contract의 state를 가지는 MPT의 root가 저장되고, code에는 evm bytecode의 hash 값이 저장된다.

두 번째는 storage trie다. etheruem의 smart contract는 내부 상태를 가진다. 따라서 smart contract인 account는 자신의 상태를 간직하는 storage trie를 하나씩 만들어서 가지고 있다.

나머지 두 개는 transactions trie와 receipts trie다. 이들은 각각 블록이 가지고 있는 트랜잭션들과 각 트랜잭션을 실행한 실행 결과를 배열로 했을 때, 배열의 인덱스를 키로 하는 MPT다. Transactions trie와 receipts trie는 merkle proof를 위해 사용된다.

이 네 개의 MPT 중에서 state trie와 storage trie만 secure tree를 사용하고, transactions trie와 receipts trie에는 secure tree를 사용하지 않는다. 즉, state trie와 storage trie는 keccak-256 hash 값인 256 bit 값을 키로 사용하고, transactions trie와 receipts trie의 키는 임의의 길이의 값이다.

Ethereum이 secure tree를 사용하는 이유는 DoS 공격을 막기 위해서다. State trie와 storage trie는 파일에 저장된다. 즉, 이 두 trie에 새 노드가 추가되거나, 수정하거나, 노드를 읽는 것은 disk IO가 있어야 하고, 가능하면 leaf node에 도착할 때까지 최대한 적은 노드를 지나서 가는 것이 좋다. MPT는 이를 위해서 중복되는 1-child branch nodeextension node로 압축할 수 있도록 했다.

하지만 자식이 하나인 branch nodeextension node로 압축된다. 따라서 공격자가 악의적으로 2개의 자식이 생기는 branch node를 만들 수 있다면, 적은 비용으로 공격할 수 있다. 그래서 secure tree는 keccak-256 hash 값을 키로 사용하여 공격자가 자신이 원하는 위치에 노드를 생성할 수 없도록 하였다.

Transactions trie와 receipts trie는 secure tree를 사용하지 않는 이유는 키를 hash 하는 것으로 보호할 수 있는 것이 없다. Transactions trie와 receipts trie에서 키는 트랜잭션과 receipt의 index기 때문에 공격자가 원하는 키를 삽입하는 것이 불가능하기 때문이다.

또한, Secure tree는 키가 256 bit로 고정된다는 장점도 있다. 덕분에 트리의 깊이가 64 이하라는 것이 보장되기 때문에 state trie나 storage trie의 임의의 값을 변경하는데 드는 최대 비용을 계산할 수 있다. 특히 state trie나 storage trie는 트랜잭션에 의해서 읽히거나 수정되는데, 이때 읽히거나 수정되는 값이 어떤 노드에 있는지 상관없이 같은 일을 하는 트랜잭션은 같은 비용을 낸다. 그렇기 때문에 최대 비용이 보장되는 것은 secure tree의 큰 장점이다.

2018-05-06

Modified Merkle Patricia Trie - ethereum이 상태를 저장하는 방법

Ethereum에서 네트워크 부분을 빼고 보면, Ethereum은 하나의 state machine이고, transaction은 ethereum의 state를 변경시키는 것이다. 이 state는 key-value pair로 표현된다. Key-value pair를 저장하는 방법은 여러 가지 있지만, ethereum에서는 state를 저장하기 위해 Modified Merkle Patricia Trie(a.k.a. MPT)라는 특수화된 방법을 사용하도록 스펙에서 정의하고 있다.

MPT는 기본적으로 Patricia trie와 Merkle tree를 합친 것이다. 여기에 추가로 ethereum의 특성에 맞게 몇 가지 최적화를 했다. 따라서 MPT를 쉽게 이해하기 위해서는 Patricia trie와 Merkle tree를 아는 것이 좋다.

Patricia Trie

Patricia trie는 Prefix tree, radix tree, trie 등 다양한 이름으로 불리는 자료구조다. Trie는 path에 key를 집어넣어 공통된 prefix를 가지는 노드들은 같은 path를 가진다. 공통된 prefix를 찾는데 가장 빠르고, 적은 메모리로 구현할 수 있으며, 구현도 간단하다. 그래서 router 등 낮은 사양의 기계에 들어가는 routing table의 구현체로 많이 사용된다.

https://commons.wikimedia.org/wiki/File:Trie_example.svg

Merkle Tree

Merkle tree는 hash들의 tree다. Leaf 노드에는 데이터를 보관한다. Leaf의 부모는 leaf의 hash를 가지고, 그 부모는 자식들의 hash의 합을 다시 hash 한 값을 가진다. Merkle tree는 leaf 노드를 제외한 노드들은 전부 hash를 가지고 있기 때문에 hash tree라고도 불린다.

https://commons.wikimedia.org/wiki/File:Hash_Tree.svg

Merkle tree를 사용하면 서로 다른 두 노드가 같은 데이터를 가졌는지 효율적으로 비교할 수 있다. 예를 들어 위와 같은 L1, L2, L3, L4가 있을 때, 같은 Top Hash를 가졌는지만 비교하면 된다. 만약 Top Hash가 다르고, 어떤 데이터가 다른지 알고 싶으면 Hash0Hash1을 비교하고, 둘 중 다른 브랜치의 hash를 비교해나가면 어떤 데이터가 다른지 알 수 있다.

Merkle Patricia Trie

MPT는 Merkle tree처럼 각 노드가 hash를 가진다. 노드의 hash는 노드 내용의 sha3 hash로 결정된다. 이 hash는 노드를 지칭하는 key로도 사용된다. go-ethereumleveldb를, parityrocksdb라는 key-value storage에 state를 저장한다. 이때 스토리지에 저장되는 key와 value는 ethereum state의 key-value가 아니다. 스토리지에 저장되는 value는 MPT 노드의 내용이고, key는 이 노드의 hash다.

대신 ethereum state의 key는 MPT에서 path로 사용된다. MPT에서 key가 같은지 비교하는 단위는 nibble이기 때문에 하나의 노드에서 최대 16개의 branch를 가질 수 있다. 거기에 노드도 값을 가지기 때문에, 16개의 브랜치와 값을 합쳐 17개의 아이템을 가진 배열이 branch node가 된다.

아래로 자식이 없는 노드는 leaf node라고 불린다. Leaf node는 자신의 path와 value 두 개의 아이템으로 이루어진 배열이다. 예를 들어 "0xBEA"라는 키에 1000이 들어있고, "0xBEE"라는 키에 2000이 들어있는 경우를 생각해보자. 그렇다면 "0xBE"를 path로 가지는 branch node가 있고, 그 아래 "0xA"와 "0xE"를 path로 가지는 두 leaf node가 붙는다.

MPT에는 앞서 설명한 branch nodeleaf node 외에 extension node가 있다. Extension nodebranch node의 최적화다. Ethereum의 state에서는 하나의 branch node가 하나의 자식만 가지는 경우가 많다. 그래서 MPT는 하나의 자식만 가지는 branch node를 경로와 자식의 hash를 가지는 extension node로 압축한다.

여기서 문제가 하나 생긴다. Leaf nodeextension node는 둘 다 2개의 아이템을 가진 배열이다. 따라서 이 두 개의 node를 구분할 방법이 필요하다. MPT는 이를 위해서 경로에 prefix를 붙인다. 만약 노드가 leaf node고 경로가 짝수개의 nibble로 구성돼 있으면 0x20을 붙이고, 홀수개의 nibble로 구성돼 있으면 0x3을 붙인다. 반대로 extension node의 경우 짝수개의 nibble로 구성돼 있으면 0x00을, 홀수개의 nibble로 구성돼 있으면 0x1을 prefix로 붙인다. 홀수개의 nibble로 구성된 경로에는 한 개의 nibble을 prefix로 붙이고, 짝수개의 nibble로 구성된 경로에는 두 개의 nibble을 prefix로 붙이기 때문에 경로는 항상 byte로 표현 된다.

2018-04-29

Ethereum의 state trie pruning

Ethereum은 현재 상태를 prefix tree의 일종인 modified merkle patricia trie(a.k.a. MPT)로 저장한다. MPT는 state root의 hash를 계산하기 위해 state trie 전체를 볼 필요가 없이, 수정된 branch의 hash만 다시 계산하면 되기 때문에 빠르게 root hash를 찾을 수 있다.

MPT를 이용하면 새로 추가되는 노드의 수도 최소화할 수 있다. 예를 들어 위의 그림에서 block N과 block N + 1의 차이는 A의 오른쪽 자식의 값이 10에서 20으로 변경된 것뿐이다. 이 경우 10에서 20으로 변경된 노드의 부모 외의 다른 노드는 전부 기존의 노드를 재활용할 수 있다. 따라서 푸른색으로 그려진 3개의 노드만 새로 추가하면 된다.

그렇다면 더는 접근할 필요가 없는 노드들은 어떻게 되는가? 위의 예제에서 붉은색으로 표시된 3개의 노드는 block N + 1에서는 필요 없는 노드다. 그렇다면 이 3개의 노드는 3개의 푸른색 노드가 추가되고 나면 바로 지워도 될까? 그랬으면 문제가 쉬웠겠지만 아쉽게도 그렇지 않다. Ethereum은 block의 finality를 보장하지 않는다. 다른 말로 언제든지 block N + 1이 block N으로 retract 될 수 있다는 것이다. 게다가 Web3 API를 통해서 과거의 state에 접근하는 것도 가능하기 때문에 현재 상태에서 안 쓰이는 노드를 바로 지울 수는 없다.

그렇다고 영원히 남겨둘 수는 없다. 현재 ethereum에서 최신 state의 크기는 약 25GB 정도지만, 과거 state를 전부 저장하면 300GB를 넘어간다. 게다가 이 크기는 점점 커질 것이기 때문에 이를 전부 저장하는 것은 현실적이지 않다. Ethereum은 접근할 수 있는 과거 state를 127개로 제한하여 그보다 오래된 state에만 포함된 노드는 지워도 되도록 하였다. 하지만 지워도 된다는 것과 지울 수 있다는 것은 별개의 문제다. DB에 저장돼있는 노드 중 최근 127개의 노드에서 접근할 수 없는 노드를 찾아 지우는 것은 쉬운 문제가 아니다.

이 문제는 computer science에서 오랫동안 풀어 온 automatic memory management 문제와 비슷하다. 실제로 Vitalik Buterin이 쓴 state tree pruningreference counting을 언급하고 있다. 하지만 ethereum의 state trie pruning은 일반적인 memory management와 다른 점이 하나 있다.

일반적인 automatic memory management는 volatile 한 자원을 다룬다. 따라서 프로그램이 비정상 종료되는 상황을 고려하지 않아도 된다. 프로그램이 종료되면 관리해야 할 자원이 남아 있지 않기 때문이다. 하지만 state trie의 노드는 DB에 저장되는 persistence 메모리다. 프로그램의 비정상 종료로 인해 state trie가 비정상적인 상태가 되면 복구할 방법이 없다. Vitalik이 제시한 state tree pruning이 메인 넷에 들어가지 못한 것도 이런 이유에서다.

Reference counting이 아닌 다른 방법으로 state trie pruning을 구현할 수도 있다. 예를 들어 trace를 이용하는 방법도 tracing garbage collection도 automatic memory management에서 흔히 사용되는 기법이다. 하지만 trace에 필요한 추가적인 메모리나, stop-the-world에 의해 생기는 성능 문제 등이 먼저 해결돼야 한다.

이러한 문제들로 현재 go-ethereum에서는 매우 한정적으로 state trie pruning을 한다. State trie에 대해 cache를 사용하는데, 이 cache에만 저장된 노드에 대해서는 pruning을 하고 DB에 저장된 노드는 pruning을 하지 않는 방식이다. Caching 된 노드는 서버가 정상적으로 종료되거나, 생성된 지 128 block이 지났거나, 캐시 크기를 넘겼거나, 마지막으로 cache된 노드가 DB에 저장된 지 5분이 지나면 DB에 저장한다. 즉, 위의 조건을 만족하기 전에 cache에서 삭제된 노드는 DB에 저장하지 않는다.

하지만 생성된 지 5분이 지나지 않아서 삭제되는 노드는 그리 많지 않다. 따라서 대부분의 삭제됐어야 할 노드는 여전히 DB에 남아 있다. 이에 대해 ethereum에서는 state pruning을 구현하는 것을 계속 시도하고 있다. 하지만, state trie pruning이 실제로 구현되기 전에는 fast sync를 사용하여 다음과 같은 방법을 사용하기를 권장한다.

  1. 새 클라이언트를 띄운다.
  2. 기존 클라이언트에서 새 클라이언트로 fast sync를 받는다.
  3. 기존 클라이언트를 지운다.

위의 과정을 거치면 새 노드에서는 fast sync로 동기화된 상태까지의 garbage node 없이 유효한 노드만 관리할 수 있다. 언뜻 보기에는 주먹구구식 방식으로 보이지만, 위험 부담이 있는 garbage collection을 구현하는 것보다 안전하고 현실적인 해결책이다. Ethereum에 garbage collection이 구현되기 전까지는 계속 위와 같은 방식을 이용해야 할 것으로 보인다.

2018-04-12

이더리움 샤딩

현재 이더리움이 겪고 있는 가장 큰 문제는 scalability다. 이더리움이 인기를 끌면서 트랜잭션의 양도 늘어나고 참여하고 있는 노드의 수도 늘고 있는데 PoW를 쓰는 이더리움의 특성상 초당 트랜잭션 처리 수는 늘지 못한다. 그래서 지금 이더리움 코어 개발자들이 초점을 맞춰서 개발하고 있는 것이 ethereum sharding이다.

이더리움 샤딩을 간단히 설명하면 샤드별로 merkle patricia tree를 만들고 그 샤드의 root들로 만들어진 merkle partricia tree의 root만을 블록체인에 올리는 것이다. 이렇게 하면 모든 miner가 모든 트랜잭션을 실행할 필요 없이 각 샤드별로 miner를 분산시켜 실행할 수 있기 때문에 전체 실행 속도가 올라간다는 내용이다. 설명은 간단하지만 구현은 간단하지 않다.

이더리움은 replay attack을 막기 위해서 account nonce라는 것을 도입했다. 현재 state에서 account가 가지는 nonce보다 1 큰 nonce를 가지는 트랜잭션만 유효한 transaction이기 때문에, 같은 account에서 보내는 2개 이상의 트랜잭션은 동시에 처리될 수 없다. 즉, 한 account의 트랜잭션이 두 개 이상의 샤드에서 실행되면 안전하지 못하다는 것이고, account는 샤드에 종속돼야 한다. 또한, 샤드는 독립적으로 실행돼야 한다. 이 말은 한 샤드에서 실행된 트랜잭션이 다른 샤드에 영향을 주지 못 한다는 것이고, 다른 샤드에 존재하는 account 사이에서는 트랜잭션을 주고받을 수 없다는 것이다.

현재 제안된 샤드 간 통신은 receipt tree를 이용하는 것이다. 예를 들어 shard N의 account A에서 shard M의 account B로 송금하는 상황을 생각해보자. 이때 N은 account A의 계좌를 차감하는 transaction T1을 실행한다. T1M의 state를 변경할 수 없기 때문에 B에게 바로 입금할 수 없다. 따라서 T1MB에게 돈을 추가하라는 receipt만 남긴다. 그 후 M에서 B에게 입금하는 transaction T2를 실행하고 자신이 입금했다는 것을 다시 receipt에 적는다.

이렇게 하면 기존보다 비효율적이지만 샤드 간 송금은 처리할 수 있다. 하지만 아직 넘어야 할 산이 있다. 이더리움의 consensus 방식은 finality를 확률적으로만 보장한다. 위의 예제에서 T1을 실행됐으면 T2가 실행되고, T1이 실행되지 않았으면 T2도 실행돼서는 안 된다. 따라서 N에 문제가 생겨 reorganization이 발생하여 T1이 사라졌으면, M도 reorganization하여 T2를 지워야 한다. 이를 악용하면, 쉽게 DoS 공격이 가능해진다. 따라서 T2를 실행하기 전의 T1의 finality를 보장해야 하는데, 앞서 말했듯이 현재 이더리움에서는 방법이 없다. 완전히 PoS로 옮겨가거나 최소한 Friendly Finality Gadget이 들어가야 확실하게 finality를 보장할 수있게된다. 그래서 현재 진행중인 sharding phase 1에서는 샤드 간 통신은 구현이 없고 phase 4에서야 구현될 것이라고 한다.

샤드 간 송금은 finalty만 보장되면 위와 같은 방식으로 할 수 있지만, smart contract를 실행하는 것은 이것보다 더 복잡해진다. smart contract에서는 다른 smart contract를 호출할 수 있는데, smart contract도 각자 자신의 state를 가지고 있기 때문에, 모든 smart contract가 같은 shard에 있어야 한다. 이 문제를 해결하기 위해 smart contract를 lock 하여 실행시킬 shard로 가져오는 cross shard lock scheme이 제안은 됐지만, 아직 결정된 것은 아무것도 없다.

이더리움의 성능 문제가 현재 이더리움이 가지는 가장 큰 문제인 것은 확실하다. 이더리움의 트랜잭션 양은 늘어나고 있는데 아직도 이더리움의 초당 트랜잭션 처리 속도는 20tx/s를 넘지 못하고 있다. 하지만 샤딩만으로 얼마나 성능을 향상할 수 있을지는 의문이다. 샤드를 N개 만들었을 때, 최상의 결과는 N배 빨라지는 것이다. 이것도 정말 이상적인 것이고 실제로 이만한 성능 향상을 얻지는 못할 것이다. 따라서 성능을 많이 올리고 싶으면 샤드의 개수를 늘려야 한다.

하지만 위에서 말했듯이 샤드를 도입하면, 샤드 간 transaction은 지금의 이더리움보다 처리 속도가 느려진다. 현재 제안된 casper에서 finality를 보장하기 위해서는 최소 50 block을 기다린다. 샤드 간 통신에서는 두 번째 트랜잭션이 실행되기 전에 finality가 보장돼야 하니, 샤드 간 통신의 finality를 보장하기 위해서는 최소 10분을 더 기다려야 한다는 것이다. 과연 이 overhead를 감수하면서 샤드가 가지고 올 성능 향상이 크지는 않을 것 같다.

2018-03-26

이더리움과 Eclipse attack

p2p 네트워크는 많은 취약점을 가지고 있는데 대표적인 것이 Eclipse attack이다. Eclipse attack은 네트워크 전체를 공격하는 것이 아니라 목표로 하는 노드의 Routing table을 공격하여 목표로 하는 노드와 전체 네트워크 사이에 악의적인 노드를 집어넣는 공격이다. Routing table을 공격하는 방법이기 때문에 routing-table poisoning이라고도 불린다.

이더리움도 p2p 네트워크를 사용하여 메시지를 주고받기 때문에 eclipse attack이 가능하리라 생각은 했는데 지난 3월 1일 발표 된 페이퍼에 따르면 단 2개의 노드만으로 하나의 노드를 완전히 고립시키는 것이 가능하다고 한다. 이 페이퍼는 올해 1월 진행됐던 바운티 프로그램에서 나온 문제점들을 정리한 페이퍼로 크게 세 가지 공격방법으로 나눌 수 있다.

우선 첫 번째 문제는 이해하기 위해 이더리움이 p2p 네트워크를 어떻게 관리하는지 이해해야 한다. 네트워크 그래프 구성에 가장 중요한 것은 다른 노드를 어떻게 찾을 것인가 하는 것이다. 이를 흔히 node discovery라고 하는데 이더리움은 node discovery를 위해 DHT(Distributed Hash Table) 프로토콜 중 하나인 Kademlia의 일부를 수정해서 사용한다.
Kademlia가 다른 DHT와 다른 가장 큰 특징은 노드 간의 거리를 XOR distance로 측정한다는 것이다. XOR distance의 거리는 symmetric 하므로 노드 아이디만 알고 있으면, 노드 A가 생각하는 노드 B까지의 거리나, 노드 B가 생각하는 노드 A까지의 거리나, 노드 C가 생각하는 노드 A와 노드 B 사이의 거리가 같다. 따라서 각 노드는 자신이 알고 있는 노드 중에서 자신과 가까운 노드들과만 통신하면 적은 연결 수로도 큰 네트워크를 구성할 수 있다는 장점이 있다. Kademlia 페이퍼에는 대략 노드의 개수를 N이라고 할 때 각 노드는 O(log(N))개의 연결만 유지하면 된다고 말한다.
이더리움이 Eclipse attack에 취약했던 근본적인 원인이 여기에 있다. Kademlia 프로토콜에서는 거리를 노드 아이디를 기반으로 구하는데, 어떤 노드의 아이디가 무엇인지 네트워크 안에 있는 모든 노드가 같은 값으로 알고 있어야 한다. 그래야 Kademlia가 주장하는 효율성을 가져올 수 있다. 문제는 분산 환경에서 각자의 아이디를 동의하는 방법이 없기 때문에 연결을 요청하는 노드가 자신의 public key를 알려주면 그 public key를 노드 아이디를 Keccak hashing 한 값을 노드 아이디로 준다. 즉, 각 노드가 자신의 아이디를 정하여 알려주는 것이다. 따라서 하나의 머신에서 여러 개의 노드 아이디를 생성해낼 수 있고, 악의적인 사용자가 하나의 머신을 여러 개의 노드인 것처럼 꾸미는 Sybil attack이 가능해진다.
이를 막기 위해 IP 주소와 노드 아이디를 기억하여 하나의 주소에서 여러 개의 키를 사용 못 하게 하는 것이 필요하지만, 아직 구현된 솔루션은 존재하지 않는다.

두 번째 공격 방법은 서버의 시간을 조정하는 것이다. 이더리움은 replay-attack을 막기 위해서 메시지에 보낸 시간을 적는다. 그리고 받은 메시지의 시간이 현재 시각과 20초 이상 차이가 나면 replay-attack으로 받은 메시지로 가정하고 무시한다. 문제는 replay-attack인지 판단하기 위해 메시지에 적힌 시간을 사용할 뿐, 메시지를 보낸 실제 시간은 알 수 없다는 것이다. 예를 들어 두 노드 사이에 시간이 20초 이상 난다면, replay-attack이 아닌 정상적인 메시지도 절대 처리되지 않는다.
현대 서버 환경에서 이럴 일은 거의 없다. 보통 서버 시간을 수동으로 설정하지 않고 NTP(Network Time Protocol)를 이용해 주기적으로 동기화하기 때문이다. 따라서 모든 서버의 시간은 길어봐야 몇 초 이내의 오차를 가질 것이라고 가정해도 된다.
하지만 이는 정상적인 상황에서만 가능한 가정이다. 만약 희생자가 될 노드가 사용할 NTP 서버를 해킹하거나 NTP 메시지를 가로채 동기화하지 못 하는 상황을 만들면 어떻게 될까. 아니면 조금 더 적극적으로 NTP 메시지를 가로채 실제 시간과 20초 이상 앞으로 당기면 어떻게 될까.
이 경우 희생자 노드는 네트워크상의 모든 노드와 시간 차이가 나게 될 테니, 다른 노드들은 모두 이 노드에서 보내는 메시지를 무시할 것이다. 그렇게 되면 희생자 노드는 네트워크에서 잊히게 되고 결국 희생자와 통신하는 것은 NTP 서버를 해킹했던 공격자 노드밖에 남지 않게 된다.
이는 메시지에 시간을 적는 방식으로 replay-attack을 막으려고 했던 이더리움 프로토콜의 문제다. 이런 공격을 막기 위해서는 세션별 혹은 메시지 별로 nonce를 사용하여 sign 하는 방법을 이용해야 한다. 하지만 이에 관한 솔루션도 아직 이더리움에는 구현된 것은 없는 것으로 보인다. 따라서 이더리움 노드를 돌리고 있는 서버는 시간이 잘 동기화되고 있는지 주기적으로 확인하는 방법밖에 없다.

마지막 공격 방법은 일종의 타이밍 어택이다. 이더리움은 네트워크에 너무 많은 리소스를 사용하지 않기 위해 통신할 최대 연결 수를 정해놓고 있다. 현재 최대 연결 수는 25개로 25개 이상의 연결이 맺어지면 더는 새 연결을 요청하거나 받아주지 않는다.
반대로 말해서 어떤 노드의 연결을 25개 점유할 수 있으면 이 노드는 더 이상 새 연결을 요청하지도 받아주지도 않을 것이다. 그렇다면 이것은 어떻게 가능할까?
간단하다. 연결이 0개인 순간을 노리면 된다. 어떤 서버라도 서버가 처음 시작했을 때는 연결이 0개인 상태가 된다. 이 타이밍을 노려서 25개의 연결을 먼저 맺으면 된다. 그렇다면 서버가 재시작되는 순간은 어떻게 찾을까.
하드웨어 문제든 OS 업데이트든 이더리움 클라이언트를 업데이트하든 서버가 재시작될 이유는 많이 있다. 하지만 조금 더 공격적으로 DoS 공격을 하여 이더리움 클라이언트를 죽이는 방법도 있다. 보통 이더리움 마이닝 노드는 서버가 종료되면 바로 다시 시작하게 돼 있기 때문에 DoS 공격으로 서버를 죽이고 나면 얼마 지나지 않아 서버가 재시작할 것이고 이때 25개 이상의 연결을 빠르게 요청하면 된다.
이를 막는 방법은 앞에서 설명한 두 방법보다 쉽다. 앞의 두 방법은 프로토콜 자체를 바꿔야 하지만 이 공격은 구현체를 수정하는 것만으로 쉽게 막을 수 있다. 전체 연결 개수 제한과 별개로 외부에서 들어오는 연결의 개수를 제한하는 것이다. 즉, 최소 몇 개의 연결은 내가 요청하는 노드와 연결이 되도록 하면 누군가 다른 사람에 의해 내 연결이 독점 당할 위험은 사라진다. 이 솔루션은 2월에 릴리즈 된 go-ethereum 1.8 버전부터 적용된 것으로 보인다.