forked from LBRYCommunity/lbry-sdk
463 lines
21 KiB
Python
463 lines
21 KiB
Python
|
#!/usr/bin/env python
|
||
|
#
|
||
|
# This library is free software, distributed under the terms of
|
||
|
# the GNU Lesser General Public License Version 3, or any later version.
|
||
|
# See the COPYING file included in this archive
|
||
|
|
||
|
import hashlib
|
||
|
import unittest
|
||
|
import struct
|
||
|
|
||
|
import lbrynet.dht.node
|
||
|
import lbrynet.dht.constants
|
||
|
import lbrynet.dht.datastore
|
||
|
|
||
|
class NodeIDTest(unittest.TestCase):
|
||
|
""" Test case for the Node class's ID """
|
||
|
def setUp(self):
|
||
|
self.node = lbrynet.dht.node.Node()
|
||
|
|
||
|
def testAutoCreatedID(self):
|
||
|
""" Tests if a new node has a valid node ID """
|
||
|
self.failUnlessEqual(type(self.node.id), str, 'Node does not have a valid ID')
|
||
|
self.failUnlessEqual(len(self.node.id), 20, 'Node ID length is incorrect! Expected 160 bits, got %d bits.' % (len(self.node.id)*8))
|
||
|
|
||
|
def testUniqueness(self):
|
||
|
""" Tests the uniqueness of the values created by the NodeID generator
|
||
|
"""
|
||
|
generatedIDs = []
|
||
|
for i in range(100):
|
||
|
newID = self.node._generateID()
|
||
|
# ugly uniqueness test
|
||
|
self.failIf(newID in generatedIDs, 'Generated ID #%d not unique!' % (i+1))
|
||
|
generatedIDs.append(newID)
|
||
|
|
||
|
def testKeyLength(self):
|
||
|
""" Tests the key Node ID key length """
|
||
|
for i in range(20):
|
||
|
id = self.node._generateID()
|
||
|
# Key length: 20 bytes == 160 bits
|
||
|
self.failUnlessEqual(len(id), 20, 'Length of generated ID is incorrect! Expected 160 bits, got %d bits.' % (len(id)*8))
|
||
|
|
||
|
|
||
|
class NodeDataTest(unittest.TestCase):
|
||
|
""" Test case for the Node class's data-related functions """
|
||
|
def setUp(self):
|
||
|
import lbrynet.dht.contact
|
||
|
h = hashlib.sha1()
|
||
|
h.update('test')
|
||
|
self.node = lbrynet.dht.node.Node()
|
||
|
self.contact = lbrynet.dht.contact.Contact(h.digest(), '127.0.0.1', 12345, self.node._protocol)
|
||
|
self.token = self.node.make_token(self.contact.compact_ip())
|
||
|
self.cases = []
|
||
|
for i in xrange(5):
|
||
|
h.update(str(i))
|
||
|
self.cases.append((h.digest(), 5000+2*i))
|
||
|
self.cases.append((h.digest(), 5001+2*i))
|
||
|
#(('a', 'hello there\nthis is a test'),
|
||
|
# ('b', unicode('jasdklfjklsdj;f2352352ljklzsdlkjkasf\ndsjklafsd')),
|
||
|
# ('e', 123),
|
||
|
# ('f', [('this', 'is', 1), {'complex': 'data entry'}]),
|
||
|
# ('aMuchLongerKeyThanAnyOfThePreviousOnes', 'some data'))
|
||
|
|
||
|
def testStore(self):
|
||
|
|
||
|
def check_val_in_result(r, peer_info):
|
||
|
self.failUnless
|
||
|
|
||
|
""" Tests if the node can store (and privately retrieve) some data """
|
||
|
for key, value in self.cases:
|
||
|
self.node.store(key, {'port': value, 'bbid': self.contact.id, 'token': self.token}, self.contact.id, _rpcNodeContact=self.contact)
|
||
|
for key, value in self.cases:
|
||
|
expected_result = self.contact.compact_ip() + str(struct.pack('>H', value)) + self.contact.id
|
||
|
self.failUnless(self.node._dataStore.hasPeersForBlob(key), 'Stored key not found in node\'s DataStore: "%s"' % key)
|
||
|
self.failUnless(expected_result in self.node._dataStore.getPeersForBlob(key), 'Stored val not found in node\'s DataStore: key:"%s" port:"%s" %s' % (key, value, self.node._dataStore.getPeersForBlob(key)))
|
||
|
|
||
|
class NodeContactTest(unittest.TestCase):
|
||
|
""" Test case for the Node class's contact management-related functions """
|
||
|
def setUp(self):
|
||
|
self.node = lbrynet.dht.node.Node()
|
||
|
|
||
|
def testAddContact(self):
|
||
|
""" Tests if a contact can be added and retrieved correctly """
|
||
|
import lbrynet.dht.contact
|
||
|
# Create the contact
|
||
|
h = hashlib.sha1()
|
||
|
h.update('node1')
|
||
|
contactID = h.digest()
|
||
|
contact = lbrynet.dht.contact.Contact(contactID, '127.0.0.1', 91824, self.node._protocol)
|
||
|
# Now add it...
|
||
|
self.node.addContact(contact)
|
||
|
# ...and request the closest nodes to it using FIND_NODE
|
||
|
closestNodes = self.node._routingTable.findCloseNodes(contactID, lbrynet.dht.constants.k)
|
||
|
self.failUnlessEqual(len(closestNodes), 1, 'Wrong amount of contacts returned; expected 1, got %d' % len(closestNodes))
|
||
|
self.failUnless(contact in closestNodes, 'Added contact not found by issueing _findCloseNodes()')
|
||
|
|
||
|
def testAddSelfAsContact(self):
|
||
|
""" Tests the node's behaviour when attempting to add itself as a contact """
|
||
|
import lbrynet.dht.contact
|
||
|
# Create a contact with the same ID as the local node's ID
|
||
|
contact = lbrynet.dht.contact.Contact(self.node.id, '127.0.0.1', 91824, None)
|
||
|
# Now try to add it
|
||
|
self.node.addContact(contact)
|
||
|
# ...and request the closest nodes to it using FIND_NODE
|
||
|
closestNodes = self.node._routingTable.findCloseNodes(self.node.id, lbrynet.dht.constants.k)
|
||
|
self.failIf(contact in closestNodes, 'Node added itself as a contact')
|
||
|
|
||
|
|
||
|
#class NodeLookupTest(unittest.TestCase):
|
||
|
# """ Test case for the Node class's iterative node lookup algorithm """
|
||
|
# def setUp(self):
|
||
|
# import entangled.kademlia.contact
|
||
|
# self.node = entangled.kademlia.node.Node()
|
||
|
# self.remoteNodes = []
|
||
|
# for i in range(10):
|
||
|
# remoteNode = entangled.kademlia.node.Node()
|
||
|
# remoteContact = entangled.kademlia.contact.Contact(remoteNode.id, '127.0.0.1', 91827+i, self.node._protocol)
|
||
|
# self.remoteNodes.append(remoteNode)
|
||
|
# self.node.addContact(remoteContact)
|
||
|
|
||
|
|
||
|
# def testIterativeFindNode(self):
|
||
|
# """ Ugly brute-force test to see if the iterative node lookup algorithm runs without failing """
|
||
|
# import entangled.kademlia.protocol
|
||
|
# entangled.kademlia.protocol.reactor.listenUDP(91826, self.node._protocol)
|
||
|
# for i in range(10):
|
||
|
# entangled.kademlia.protocol.reactor.listenUDP(91827+i, self.remoteNodes[i]._protocol)
|
||
|
# df = self.node.iterativeFindNode(self.node.id)
|
||
|
# df.addBoth(lambda _: entangled.kademlia.protocol.reactor.stop())
|
||
|
# entangled.kademlia.protocol.reactor.run()
|
||
|
|
||
|
|
||
|
""" Some scaffolding for the NodeLookupTest class. Allows isolated node testing by simulating remote node responses"""
|
||
|
from twisted.internet import protocol, defer, selectreactor
|
||
|
from lbrynet.dht.msgtypes import ResponseMessage
|
||
|
class FakeRPCProtocol(protocol.DatagramProtocol):
|
||
|
def __init__(self):
|
||
|
self.reactor = selectreactor.SelectReactor()
|
||
|
self.testResponse = None
|
||
|
self.network = None
|
||
|
|
||
|
|
||
|
def createNetwork(self, contactNetwork):
|
||
|
""" set up a list of contacts together with their closest contacts
|
||
|
@param contactNetwork: a sequence of tuples, each containing a contact together with its closest
|
||
|
contacts: C{(<contact>, <closest contact 1, ...,closest contact n>)}
|
||
|
"""
|
||
|
self.network = contactNetwork
|
||
|
|
||
|
""" Fake RPC protocol; allows entangled.kademlia.contact.Contact objects to "send" RPCs """
|
||
|
def sendRPC(self, contact, method, args, rawResponse=False):
|
||
|
#print method + " " + str(args)
|
||
|
|
||
|
if method == "findNode":
|
||
|
# get the specific contacts closest contacts
|
||
|
closestContacts = []
|
||
|
#print "contact" + contact.id
|
||
|
for contactTuple in self.network:
|
||
|
#print contactTuple[0].id
|
||
|
if contact == contactTuple[0]:
|
||
|
# get the list of closest contacts for this contact
|
||
|
closestContactsList = contactTuple[1]
|
||
|
#print "contact" + contact.id
|
||
|
|
||
|
# Pack the closest contacts into a ResponseMessage
|
||
|
for closeContact in closestContactsList:
|
||
|
#print closeContact.id
|
||
|
closestContacts.append((closeContact.id, closeContact.address, closeContact.port))
|
||
|
message = ResponseMessage("rpcId", contact.id, closestContacts)
|
||
|
|
||
|
df = defer.Deferred()
|
||
|
df.callback((message,(contact.address, contact.port)))
|
||
|
return df
|
||
|
elif method == "findValue":
|
||
|
for contactTuple in self.network:
|
||
|
if contact == contactTuple[0]:
|
||
|
# Get the data stored by this remote contact
|
||
|
dataDict = contactTuple[2]
|
||
|
dataKey = dataDict.keys()[0]
|
||
|
data = dataDict.get(dataKey)
|
||
|
|
||
|
# Check if this contact has the requested value
|
||
|
if dataKey == args[0]:
|
||
|
# Return the data value
|
||
|
response = dataDict
|
||
|
|
||
|
print "data found at contact: " + contact.id
|
||
|
else:
|
||
|
# Return the closest contact to the requested data key
|
||
|
print "data not found at contact: " + contact.id
|
||
|
closeContacts = contactTuple[1]
|
||
|
closestContacts = []
|
||
|
for closeContact in closeContacts:
|
||
|
closestContacts.append((closeContact.id, closeContact.address, closeContact.port))
|
||
|
response = closestContacts
|
||
|
|
||
|
# Create the response message
|
||
|
message = ResponseMessage("rpcId", contact.id, response)
|
||
|
df = defer.Deferred()
|
||
|
df.callback((message,(contact.address, contact.port)))
|
||
|
return df
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
print "findValue"
|
||
|
|
||
|
def _send(self, data, rpcID, address):
|
||
|
""" fake sending data """
|
||
|
|
||
|
|
||
|
|
||
|
class NodeLookupTest(unittest.TestCase):
|
||
|
""" Test case for the Node class's iterativeFind node lookup algorithm """
|
||
|
|
||
|
def setUp(self):
|
||
|
|
||
|
# create a fake protocol to imitate communication with other nodes
|
||
|
self._protocol = FakeRPCProtocol()
|
||
|
|
||
|
# Note: The reactor is never started for this test. All deferred calls run sequentially,
|
||
|
# since there is no asynchronous network communication
|
||
|
|
||
|
# create the node to be tested in isolation
|
||
|
self.node = lbrynet.dht.node.Node(None, 4000, None, None, self._protocol)
|
||
|
|
||
|
self.updPort = 81173
|
||
|
|
||
|
# create a dummy reactor
|
||
|
#self._protocol.reactor.listenUDP(self.updPort, self._protocol)
|
||
|
|
||
|
self.contactsAmount = 80
|
||
|
# set the node ID manually for testing
|
||
|
self.node.id = '12345678901234567800'
|
||
|
|
||
|
# Reinitialise the routing table
|
||
|
self.node._routingTable = lbrynet.dht.routingtable.OptimizedTreeRoutingTable(self.node.id)
|
||
|
|
||
|
# create 160 bit node ID's for test purposes
|
||
|
self.testNodeIDs = []
|
||
|
#idNum = long(self.node.id.encode('hex'), 16)
|
||
|
idNum = int(self.node.id)
|
||
|
for i in range(self.contactsAmount):
|
||
|
# create the testNodeIDs in ascending order, away from the actual node ID, with regards to the distance metric
|
||
|
self.testNodeIDs.append(idNum + i + 1)
|
||
|
|
||
|
|
||
|
# generate contacts
|
||
|
self.contacts = []
|
||
|
for i in range(self.contactsAmount):
|
||
|
contact = lbrynet.dht.contact.Contact(str(self.testNodeIDs[i]), "127.0.0.1", self.updPort + i + 1, self._protocol)
|
||
|
self.contacts.append(contact)
|
||
|
|
||
|
# create the network of contacts in format: (contact, closest contacts)
|
||
|
contactNetwork = ((self.contacts[0], self.contacts[8:15]),
|
||
|
(self.contacts[1], self.contacts[16:23]),
|
||
|
(self.contacts[2], self.contacts[24:31]),
|
||
|
(self.contacts[3], self.contacts[32:39]),
|
||
|
(self.contacts[4], self.contacts[40:47]),
|
||
|
(self.contacts[5], self.contacts[48:55]),
|
||
|
(self.contacts[6], self.contacts[56:63]),
|
||
|
(self.contacts[7], self.contacts[64:71]),
|
||
|
(self.contacts[8], self.contacts[72:79]),
|
||
|
(self.contacts[40], self.contacts[41:48]),
|
||
|
(self.contacts[41], self.contacts[41:48]),
|
||
|
(self.contacts[42], self.contacts[41:48]),
|
||
|
(self.contacts[43], self.contacts[41:48]),
|
||
|
(self.contacts[44], self.contacts[41:48]),
|
||
|
(self.contacts[45], self.contacts[41:48]),
|
||
|
(self.contacts[46], self.contacts[41:48]),
|
||
|
(self.contacts[47], self.contacts[41:48]),
|
||
|
(self.contacts[48], self.contacts[41:48]),
|
||
|
(self.contacts[50], self.contacts[0:7]),
|
||
|
(self.contacts[51], self.contacts[8:15]),
|
||
|
(self.contacts[52], self.contacts[16:23]))
|
||
|
|
||
|
contacts_with_datastores = []
|
||
|
|
||
|
for contact_tuple in contactNetwork:
|
||
|
contacts_with_datastores.append((contact_tuple[0], contact_tuple[1], lbrynet.dht.datastore.DictDataStore()))
|
||
|
|
||
|
self._protocol.createNetwork(contacts_with_datastores)
|
||
|
|
||
|
def testNodeBootStrap(self):
|
||
|
""" Test bootstrap with the closest possible contacts """
|
||
|
|
||
|
df = self.node._iterativeFind(self.node.id, self.contacts[0:8])
|
||
|
# Set the expected result
|
||
|
expectedResult = []
|
||
|
|
||
|
for item in self.contacts[0:6]:
|
||
|
expectedResult.append(item.id)
|
||
|
#print item.id
|
||
|
|
||
|
# Get the result from the deferred
|
||
|
activeContacts = df.result
|
||
|
|
||
|
|
||
|
# Check the length of the active contacts
|
||
|
self.failUnlessEqual(activeContacts.__len__(), expectedResult.__len__(), \
|
||
|
"More active contacts should exist, there should be %d contacts" %expectedResult.__len__())
|
||
|
|
||
|
|
||
|
# Check that the received active contacts are the same as the input contacts
|
||
|
self.failUnlessEqual(activeContacts, expectedResult, \
|
||
|
"Active should only contain the closest possible contacts which were used as input for the boostrap")
|
||
|
|
||
|
# def testFindingCloserNodes(self):
|
||
|
# """ Test discovery of closer contacts"""
|
||
|
#
|
||
|
# # Use input contacts that have knowledge of closer contacts,
|
||
|
# df = self.node._iterativeFind(self.node.id, self.contacts[50:53])
|
||
|
# #set the expected result
|
||
|
# expectedResult = []
|
||
|
# #print "############ Expected Active contacts #################"
|
||
|
# for item in self.contacts[0:9]:
|
||
|
# expectedResult.append(item.id)
|
||
|
# #print item.id
|
||
|
# #print "#######################################################"
|
||
|
#
|
||
|
# # Get the result from the deferred
|
||
|
# activeContacts = df.result
|
||
|
#
|
||
|
# #print "!!!!!!!!!!! Receieved Active contacts !!!!!!!!!!!!!!!"
|
||
|
# #for item in activeContacts:
|
||
|
# # print item.id
|
||
|
# #print "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
||
|
#
|
||
|
# # Check the length of the active contacts
|
||
|
# self.failUnlessEqual(activeContacts.__len__(), expectedResult.__len__(), \
|
||
|
# "Length of received active contacts not as expected, should be %d" %expectedResult.__len__())
|
||
|
#
|
||
|
#
|
||
|
# # Check that the received active contacts are now closer to this node
|
||
|
# self.failUnlessEqual(activeContacts, expectedResult, \
|
||
|
# "Active contacts should now only contain the closest possible contacts")
|
||
|
|
||
|
|
||
|
|
||
|
# def testIterativeStore(self):
|
||
|
# """ test storing values """
|
||
|
#
|
||
|
# # create the network of contacts in format: (contact, closest contacts)
|
||
|
# contactNetwork = ((self.contacts[0], self.contacts[0:8]),
|
||
|
# (self.contacts[1], self.contacts[0:8]),
|
||
|
# (self.contacts[2], self.contacts[0:8]),
|
||
|
# (self.contacts[3], self.contacts[0:8]),
|
||
|
# (self.contacts[4], self.contacts[0:8]),
|
||
|
# (self.contacts[5], self.contacts[0:8]),
|
||
|
# (self.contacts[6], self.contacts[0:8]),
|
||
|
# (self.contacts[7], self.contacts[0:8]),
|
||
|
# (self.contacts[8], self.contacts[0:8]),
|
||
|
# (self.contacts[40], self.contacts[41:48]),
|
||
|
# (self.contacts[41], self.contacts[41:48]),
|
||
|
# (self.contacts[42], self.contacts[41:48]),
|
||
|
# (self.contacts[43], self.contacts[41:48]),
|
||
|
# (self.contacts[44], self.contacts[41:48]),
|
||
|
# (self.contacts[45], self.contacts[41:48]),
|
||
|
# (self.contacts[46], self.contacts[41:48]),
|
||
|
# (self.contacts[47], self.contacts[41:48]),
|
||
|
# (self.contacts[48], self.contacts[41:48]))
|
||
|
# contacts_with_datastores = []
|
||
|
#
|
||
|
# for contact_tuple in contactNetwork:
|
||
|
# contacts_with_datastores.append((contact_tuple[0], contact_tuple[1], lbrynet.dht.datastore.DictDataStore()))
|
||
|
#
|
||
|
# self._protocol.createNetwork(contacts_with_datastores)
|
||
|
#
|
||
|
#
|
||
|
# #self._protocol.createNetwork(contactNetwork)
|
||
|
#
|
||
|
#
|
||
|
# # Test storing a value that has an hash id close to the known contacts
|
||
|
# # The value should only be stored at those nodes
|
||
|
# value = 'value'
|
||
|
# valueID = self.contacts[40].id
|
||
|
#
|
||
|
# # Manually populate the routing table with contacts that have ID's close to the valueID
|
||
|
# for contact in self.contacts[40:48]:
|
||
|
# self.node.addContact(contact)
|
||
|
#
|
||
|
# # Manually populate the routing table with contacts that have ID's far away from the valueID
|
||
|
# for contact in self.contacts[0:8]:
|
||
|
# self.node.addContact(contact)
|
||
|
#
|
||
|
# # Store the value
|
||
|
# df = self.node.announceHaveBlob(valueID, value)
|
||
|
#
|
||
|
# storageNodes = df.result
|
||
|
#
|
||
|
# storageNodeIDs = []
|
||
|
# for item in storageNodes:
|
||
|
# storageNodeIDs.append(item.id)
|
||
|
# storageNodeIDs.sort()
|
||
|
# #print storageNodeIDs
|
||
|
#
|
||
|
# expectedIDs = []
|
||
|
# for item in self.contacts[40:43]:
|
||
|
# expectedIDs.append(item.id)
|
||
|
# #print expectedIDs
|
||
|
#
|
||
|
# #print '#### storage nodes ####'
|
||
|
# #for node in storageNodes:
|
||
|
# # print node.id
|
||
|
#
|
||
|
#
|
||
|
# # check that the value has been stored at nodes with ID's close to the valueID
|
||
|
# self.failUnlessEqual(storageNodeIDs, expectedIDs, \
|
||
|
# "Value not stored at nodes with ID's close to the valueID")
|
||
|
#
|
||
|
# def testFindValue(self):
|
||
|
# # create test values using the contact ID as the key
|
||
|
# testValues = ({self.contacts[0].id: "some test data"},
|
||
|
# {self.contacts[1].id: "some more test data"},
|
||
|
# {self.contacts[8].id: "and more data"}
|
||
|
# )
|
||
|
#
|
||
|
#
|
||
|
# # create the network of contacts in format: (contact, closest contacts, values)
|
||
|
# contactNetwork = ((self.contacts[0], self.contacts[0:6], testValues[0]),
|
||
|
# (self.contacts[1], self.contacts[0:6], testValues[1]),
|
||
|
# (self.contacts[2], self.contacts[0:6], {'2':'2'}),
|
||
|
# (self.contacts[3], self.contacts[0:6], {'4':'5'}),
|
||
|
# (self.contacts[4], self.contacts[0:6], testValues[2]),
|
||
|
# (self.contacts[5], self.contacts[0:6], {'2':'2'}),
|
||
|
# (self.contacts[6], self.contacts[0:6], {'2':'2'}))
|
||
|
#
|
||
|
# self._protocol.createNetwork(contactNetwork)
|
||
|
#
|
||
|
# # Initialise the routing table with some contacts
|
||
|
# for contact in self.contacts[0:4]:
|
||
|
# self.node.addContact(contact)
|
||
|
#
|
||
|
# # Initialise the node with some known contacts
|
||
|
# #self.node._iterativeFind(self.node.id, self.contacts[0:3])
|
||
|
#
|
||
|
# df = self.node.iterativeFindValue(testValues[1].keys()[0])
|
||
|
#
|
||
|
# resultDict = df.result
|
||
|
# keys = resultDict.keys()
|
||
|
#
|
||
|
# for key in keys:
|
||
|
# if key == 'closestNodeNoValue':
|
||
|
# print "closest contact without data " + " " + resultDict.get(key).id
|
||
|
# else:
|
||
|
# print "data key :" + key + "; " + "data: " + resultDict.get(key)
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
def suite():
|
||
|
suite = unittest.TestSuite()
|
||
|
suite.addTest(unittest.makeSuite(NodeIDTest))
|
||
|
suite.addTest(unittest.makeSuite(NodeDataTest))
|
||
|
suite.addTest(unittest.makeSuite(NodeContactTest))
|
||
|
suite.addTest(unittest.makeSuite(NodeLookupTest))
|
||
|
return suite
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
# If this module is executed from the commandline, run all its tests
|
||
|
unittest.TextTestRunner().run(suite())
|