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:
parent
15857e6551
commit
2d34e73648
|
@ -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", '') != '':
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user