Bugfix: Only account shares for dynamic targetting after all checks have passed
[bitcoin:eloipool.git] / eloipool.py
1 #!/usr/bin/python3
2 # Eloipool - Python Bitcoin pool server
3 # Copyright (C) 2011-2012  Luke Dashjr <luke-jr+eloipool@utopios.org>
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU Affero General Public License for more details.
14 #
15 # You should have received a copy of the GNU Affero General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 import config
19
20 if not hasattr(config, 'ServerName'):
21         config.ServerName = 'Unnamed Eloipool'
22
23 if not hasattr(config, 'ShareTarget'):
24         config.ShareTarget = 0x00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff
25
26
27 import logging
28
29 if len(logging.root.handlers) == 0:
30         logging.basicConfig(
31                 format='%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s',
32                 level=logging.DEBUG,
33         )
34         for infoOnly in ('checkShare', 'JSONRPCHandler', 'merkleMaker', 'Waker for JSONRPCServer', 'JSONRPCServer'):
35                 logging.getLogger(infoOnly).setLevel(logging.INFO)
36
37 def RaiseRedFlags(reason):
38         logging.getLogger('redflag').critical(reason)
39         return reason
40
41
42 from bitcoin.node import BitcoinLink, BitcoinNode
43 bcnode = BitcoinNode(config.UpstreamNetworkId)
44 bcnode.userAgent += b'Eloipool:0.1/'
45
46 import jsonrpc
47 UpstreamBitcoindJSONRPC = jsonrpc.ServiceProxy(config.UpstreamURI)
48
49
50 from bitcoin.script import BitcoinScript
51 from bitcoin.txn import Txn
52 from base58 import b58decode
53 from struct import pack
54 import subprocess
55 from time import time
56
57 def makeCoinbaseTxn(coinbaseValue, useCoinbaser = True):
58         txn = Txn.new()
59         
60         if useCoinbaser and hasattr(config, 'CoinbaserCmd') and config.CoinbaserCmd:
61                 coinbased = 0
62                 try:
63                         cmd = config.CoinbaserCmd
64                         cmd = cmd.replace('%d', str(coinbaseValue))
65                         p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
66                         nout = int(p.stdout.readline())
67                         for i in range(nout):
68                                 amount = int(p.stdout.readline())
69                                 addr = p.stdout.readline().rstrip(b'\n').decode('utf8')
70                                 pkScript = BitcoinScript.toAddress(addr)
71                                 txn.addOutput(amount, pkScript)
72                                 coinbased += amount
73                 except:
74                         coinbased = coinbaseValue + 1
75                 if coinbased >= coinbaseValue:
76                         logging.getLogger('makeCoinbaseTxn').error('Coinbaser failed!')
77                         txn.outputs = []
78                 else:
79                         coinbaseValue -= coinbased
80         
81         pkScript = BitcoinScript.toAddress(config.TrackerAddr)
82         txn.addOutput(coinbaseValue, pkScript)
83         
84         # TODO
85         # TODO: red flag on dupe coinbase
86         return txn
87
88
89 import jsonrpc_getwork
90 from util import Bits2Target
91
92 workLog = {}
93 userStatus = {}
94 networkTarget = None
95 DupeShareHACK = {}
96
97 server = None
98 def updateBlocks():
99         server.wakeLongpoll()
100
101 def blockChanged():
102         global DupeShareHACK
103         DupeShareHACK = {}
104         jsonrpc_getwork._CheckForDupesHACK = {}
105         global MM, networkTarget, server
106         bits = MM.currentBlock[2]
107         if bits is None:
108                 networkTarget = None
109         else:
110                 networkTarget = Bits2Target(bits)
111         workLog.clear()
112         updateBlocks()
113
114
115 from merklemaker import merkleMaker
116 MM = merkleMaker()
117 MM.__dict__.update(config.__dict__)
118 MM.clearCoinbaseTxn = makeCoinbaseTxn(5000000000, False)  # FIXME
119 MM.clearCoinbaseTxn.assemble()
120 MM.makeCoinbaseTxn = makeCoinbaseTxn
121 MM.onBlockChange = blockChanged
122 MM.onBlockUpdate = updateBlocks
123
124
125 from binascii import b2a_hex
126 from copy import deepcopy
127 from struct import pack, unpack
128 import threading
129 from time import time
130 from util import RejectedShare, dblsha, hash2int, swap32
131 import jsonrpc
132 import traceback
133
134 gotwork = None
135 if hasattr(config, 'GotWorkURI'):
136         gotwork = jsonrpc.ServiceProxy(config.GotWorkURI)
137
138 if not hasattr(config, 'DynamicTargetting'):
139         config.DynamicTargetting = 0
140 else:
141         config.DynamicTargetGoal *= 2
142
143 def submitGotwork(info):
144         try:
145                 gotwork.gotwork(info)
146         except:
147                 checkShare.logger.warning('Failed to submit gotwork\n' + traceback.format_exc())
148
149 def getTarget(username, now):
150         if not config.DynamicTargetting:
151                 return None
152         if username in userStatus:
153                 status = userStatus[username]
154         else:
155                 userStatus[username] = [None, now, 0]
156                 return None
157         (targetIn, lastUpdate, work) = status
158         if work <= config.DynamicTargetGoal:
159                 if now < lastUpdate + 120:
160                         return targetIn
161                 if not work:
162                         if targetIn:
163                                 getTarget.logger.debug("No shares from '%s', resetting to minimum target")
164                                 userStatus[username] = [None, now, 0]
165                         return None
166         
167         deltaSec = now - lastUpdate
168         targetIn = targetIn or config.ShareTarget
169         target = targetIn
170         target = int(target * config.DynamicTargetGoal * deltaSec / 120 / work)
171         if target > config.ShareTarget:
172                 target = None
173         if target != targetIn:
174                 pfx = 'Retargetting %s' % (repr(username),)
175                 getTarget.logger.debug("%s from: %064x" % (pfx, targetIn,))
176                 getTarget.logger.debug("%s   to: %064x" % (pfx, target,))
177         userStatus[username] = [target, now, 0]
178         return target
179 getTarget.logger = logging.getLogger('getTarget')
180
181 def setWLI(username, wli, wld):
182         now = time()
183         target = getTarget(username, now)
184         workLog.setdefault(username, {})[wli] = (wld, now, target)
185         return target or config.ShareTarget
186
187 def getBlockHeader(username):
188         MRD = MM.getMRD()
189         (merkleRoot, merkleTree, coinbase, prevBlock, bits, rollPrevBlk) = MRD
190         timestamp = pack('<L', int(time()))
191         hdr = b'\2\0\0\0' + prevBlock + merkleRoot + timestamp + bits + b'iolE'
192         workLog.setdefault(username, {})[merkleRoot] = (MRD, time())
193         target = setWLI(username, merkleRoot, MRD)
194         return (hdr, workLog[username][merkleRoot], target)
195
196 def getBlockTemplate(username):
197         MC = MM.getMC()
198         (dummy, merkleTree, coinbase, prevBlock, bits) = MC
199         wliPos = coinbase[0] + 2
200         wliLen = coinbase[wliPos - 1]
201         wli = coinbase[wliPos:wliPos+wliLen]
202         target = setWLI(username, wli, MC)
203         return (MC, target)
204
205 loggersShare = []
206
207 RBDs = []
208 RBPs = []
209
210 from bitcoin.varlen import varlenEncode, varlenDecode
211 import bitcoin.txn
212 def assembleBlock(blkhdr, txlist):
213         payload = blkhdr
214         payload += varlenEncode(len(txlist))
215         for tx in txlist:
216                 payload += tx.data
217         return payload
218
219 def blockSubmissionThread(payload, blkhash):
220         myblock = (blkhash, payload[4:36])
221         payload = b2a_hex(payload).decode('ascii')
222         nexterr = 0
223         while True:
224                 try:
225                         rv = UpstreamBitcoindJSONRPC.submitblock(payload)
226                         break
227                 except:
228                         try:
229                                 rv = UpstreamBitcoindJSONRPC.getmemorypool(payload)
230                                 if rv is True:
231                                         rv = None
232                                 elif rv is False:
233                                         rv = 'rejected'
234                                 break
235                         except:
236                                 pass
237                         now = time()
238                         if now > nexterr:
239                                 # FIXME: This will show "Method not found" on pre-BIP22 servers
240                                 RaiseRedFlags(traceback.format_exc())
241                                 nexterr = now + 5
242                         if MM.currentBlock[0] not in myblock:
243                                 RaiseRedFlags('Giving up on submitting block upstream')
244                                 return
245         if rv:
246                 # FIXME: The returned value could be a list of multiple responses
247                 RaiseRedFlags('Upstream block submission failed: %s' % (rv,))
248
249 def checkShare(share):
250         shareTime = share['time'] = time()
251         
252         data = share['data']
253         data = data[:80]
254         (prevBlock, height, bits) = MM.currentBlock
255         sharePrevBlock = data[4:36]
256         if sharePrevBlock != prevBlock:
257                 if sharePrevBlock == MM.lastBlock[0]:
258                         raise RejectedShare('stale-prevblk')
259                 raise RejectedShare('bad-prevblk')
260         
261         # TODO: use userid
262         username = share['username']
263         if username not in workLog:
264                 raise RejectedShare('unknown-user')
265         
266         if data[72:76] != bits:
267                 raise RejectedShare('bad-diffbits')
268         
269         # Note that we should accept miners reducing version to 1 if they don't understand 2 yet
270         # FIXME: When the supermajority is upgraded to version 2, stop accepting 1!
271         if data[1:4] != b'\0\0\0' or data[0] > 2:
272                 raise RejectedShare('bad-version')
273         
274         shareMerkleRoot = data[36:68]
275         if 'blkdata' in share:
276                 pl = share['blkdata']
277                 (txncount, pl) = varlenDecode(pl)
278                 cbtxn = bitcoin.txn.Txn(pl)
279                 cbtxn.disassemble(retExtra=True)
280                 coinbase = cbtxn.getCoinbase()
281                 wliPos = coinbase[0] + 2
282                 wliLen = coinbase[wliPos - 1]
283                 wli = coinbase[wliPos:wliPos+wliLen]
284                 mode = 'MC'
285                 moden = 1
286         else:
287                 wli = shareMerkleRoot
288                 mode = 'MRD'
289                 moden = 0
290         
291         MWL = workLog[username]
292         if wli not in MWL:
293                 raise RejectedShare('unknown-work')
294         (wld, issueT, workTarget) = MWL[wli]
295         share[mode] = wld
296         
297         if data in DupeShareHACK:
298                 raise RejectedShare('duplicate')
299         DupeShareHACK[data] = None
300         
301         blkhash = dblsha(data)
302         if blkhash[28:] != b'\0\0\0\0':
303                 raise RejectedShare('H-not-zero')
304         blkhashn = hash2int(blkhash)
305         
306         global networkTarget
307         logfunc = getattr(checkShare.logger, 'info' if blkhashn <= networkTarget else 'debug')
308         logfunc('BLKHASH: %64x' % (blkhashn,))
309         logfunc(' TARGET: %64x' % (networkTarget,))
310         
311         workMerkleTree = wld[1]
312         workCoinbase = wld[2]
313         
314         # NOTE: this isn't actually needed for MC mode, but we're abusing it for a trivial share check...
315         txlist = workMerkleTree.data
316         txlist = [deepcopy(txlist[0]),] + txlist[1:]
317         cbtxn = txlist[0]
318         cbtxn.setCoinbase(workCoinbase)
319         cbtxn.assemble()
320         
321         if blkhashn <= networkTarget:
322                 logfunc("Submitting upstream")
323                 RBDs.append( deepcopy( (data, txlist, share.get('blkdata', None), workMerkleTree) ) )
324                 if not moden:
325                         payload = assembleBlock(data, txlist)
326                 else:
327                         payload = share['data'] + share['blkdata']
328                 logfunc('Real block payload: %s' % (b2a_hex(payload).decode('utf8'),))
329                 RBPs.append(payload)
330                 threading.Thread(target=blockSubmissionThread, args=(payload, blkhash)).start()
331                 bcnode.submitBlock(payload)
332                 share['upstreamResult'] = True
333                 MM.updateBlock(blkhash)
334         
335         # Gotwork hack...
336         if gotwork and blkhashn <= config.GotWorkTarget:
337                 try:
338                         coinbaseMrkl = cbtxn.data
339                         coinbaseMrkl += blkhash
340                         steps = workMerkleTree._steps
341                         coinbaseMrkl += pack('B', len(steps))
342                         for step in steps:
343                                 coinbaseMrkl += step
344                         coinbaseMrkl += b"\0\0\0\0"
345                         info = {}
346                         info['hash'] = b2a_hex(blkhash).decode('ascii')
347                         info['header'] = b2a_hex(data).decode('ascii')
348                         info['coinbaseMrkl'] = b2a_hex(coinbaseMrkl).decode('ascii')
349                         thr = threading.Thread(target=submitGotwork, args=(info,))
350                         thr.daemon = True
351                         thr.start()
352                 except:
353                         checkShare.logger.warning('Failed to build gotwork request')
354         
355         if workTarget is None:
356                 workTarget = config.ShareTarget
357         if blkhashn > workTarget:
358                 raise RejectedShare('high-hash')
359         share['target'] = workTarget
360         share['_targethex'] = '%064x' % (workTarget,)
361         
362         shareTimestamp = unpack('<L', data[68:72])[0]
363         if shareTime < issueT - 120:
364                 raise RejectedShare('stale-work')
365         if shareTimestamp < shareTime - 300:
366                 raise RejectedShare('time-too-old')
367         if shareTimestamp > shareTime + 7200:
368                 raise RejectedShare('time-too-new')
369         
370         if moden:
371                 cbpre = cbtxn.getCoinbase()
372                 cbpreLen = len(cbpre)
373                 if coinbase[:cbpreLen] != cbpre:
374                         raise RejectedShare('bad-cb-prefix')
375                 
376                 # Filter out known "I support" flags, to prevent exploits
377                 for ff in (b'/P2SH/', b'NOP2SH', b'p2sh/CHV', b'p2sh/NOCHV'):
378                         if coinbase.find(ff) > max(-1, cbpreLen - len(ff)):
379                                 raise RejectedShare('bad-cb-flag')
380                 
381                 if len(coinbase) > 100:
382                         raise RejectedShare('bad-cb-length')
383                 
384                 cbtxn.setCoinbase(coinbase)
385                 cbtxn.assemble()
386                 if shareMerkleRoot != workMerkleTree.withFirst(cbtxn):
387                         raise RejectedShare('bad-txnmrklroot')
388                 
389                 allowed = assembleBlock(data, txlist)
390                 if allowed != share['data'] + share['blkdata']:
391                         raise RejectedShare('bad-txns')
392         
393         status = userStatus[username]
394         target = status[0] or config.ShareTarget
395         if target == workTarget:
396                 userStatus[username][2] += 1
397         else:
398                 userStatus[username][2] += float(target) / workTarget
399 checkShare.logger = logging.getLogger('checkShare')
400
401 def receiveShare(share):
402         # TODO: username => userid
403         try:
404                 checkShare(share)
405         except RejectedShare as rej:
406                 share['rejectReason'] = str(rej)
407                 raise
408         finally:
409                 if '_origdata' in share:
410                         share['solution'] = share['_origdata']
411                 else:
412                         share['solution'] = b2a_hex(swap32(share['data'])).decode('utf8')
413                 for i in loggersShare:
414                         i(share)
415
416 def newBlockNotification():
417         logging.getLogger('newBlockNotification').info('Received new block notification')
418         MM.updateMerkleTree()
419         # TODO: Force RESPOND TO LONGPOLLS?
420         pass
421
422 def newBlockNotificationSIGNAL(signum, frame):
423         # Use a new thread, in case the signal handler is called with locks held
424         thr = threading.Thread(target=newBlockNotification, name='newBlockNotification via signal %s' % (signum,))
425         thr.daemon = True
426         thr.start()
427
428 from signal import signal, SIGUSR1
429 signal(SIGUSR1, newBlockNotificationSIGNAL)
430
431
432 import os
433 import os.path
434 import pickle
435 import signal
436 import sys
437 from time import sleep
438 import traceback
439
440 SAVE_STATE_FILENAME = 'eloipool.worklog'
441
442 def stopServers():
443         logger = logging.getLogger('stopServers')
444         
445         if hasattr(stopServers, 'already'):
446                 logger.debug('Already tried to stop servers before')
447                 return
448         stopServers.already = True
449         
450         logger.info('Stopping servers...')
451         global bcnode, server
452         servers = (bcnode, server)
453         for s in servers:
454                 s.keepgoing = False
455         for s in servers:
456                 try:
457                         s.wakeup()
458                 except:
459                         logger.error('Failed to stop server %s\n%s' % (s, traceback.format_exc()))
460         i = 0
461         while True:
462                 sl = []
463                 for s in servers:
464                         if s.running:
465                                 sl.append(s.__class__.__name__)
466                 if not sl:
467                         break
468                 i += 1
469                 if i >= 0x100:
470                         logger.error('Servers taking too long to stop (%s), giving up' % (', '.join(sl)))
471                         break
472                 sleep(0.01)
473         
474         for s in servers:
475                 for fd in s._fd.keys():
476                         os.close(fd)
477
478 def saveState(t = None):
479         logger = logging.getLogger('saveState')
480         
481         # Then, save data needed to resume work
482         logger.info('Saving work state to \'%s\'...' % (SAVE_STATE_FILENAME,))
483         i = 0
484         while True:
485                 try:
486                         with open(SAVE_STATE_FILENAME, 'wb') as f:
487                                 pickle.dump(t, f)
488                                 pickle.dump(DupeShareHACK, f)
489                                 pickle.dump(workLog, f)
490                         break
491                 except:
492                         i += 1
493                         if i >= 0x10000:
494                                 logger.error('Failed to save work\n' + traceback.format_exc())
495                                 try:
496                                         os.unlink(SAVE_STATE_FILENAME)
497                                 except:
498                                         logger.error(('Failed to unlink \'%s\'; resume may have trouble\n' % (SAVE_STATE_FILENAME,)) + traceback.format_exc())
499
500 def exit():
501         t = time()
502         stopServers()
503         saveState(t)
504         logging.getLogger('exit').info('Goodbye...')
505         os.kill(os.getpid(), signal.SIGTERM)
506         sys.exit(0)
507
508 def restart():
509         t = time()
510         stopServers()
511         saveState(t)
512         logging.getLogger('restart').info('Restarting...')
513         try:
514                 os.execv(sys.argv[0], sys.argv)
515         except:
516                 logging.getLogger('restart').error('Failed to exec\n' + traceback.format_exc())
517
518 def restoreState():
519         if not os.path.exists(SAVE_STATE_FILENAME):
520                 return
521         
522         global workLog, DupeShareHACK
523         
524         logger = logging.getLogger('restoreState')
525         s = os.stat(SAVE_STATE_FILENAME)
526         logger.info('Restoring saved state from \'%s\' (%d bytes)' % (SAVE_STATE_FILENAME, s.st_size))
527         try:
528                 with open(SAVE_STATE_FILENAME, 'rb') as f:
529                         t = pickle.load(f)
530                         if type(t) == tuple:
531                                 if len(t) > 2:
532                                         # Future formats, not supported here
533                                         ver = t[3]
534                                         # TODO
535                                 
536                                 # Old format, from 2012-02-02 to 2012-02-03
537                                 workLog = t[0]
538                                 DupeShareHACK = t[1]
539                                 t = None
540                         else:
541                                 if isinstance(t, dict):
542                                         # Old format, from 2012-02-03 to 2012-02-03
543                                         DupeShareHACK = t
544                                         t = None
545                                 else:
546                                         # Current format, from 2012-02-03 onward
547                                         DupeShareHACK = pickle.load(f)
548                                 
549                                 if t + 120 >= time():
550                                         workLog = pickle.load(f)
551                                 else:
552                                         logger.debug('Skipping restore of expired workLog')
553         except:
554                 logger.error('Failed to restore state\n' + traceback.format_exc())
555                 return
556         logger.info('State restored successfully')
557         if t:
558                 logger.info('Total downtime: %g seconds' % (time() - t,))
559
560
561 from jsonrpcserver import JSONRPCListener, JSONRPCServer
562 import interactivemode
563 from networkserver import NetworkListener
564 import threading
565 import sharelogging
566 import imp
567
568 if __name__ == "__main__":
569         if not hasattr(config, 'ShareLogging'):
570                 config.ShareLogging = ()
571         if hasattr(config, 'DbOptions'):
572                 logging.getLogger('backwardCompatibility').warn('DbOptions configuration variable is deprecated; upgrade to ShareLogging var before 2013-03-05')
573                 config.ShareLogging = list(config.ShareLogging)
574                 config.ShareLogging.append( {
575                         'type': 'sql',
576                         'engine': 'postgres',
577                         'dbopts': config.DbOptions,
578                         'statement': "insert into shares (rem_host, username, our_result, upstream_result, reason, solution) values ({Q(remoteHost)}, {username}, {YN(not(rejectReason))}, {YN(upstreamResult)}, {rejectReason}, decode({solution}, 'hex'))",
579                 } )
580         for i in config.ShareLogging:
581                 if not hasattr(i, 'keys'):
582                         name, parameters = i
583                         logging.getLogger('backwardCompatibility').warn('Using short-term backward compatibility for ShareLogging[\'%s\']; be sure to update config before 2012-04-04' % (name,))
584                         if name == 'postgres':
585                                 name = 'sql'
586                                 i = {
587                                         'engine': 'postgres',
588                                         'dbopts': parameters,
589                                 }
590                         elif name == 'logfile':
591                                 i = {}
592                                 i['thropts'] = parameters
593                                 if 'filename' in parameters:
594                                         i['filename'] = parameters['filename']
595                                         i['thropts'] = dict(i['thropts'])
596                                         del i['thropts']['filename']
597                         else:
598                                 i = parameters
599                         i['type'] = name
600                 
601                 name = i['type']
602                 parameters = i
603                 try:
604                         fp, pathname, description = imp.find_module(name, sharelogging.__path__)
605                         m = imp.load_module(name, fp, pathname, description)
606                         lo = getattr(m, name)(**parameters)
607                         loggersShare.append(lo.logShare)
608                 except:
609                         logging.getLogger('sharelogging').error("Error setting up share logger %s: %s", name,  sys.exc_info())
610
611         LSbc = []
612         if not hasattr(config, 'BitcoinNodeAddresses'):
613                 config.BitcoinNodeAddresses = ()
614         for a in config.BitcoinNodeAddresses:
615                 LSbc.append(NetworkListener(bcnode, a))
616         
617         if hasattr(config, 'UpstreamBitcoindNode') and config.UpstreamBitcoindNode:
618                 BitcoinLink(bcnode, dest=config.UpstreamBitcoindNode)
619         
620         import jsonrpc_getblocktemplate
621         import jsonrpc_getwork
622         import jsonrpc_setworkaux
623         
624         server = JSONRPCServer()
625         if hasattr(config, 'JSONRPCAddress'):
626                 logging.getLogger('backwardCompatibility').warn('JSONRPCAddress configuration variable is deprecated; upgrade to JSONRPCAddresses list before 2013-03-05')
627                 if not hasattr(config, 'JSONRPCAddresses'):
628                         config.JSONRPCAddresses = []
629                 config.JSONRPCAddresses.insert(0, config.JSONRPCAddress)
630         LS = []
631         for a in config.JSONRPCAddresses:
632                 LS.append(JSONRPCListener(server, a))
633         if hasattr(config, 'SecretUser'):
634                 server.SecretUser = config.SecretUser
635         server.aux = MM.CoinbaseAux
636         server.getBlockHeader = getBlockHeader
637         server.getBlockTemplate = getBlockTemplate
638         server.receiveShare = receiveShare
639         server.RaiseRedFlags = RaiseRedFlags
640         server.ShareTarget = config.ShareTarget
641         
642         if hasattr(config, 'TrustedForwarders'):
643                 server.TrustedForwarders = config.TrustedForwarders
644         server.ServerName = config.ServerName
645         
646         MM.start()
647         
648         restoreState()
649         
650         bcnode_thr = threading.Thread(target=bcnode.serve_forever)
651         bcnode_thr.daemon = True
652         bcnode_thr.start()
653         
654         server.serve_forever()