Skip to content

Commit

Permalink
Merge pull request #14 from ZigmundVonZaun/fady
Browse files Browse the repository at this point in the history
Fady: Documentation and minor linting.
  • Loading branch information
49e94b8f256530dc0d41f740dfe8a4c1 authored Jun 16, 2018
2 parents 0a1d7fe + 6d3fa36 commit 67a4422
Show file tree
Hide file tree
Showing 10 changed files with 180 additions and 38 deletions.
4 changes: 4 additions & 0 deletions .env.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
K_BUCKET_SIZE = 20
ID_BITS = 128
ITERATION_SLEEP = 1
ALPHA = 3
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ venv/

# IntelliJ

.idea/
.idea/

# Environmental Variables

.env
39 changes: 36 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,48 @@
### Little Bird Kademlia P2P DHT Network

A Python3 Kademlia overlay network implementation.

[![Build Status](https://travis-ci.org/ZigmundVonZaun/little-bird.svg?branch=master)](https://travis-ci.org/ZigmundVonZaun/little-bird)
[![MIT license](https://img.shields.io/badge/License-MIT-blue.svg)](https://lbesson.mit-license.org/)

<img src="https://static.vecteezy.com/system/resources/previews/000/036/946/non_2x/oriole-bird-vector.jpg" width="320">
<img src="docs/images/bird.jpg" width="320">

A Python3 Kademlia overlay network implementation.
### Demo.

### Installation.

### Demo.
From PyPi (Stable version)

```bash

$ mkdir tmp && cd tmp

$ pip3 install little-bird

```

From GitHub (Bleeding Edge/Development version)

```bash

$ git clone https://github.com/ZigmundVonZaun/little-bird.git

$ python3 -m virtualenv venv

$ pip3 install -r requirements.txt

```

### Setup.

Copy the ```.env.dist``` to ```.env``` found on the directory root
and edit it as you see fit.

```bash

$ cp .env.dist .env && nano .env

```

### Introduction.

Expand Down
34 changes: 28 additions & 6 deletions dht/bucket_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,36 @@
from dht.utils import Utils


class BucketSet(object):
def __init__(self, bucket_size, buckets, id):
self.id = id
self.bucket_size = bucket_size
self.buckets = [list() for _ in range(buckets)]
class BucketSet:
def __init__(self, k, id_bits, id):
"""
Initialises a set of k-buckets. Kademlia stores a list up to k in size for each of the node ids bits.
A 128 bit id has 128 such lists. Each list contains triple information (ip_address, udp_port, node-id)
:param k: The bucket size is a constant ,k, which caps the size of the lists within a k bucket
, normally k=20.
:param id_bits: Bits in the nodeid.
:param id: The peer id
"""
self.id = id # the unique nodes id
self.bucket_size = k # lists are >= k
self.buckets = [list() for _ in range(id_bits)] # create lists as large as the bits of the nodeid
self.lock = threading.Lock()

def to_list(self):
"""
Converts k-buckets to list object. Returns this list object
:return: A list representation of the buckets
"""
l = []
for bucket in self.buckets: l += bucket
for bucket in self.buckets:
l += bucket
return l

def to_dict(self):
"""
Converts k-buckets to list object. Returns this list object
:return: Returns a dict representation
"""
l = []
for bucket in self.buckets:
for peer in bucket:
Expand All @@ -26,6 +43,11 @@ def to_dict(self):
return l

def insert(self, peer):
"""
Adds a peer into a k-bucket
:param peer: A participant within the network.
:return: None
"""
if peer.id != self.id:
bucket_number = Utils.largest_differing_bit(self.id, peer.id)
peer_triple = peer.astriple()
Expand Down
88 changes: 65 additions & 23 deletions dht/dht.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import random
import threading
import time
Expand All @@ -9,30 +10,51 @@
from dht.shortlist import Shortlist
from dht.utils import Utils

k = 20
alpha = 3
id_bits = 128
iteration_sleep = 1
from dotenv import load_dotenv, find_dotenv

load_dotenv(find_dotenv())

K_BUCKET_SIZE = 20 if os.getenv("K_BUCKET_SIZE") is None else int(os.getenv("K_BUCKET_SIZE"))
ALPHA = 3 if os.getenv("ALPHA") is None else int(os.getenv("ALPHA"))
ID_BITS = 128 if os.getenv("ID_BITS") is None else int(os.getenv("ID_BITS"))
ITERATION_SLEEP = 1 if os.getenv("ITERATION_SLEEP") is None else int(os.getenv("ITERATION_SLEEP"))


class DHT:
def __init__(self,
host,
port,
id=None,
seeds=[],
storage={},
info={},
seeds=None,
storage=None,
info=None,
hash_function=Utils.hash_function,
requesthandler=DHTRequestHandler):
"""
Initialises a new distributed hash table
:param host: hostname of this here table
:param port: listening port of the current table
:param id: id of the current table
:param seeds: seeds present in the table
:param storage: shelf to be used by the table
:param info:
:param hash_function: hash function used to compute the ids
:param requesthandler: handles requests from other nodes in the network
"""
if info is None:
info = {}
if storage is None:
storage = {}
if seeds is None:
seeds = []
if not id:
id = Utils.random_id()
self.storage = storage
self.info = info
self.hash_function = hash_function
self.peer = Peer(host, port, id, info)
self.data = self.storage
self.buckets = BucketSet(k, id_bits, self.peer.id)
self.buckets = BucketSet(K_BUCKET_SIZE, ID_BITS, self.peer.id)
self.rpc_ids = {} # should probably have a lock for this
self.server = DHTServer(self.peer.address(), requesthandler)
self.server.dht = self
Expand All @@ -42,49 +64,59 @@ def __init__(self,
self.bootstrap(seeds)

def identity(self):
"""
Returns the nodeid on the network
:return: nodeid on the network
"""
return self.peer.id

def iterative_find_nodes(self, key, boot_peer=None):
shortlist = Shortlist(k, key)
shortlist.update(self.buckets.nearest_nodes(key, limit=alpha))
shortlist = Shortlist(K_BUCKET_SIZE, key)
shortlist.update(self.buckets.nearest_nodes(key, limit=ALPHA))
if boot_peer:
rpc_id = random.getrandbits(id_bits)
rpc_id = random.getrandbits(ID_BITS)
self.rpc_ids[rpc_id] = shortlist
boot_peer.find_node(key, rpc_id, socket=self.server.socket, peer_id=self.peer.id, peer_info=self.peer.info)
while (not shortlist.complete()) or boot_peer:
nearest_nodes = shortlist.get_next_iteration(alpha)
nearest_nodes = shortlist.get_next_iteration(ALPHA)
for peer in nearest_nodes:
shortlist.mark(peer)
rpc_id = random.getrandbits(id_bits)
rpc_id = random.getrandbits(ID_BITS)
self.rpc_ids[rpc_id] = shortlist
peer.find_node(key, rpc_id, socket=self.server.socket, peer_id=self.peer.id, peer_info=self.info)
time.sleep(iteration_sleep)
time.sleep(ITERATION_SLEEP)
boot_peer = None
return shortlist.results()

def iterative_find_value(self, key):
shortlist = Shortlist(k, key)
shortlist.update(self.buckets.nearest_nodes(key, limit=alpha))
shortlist = Shortlist(K_BUCKET_SIZE, key)
shortlist.update(self.buckets.nearest_nodes(key, limit=ALPHA))
while not shortlist.complete():
nearest_nodes = shortlist.get_next_iteration(alpha)
nearest_nodes = shortlist.get_next_iteration(ALPHA)
for peer in nearest_nodes:
shortlist.mark(peer)
rpc_id = random.getrandbits(id_bits)
rpc_id = random.getrandbits(ID_BITS)
self.rpc_ids[rpc_id] = shortlist
peer.find_value(key,
rpc_id,
socket=self.server.socket,
peer_id=self.peer.id,
peer_info=self.info)
time.sleep(iteration_sleep)
time.sleep(ITERATION_SLEEP)
return shortlist.completion_result()

# Return the list of connected peers
def peers(self):
return self.buckets.to_dict()

# Boostrap the network with a list of bootstrap nodes
def bootstrap(self, bootstrap_nodes=[]):
def bootstrap(self, bootstrap_nodes=None):
"""
Bootstrap the network with a list of bootstrap nodes
:param bootstrap_nodes: A list of nodes to bootstrap the network with
:return: None
"""
if bootstrap_nodes is None:
bootstrap_nodes = []
for bnode in bootstrap_nodes:
boot_peer = Peer(bnode[0], bnode[1], "", "")
self.iterative_find_nodes(self.peer.id, boot_peer=boot_peer)
Expand All @@ -93,17 +125,27 @@ def bootstrap(self, bootstrap_nodes=[]):
for bnode in self.buckets.to_list():
self.iterative_find_nodes(self.peer.id, boot_peer=Peer(bnode[0], bnode[1], bnode[2], bnode[3]))

# Get a value in a sync way, calling an handler
def get_sync(self, key, handler):
"""
Get a value in a sync way, calling an handler
:param key: Key we are searching for
:param handler: Handler to pass value to after getting the key
:return:
"""
try:
d = self[key]
except:
d = None

handler(d)

# Get a value in async way
def get(self, key, handler):
"""
Get a value in async way
:param key: Key we are searching for
:param handler:
:return: Handler to pass value to after getting the key
"""
# print ('dht.get',key)
t = threading.Thread(target=self.get_sync, args=(key, handler))
t.start()
Expand Down
5 changes: 5 additions & 0 deletions dht/dht_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,10 @@

class DHTServer(socketserver.ThreadingMixIn, socketserver.UDPServer):
def __init__(self, host_address, handler_cls):
"""
Initialises a UDP socket server needed in order to receive communications from other nodes in the network.
:param host_address: Host address.
:param handler_cls: Specify handler that fires after a message is received.
"""
socketserver.UDPServer.__init__(self, host_address, handler_cls)
self.send_lock = threading.Lock()
31 changes: 27 additions & 4 deletions dht/storage.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,46 @@
import shelve


def test():
pass


class Shelve:
"""
It works like Python 2s pickle
"""
def __init__(self, f):
"""
Initialises a shelf for object persistence against instances
:param f: file path pointing to external file shelf should use
"""
self.shelve = shelve.open(f)

def dump(self):
"""
Shows all saved keys within the file
:return: stdout output of all keys in the shelf
"""
for x in self.shelve:
print('key:', x, '\t\tvalue:', self.shelve[x])

def __getitem__(self, key):
"""
Gets a key within the shelf
:param key: Specifies the key of the object within the shelf
:return: Returns a value from the shelf
"""
return self.shelve[str(key)]

def __setitem__(self, key, value):
"""
Sets a key and its respective value into the shelf.
:param key: Key for later reindexing
:param value: Value to save
:return: None
"""
self.shelve[str(key)] = value

def __contains__(self, key):
"""
Checks whether a key exists in the shelf.
:param key: Specifies the key to check
:return: Key if it exists
"""
return str(key) in self.shelve
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
matplotlib
networkx
logbook
logbook
python-dotenv
4 changes: 4 additions & 0 deletions tests/dht_test.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import unittest
from pathlib import Path

from dht.dht import DHT


class DHTTest(unittest.TestCase):
def setUp(self):
pass

def test_dht(self):
"""
Tests key lookups with a single table
Expand Down
4 changes: 4 additions & 0 deletions tests/peer_test.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import unittest
from pathlib import Path

from dht.peer import Peer


class PeerTest(unittest.TestCase):
def setUp(self):
pass

def test_peer(self):
peer = Peer("localhost", 9789, "foo", "bar")
self.assertEqual(str(peer), "localhost:9789")

0 comments on commit 67a4422

Please sign in to comment.