Dandelion updates

- fixes and feedback from @gfanti and @amiller
- addresses #1049
- minor refactoring
- two global child stems with fixed mapping between parent and
child stem
- allow child stems which don't support dandelion
- only allow outbound connections to be stems
- adjust stems if opening/closing outbound connections (should
allow partial dandelion functionality when not enough outbound
connections are available instead of breaking)
This commit is contained in:
Peter Šurda 2017-10-20 01:21:49 +02:00
parent 15857e6551
commit 2d34e73648
Signed by untrusted user: PeterSurda
GPG Key ID: 0C5F50C0B5F37D87
9 changed files with 122 additions and 68 deletions

View File

@ -54,7 +54,7 @@ from bmconfigparser import BMConfigParser
from inventory import Inventory from inventory import Inventory
from network.connectionpool import BMConnectionPool from network.connectionpool import BMConnectionPool
from network.dandelion import DandelionStems from network.dandelion import Dandelion
from network.networkthread import BMNetworkThread from network.networkthread import BMNetworkThread
from network.receivequeuethread import ReceiveQueueThread from network.receivequeuethread import ReceiveQueueThread
from network.announcethread import AnnounceThread from network.announcethread import AnnounceThread
@ -251,7 +251,7 @@ class Main:
sqlLookup.start() sqlLookup.start()
Inventory() # init Inventory() # init
DandelionStems() # init, needs to be early because other thread may access it early Dandelion() # init, needs to be early because other thread may access it early
# SMTP delivery thread # SMTP delivery thread
if daemon and BMConfigParser().safeGet("bitmessagesettings", "smtpdeliver", '') != '': if daemon and BMConfigParser().safeGet("bitmessagesettings", "smtpdeliver", '') != '':

View File

@ -10,7 +10,7 @@ from helper_sql import *
from helper_threading import * from helper_threading import *
from inventory import Inventory from inventory import Inventory
from network.connectionpool import BMConnectionPool from network.connectionpool import BMConnectionPool
from network.dandelion import DandelionStems from network.dandelion import Dandelion
from debug import logger from debug import logger
import knownnodes import knownnodes
import queues import queues
@ -136,9 +136,7 @@ class singleCleaner(threading.Thread, StoppableThread):
for connection in BMConnectionPool().inboundConnections.values() + BMConnectionPool().outboundConnections.values(): for connection in BMConnectionPool().inboundConnections.values() + BMConnectionPool().outboundConnections.values():
connection.clean() connection.clean()
# dandelion fluff trigger by expiration # dandelion fluff trigger by expiration
for h, t in DandelionStems().timeouts: Dandelion().expire()
if time.time() > t:
DandelionStems().remove(h)
# discovery tracking # discovery tracking
exp = time.time() - singleCleaner.expireDiscoveredPeers exp = time.time() - singleCleaner.expireDiscoveredPeers

View File

@ -4,7 +4,7 @@ import time
from addresses import calculateInventoryHash from addresses import calculateInventoryHash
from debug import logger from debug import logger
from inventory import Inventory from inventory import Inventory
from network.dandelion import DandelionStems from network.dandelion import Dandelion
import protocol import protocol
import state import state
@ -68,7 +68,7 @@ class BMObject(object):
def checkAlreadyHave(self): def checkAlreadyHave(self):
# if it's a stem duplicate, pretend we don't have it # if it's a stem duplicate, pretend we don't have it
if self.inventoryHash in DandelionStems().stem: if self.inventoryHash in Dandelion().hashMap:
return return
if self.inventoryHash in Inventory(): if self.inventoryHash in Inventory():
raise BMObjectAlreadyHaveError() raise BMObjectAlreadyHaveError()

View File

@ -10,7 +10,7 @@ from debug import logger
from inventory import Inventory from inventory import Inventory
import knownnodes import knownnodes
from network.advanceddispatcher import AdvancedDispatcher from network.advanceddispatcher import AdvancedDispatcher
from network.dandelion import DandelionStems, REASSIGN_INTERVAL from network.dandelion import Dandelion
from network.bmobject import BMObject, BMObjectInsufficientPOWError, BMObjectInvalidDataError, \ from network.bmobject import BMObject, BMObjectInsufficientPOWError, BMObjectInvalidDataError, \
BMObjectExpiredError, BMObjectUnwantedStreamError, BMObjectInvalidError, BMObjectAlreadyHaveError BMObjectExpiredError, BMObjectUnwantedStreamError, BMObjectInvalidError, BMObjectAlreadyHaveError
import network.connectionpool import network.connectionpool
@ -279,8 +279,8 @@ class BMProto(AdvancedDispatcher, ObjectTracker):
#TODO make this more asynchronous #TODO make this more asynchronous
random.shuffle(items) random.shuffle(items)
for i in map(str, items): for i in map(str, items):
if i in DandelionStems().stem and \ if i in Dandelion().hashMap and \
self != DandelionStems().stem[i]: self != Dandelion().hashMap[i]:
self.antiIntersectionDelay() self.antiIntersectionDelay()
logger.info('%s asked for a stem object we didn\'t offer to it.', self.destination) logger.info('%s asked for a stem object we didn\'t offer to it.', self.destination)
break break
@ -325,12 +325,8 @@ class BMProto(AdvancedDispatcher, ObjectTracker):
if BMConfigParser().safeGetBoolean("network", "dandelion") == 0: if BMConfigParser().safeGetBoolean("network", "dandelion") == 0:
return True return True
if self.dandelionRefresh < time.time():
self.dandelionRoutes = BMConnectionPool.dandelionRouteSelector(self)
self.dandelionRefresh = time.time() + REASSIGN_INTERVAL
for i in map(str, items): for i in map(str, items):
DandelionStems().add(i, self, self.dandelionRoutes) Dandelion().addHash(i, self)
self.handleReceivedInventory(i) self.handleReceivedInventory(i)
return True return True

View File

@ -9,6 +9,7 @@ from debug import logger
import helper_bootstrap import helper_bootstrap
from network.proxy import Proxy from network.proxy import Proxy
import network.bmproto import network.bmproto
from network.dandelion import Dandelion
import network.tcp import network.tcp
import network.udp import network.udp
from network.connectionchooser import chooseConnection from network.connectionchooser import chooseConnection
@ -51,23 +52,10 @@ class BMConnectionPool(object):
except KeyError: except KeyError:
pass pass
def dandelionRouteSelector(self, node): def reRandomiseDandelionStems(self):
# Choose 2 peers randomly # Choose 2 peers randomly
# TODO: handle streams # TODO: handle streams
peers = [] Dandelion().reRandomiseStems(self.outboundConnections.values())
connections = self.outboundConnections.values()
random.shuffle(connections)
for i in connections:
if i == node:
continue
try:
if i.services | protocol.NODE_DANDELION:
peers.append(i)
if len(peers) == 2:
break
except AttributeError:
continue
return peers
def connectToStream(self, streamNumber): def connectToStream(self, streamNumber):
self.streams.append(streamNumber) self.streams.append(streamNumber)

View File

@ -1,4 +1,4 @@
from random import choice from random import choice, shuffle
from threading import RLock from threading import RLock
from time import time from time import time
@ -8,31 +8,101 @@ from singleton import Singleton
# randomise routes after 600 seconds # randomise routes after 600 seconds
REASSIGN_INTERVAL = 600 REASSIGN_INTERVAL = 600
FLUFF_TRIGGER_TIMEOUT = 300 FLUFF_TRIGGER_TIMEOUT = 300
MAX_STEMS = 2
@Singleton @Singleton
class DandelionStems(): class Dandelion():
def __init__(self): def __init__(self):
self.stem = {} self.stem = []
self.source = {} self.nodeMap = {}
self.timeouts = {} self.hashMap = {}
self.timeout = {}
self.refresh = time() + REASSIGN_INTERVAL
self.lock = RLock() self.lock = RLock()
def add(self, hashId, source, stems): def addHash(self, hashId, source):
if BMConfigParser().safeGetInt('network', 'dandelion') == 0: if BMConfigParser().safeGetInt('network', 'dandelion') == 0:
return return
with self.lock: with self.lock:
try: self.hashMap[hashId] = self.getNodeStem(source)
self.stem[hashId] = choice(stems) self.timeout[hashId] = time() + FLUFF_TRIGGER_TIMEOUT
except IndexError:
self.stem = None
self.source[hashId] = source
self.timeouts[hashId] = time()
def remove(self, hashId): def removeHash(self, hashId):
with self.lock: with self.lock:
try: try:
del self.stem[hashId] del self.hashMap[hashId]
del self.source[hashId]
del self.timeouts[hashId]
except KeyError: except KeyError:
pass pass
try:
del self.timeout[hashId]
except KeyError:
pass
def maybeAddStem(self, connection):
# fewer than MAX_STEMS outbound connections at last reshuffle?
with self.lock:
if len(self.stem) < MAX_STEMS:
self.stem.append(connection)
# active mappings pointing nowhere
for k in (k for k, v in self.nodeMap.iteritems() if self.nodeMap[k] is None):
self.nodeMap[k] = connection
for k in (k for k, v in self.hashMap.iteritems() if self.hashMap[k] is None):
self.hashMap[k] = connection
def maybeRemoveStem(self, connection):
# is the stem active?
with self.lock:
if connection in self.stem:
self.stem.remove(connection)
# active mappings to pointing to the removed node
for k in (k for k, v in self.nodeMap.iteritems() if self.nodeMap[k] == connection):
self.nodeMap[k] = None
for k in (k for k, v in self.hashMap.iteritems() if self.hashMap[k] == connection):
self.hashMap[k] = None
if len(self.stem) < MAX_STEMS:
self.stem.append(connection)
def pickStem(self, parent=None):
try:
# pick a random from available stems
stem = choice(range(len(self.stem)))
if self.stem[stem] == parent:
# one stem available and it's the parent
if len(self.stem) == 1:
return None
# else, pick the other one
return self.stem[1 - stem]
# all ok
return self.stem[stem]
except IndexError:
# no stems available
return None
def getNodeStem(self, node=None):
with self.lock:
try:
return self.nodeMap[node]
except KeyError:
self.nodeMap[node] = self.pickStem()
return self.nodeMap[node]
def getHashStem(self, hashId):
with self.lock:
return self.hashMap[hashId]
def expire(self):
with self.lock:
deadline = time()
toDelete = [k for k, v in self.hashMap.iteritems() if self.timeout[k] < deadline]
for k in toDelete:
del self.timeout[k]
del self.hashMap[k]
def reRandomiseStems(self, connections):
shuffle(connections)
with self.lock:
# random two connections
self.stem = connections[:MAX_STEMS]
self.nodeMap = {}
# hashMap stays to cater for pending stems
self.refresh = time() + REASSIGN_INTERVAL

View File

@ -7,7 +7,7 @@ import addresses
from bmconfigparser import BMConfigParser from bmconfigparser import BMConfigParser
from helper_threading import StoppableThread from helper_threading import StoppableThread
from network.connectionpool import BMConnectionPool from network.connectionpool import BMConnectionPool
from network.dandelion import DandelionStems, REASSIGN_INTERVAL from network.dandelion import Dandelion
from queues import invQueue from queues import invQueue
import protocol import protocol
import state import state
@ -17,26 +17,17 @@ class InvThread(threading.Thread, StoppableThread):
threading.Thread.__init__(self, name="InvBroadcaster") threading.Thread.__init__(self, name="InvBroadcaster")
self.initStop() self.initStop()
self.name = "InvBroadcaster" self.name = "InvBroadcaster"
# for locally generated objects
self.dandelionRoutes = []
self.dandelionRefresh = 0
def dandelionLocalRouteRefresh(self):
if self.dandelionRefresh < time():
self.dandelionRoutes = BMConnectionPool().dandelionRouteSelector(None)
self.dandelionRefresh = time() + REASSIGN_INTERVAL
def run(self): def run(self):
while not state.shutdown: while not state.shutdown:
chunk = [] chunk = []
while True: while True:
self.dandelionLocalRouteRefresh()
try: try:
data = invQueue.get(False) data = invQueue.get(False)
chunk.append((data[0], data[1])) chunk.append((data[0], data[1]))
# locally generated # locally generated
if len(data) == 2: if len(data) == 2:
DandelionStems().add(data[1], None, self.dandelionRoutes) Dandelion().addHash(data[1], None)
BMConnectionPool().handleReceivedObject(data[0], data[1]) BMConnectionPool().handleReceivedObject(data[0], data[1])
# came over the network # came over the network
else: else:
@ -61,17 +52,19 @@ class InvThread(threading.Thread, StoppableThread):
del connection.objectsNewToThem[inv[1]] del connection.objectsNewToThem[inv[1]]
except KeyError: except KeyError:
continue continue
if inv[1] in DandelionStems().stem: try:
if connection == DandelionStems().stem[inv[1]]: if connection == Dandelion().hashMap[inv[1]]:
# Fluff trigger by RNG # Fluff trigger by RNG
# auto-ignore if config set to 0, i.e. dandelion is off # auto-ignore if config set to 0, i.e. dandelion is off
if randint(1, 100) < BMConfigParser().safeGetBoolean("network", "dandelion"): # send a normal inv if stem node doesn't support dandelion
if randint(1, 100) < BMConfigParser().safeGetBoolean("network", "dandelion") and \
connection.services | protocol.NODE_DANDELION > 0:
stems.append(inv[1]) stems.append(inv[1])
else: else:
fluffs.append(inv[1]) fluffs.append(inv[1])
continue except KeyError:
else:
fluffs.append(inv[1]) fluffs.append(inv[1])
if fluffs: if fluffs:
shuffle(fluffs) shuffle(fluffs)
connection.append_write_buf(protocol.CreatePacket('inv', \ connection.append_write_buf(protocol.CreatePacket('inv', \
@ -80,7 +73,12 @@ class InvThread(threading.Thread, StoppableThread):
shuffle(stems) shuffle(stems)
connection.append_write_buf(protocol.CreatePacket('dinv', \ connection.append_write_buf(protocol.CreatePacket('dinv', \
addresses.encodeVarint(len(stems)) + "".join(stems))) addresses.encodeVarint(len(stems)) + "".join(stems)))
invQueue.iterate() invQueue.iterate()
for i in range(len(chunk)): for i in range(len(chunk)):
invQueue.task_done() invQueue.task_done()
if Dandelion().refresh < time():
BMConnectionPool().reRandomiseDandelionStems()
self.stop.wait(1) self.stop.wait(1)

View File

@ -4,7 +4,7 @@ from threading import RLock
from debug import logger from debug import logger
from inventory import Inventory from inventory import Inventory
from network.dandelion import DandelionStems from network.dandelion import Dandelion
haveBloom = False haveBloom = False
@ -84,9 +84,9 @@ class ObjectTracker(object):
if hashId not in Inventory(): if hashId not in Inventory():
with self.objectsNewToMeLock: with self.objectsNewToMeLock:
self.objectsNewToMe[hashId] = True self.objectsNewToMe[hashId] = True
elif hashId in DandelionStems().stem: elif hashId in Dandelion().hashMap:
# Fluff trigger by cycle detection # Fluff trigger by cycle detection
DandelionStems().remove(hashId) Dandelion().removeHash(hashId)
with self.objectsNewToMeLock: with self.objectsNewToMeLock:
self.objectsNewToMe[hashId] = True self.objectsNewToMe[hashId] = True

View File

@ -18,7 +18,7 @@ from network.advanceddispatcher import AdvancedDispatcher
from network.bmproto import BMProtoError, BMProtoInsufficientDataError, BMProtoExcessiveDataError, BMProto from network.bmproto import BMProtoError, BMProtoInsufficientDataError, BMProtoExcessiveDataError, BMProto
from network.bmobject import BMObject, BMObjectInsufficientPOWError, BMObjectInvalidDataError, BMObjectExpiredError, BMObjectUnwantedStreamError, BMObjectInvalidError, BMObjectAlreadyHaveError from network.bmobject import BMObject, BMObjectInsufficientPOWError, BMObjectInvalidDataError, BMObjectExpiredError, BMObjectUnwantedStreamError, BMObjectInvalidError, BMObjectAlreadyHaveError
import network.connectionpool import network.connectionpool
from network.dandelion import DandelionStems from network.dandelion import Dandelion
from network.node import Node from network.node import Node
import network.asyncore_pollchoose as asyncore import network.asyncore_pollchoose as asyncore
from network.proxy import Proxy, ProxyError, GeneralProxyError from network.proxy import Proxy, ProxyError, GeneralProxyError
@ -106,6 +106,8 @@ class TCPConnection(BMProto, TLSDispatcher):
self.fullyEstablished = True self.fullyEstablished = True
if self.isOutbound: if self.isOutbound:
knownnodes.increaseRating(self.destination) knownnodes.increaseRating(self.destination)
if self.isOutbound:
Dandelion().maybeAddStem(self)
self.sendAddr() self.sendAddr()
self.sendBigInv() self.sendBigInv()
@ -166,7 +168,7 @@ class TCPConnection(BMProto, TLSDispatcher):
with self.objectsNewToThemLock: with self.objectsNewToThemLock:
for objHash in Inventory().unexpired_hashes_by_stream(stream): for objHash in Inventory().unexpired_hashes_by_stream(stream):
# don't advertise stem objects on bigInv # don't advertise stem objects on bigInv
if objHash in DandelionStems().stem: if objHash in Dandelion().hashMap:
continue continue
bigInvList[objHash] = 0 bigInvList[objHash] = 0
self.objectsNewToThem[objHash] = time.time() self.objectsNewToThem[objHash] = time.time()
@ -218,6 +220,8 @@ class TCPConnection(BMProto, TLSDispatcher):
knownnodes.decreaseRating(self.destination) knownnodes.decreaseRating(self.destination)
if self.fullyEstablished: if self.fullyEstablished:
UISignalQueue.put(('updateNetworkStatusTab', (self.isOutbound, False, self.destination))) UISignalQueue.put(('updateNetworkStatusTab', (self.isOutbound, False, self.destination)))
if self.isOutbound:
Dandelion().maybeRemoveStem(self)
BMProto.handle_close(self) BMProto.handle_close(self)