Bugfix: Non-dynamic target now works again, as well as restarts when dynamic is enabled
[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 RegisterWork(username, wli, wld):
182         now = time()
183         target = getTarget(username, now)
184         wld = tuple(wld) + (target,)
185         workLog.setdefault(username, {})[wli] = (wld, now)
186         return target or config.ShareTarget
187
188 def getBlockHeader(username):
189         MRD = MM.getMRD()
190         (merkleRoot, merkleTree, coinbase, prevBlock, bits, rollPrevBlk) = MRD[:6]
191         timestamp = pack('<L', int(time()))
192         hdr = b'\2\0\0\0' + prevBlock + merkleRoot + timestamp + bits + b'iolE'
193         workLog.setdefault(username, {})[merkleRoot] = (MRD, time())
194         target = RegisterWork(username, merkleRoot, MRD)
195         return (hdr, workLog[username][merkleRoot], target)
196
197 def getBlockTemplate(username):
198         MC = MM.getMC()
199         (dummy, merkleTree, coinbase, prevBlock, bits) = MC[:5]
200         wliPos = coinbase[0] + 2
201         wliLen = coinbase[wliPos - 1]
202         wli = coinbase[wliPos:wliPos+wliLen]
203         target = RegisterWork(username, wli, MC)
204         return (MC, workLog[username][wli], target)
205
206 loggersShare = []
207
208 RBDs = []
209 RBPs = []
210
211 from bitcoin.varlen import varlenEncode, varlenDecode
212 import bitcoin.txn
213 def assembleBlock(blkhdr, txlist):
214         payload = blkhdr
215         payload += varlenEncode(len(txlist))
216         for tx in txlist:
217                 payload += tx.data
218         return payload
219
220 def blockSubmissionThread(payload, blkhash):
221         myblock = (blkhash, payload[4:36])
222         payload = b2a_hex(payload).decode('ascii')
223         nexterr = 0
224         while True:
225                 try:
226                         rv = UpstreamBitcoindJSONRPC.submitblock(payload)
227                         break
228                 except:
229                         try:
230                                 rv = UpstreamBitcoindJSONRPC.getmemorypool(payload)
231                                 if rv is True:
232                                         rv = None
233                                 elif rv is False:
234                                         rv = 'rejected'
235                                 break
236                         except:
237                                 pass
238                         now = time()
239                         if now > nexterr:
240                                 # FIXME: This will show "Method not found" on pre-BIP22 servers
241                                 RaiseRedFlags(traceback.format_exc())
242                                 nexterr = now + 5
243                         if MM.currentBlock[0] not in myblock:
244                                 RaiseRedFlags('Giving up on submitting block upstream')
245                                 return
246         if rv:
247                 # FIXME: The returned value could be a list of multiple responses
248                 RaiseRedFlags('Upstream block submission failed: %s' % (rv,))
249
250 def checkShare(share):
251         shareTime = share['time'] = time()
252         
253         data = share['data']
254         data = data[:80]
255         (prevBlock, height, bits) = MM.currentBlock
256         sharePrevBlock = data[4:36]
257         if sharePrevBlock != prevBlock:
258                 if sharePrevBlock == MM.lastBlock[0]:
259                         raise RejectedShare('stale-prevblk')
260                 raise RejectedShare('bad-prevblk')
261         
262         # TODO: use userid
263         username = share['username']
264         if username not in workLog:
265                 raise RejectedShare('unknown-user')
266         
267         if data[72:76] != bits:
268                 raise RejectedShare('bad-diffbits')
269         
270         # Note that we should accept miners reducing version to 1 if they don't understand 2 yet
271         # FIXME: When the supermajority is upgraded to version 2, stop accepting 1!
272         if data[1:4] != b'\0\0\0' or data[0] > 2:
273                 raise RejectedShare('bad-version')
274         
275         shareMerkleRoot = data[36:68]
276         if 'blkdata' in share:
277                 pl = share['blkdata']
278                 (txncount, pl) = varlenDecode(pl)
279                 cbtxn = bitcoin.txn.Txn(pl)
280                 cbtxn.disassemble(retExtra=True)
281                 coinbase = cbtxn.getCoinbase()
282                 wliPos = coinbase[0] + 2
283                 wliLen = coinbase[wliPos - 1]
284                 wli = coinbase[wliPos:wliPos+wliLen]
285                 mode = 'MC'
286                 moden = 1
287         else:
288                 wli = shareMerkleRoot
289                 mode = 'MRD'
290                 moden = 0
291         
292         MWL = workLog[username]
293         if wli not in MWL:
294                 raise RejectedShare('unknown-work')
295         (wld, issueT) = MWL[wli]
296         share[mode] = wld
297         
298         if data in DupeShareHACK:
299                 raise RejectedShare('duplicate')
300         DupeShareHACK[data] = None
301         
302         blkhash = dblsha(data)
303         if blkhash[28:] != b'\0\0\0\0':
304                 raise RejectedShare('H-not-zero')
305         blkhashn = hash2int(blkhash)
306         
307         global networkTarget
308         logfunc = getattr(checkShare.logger, 'info' if blkhashn <= networkTarget else 'debug')
309         logfunc('BLKHASH: %64x' % (blkhashn,))
310         logfunc(' TARGET: %64x' % (networkTarget,))
311         
312         workMerkleTree = wld[1]
313         workCoinbase = wld[2]
314         workTarget = wld[6]
315         
316         # NOTE: this isn't actually needed for MC mode, but we're abusing it for a trivial share check...
317         txlist = workMerkleTree.data
318         txlist = [deepcopy(txlist[0]),] + txlist[1:]
319         cbtxn = txlist[0]
320         cbtxn.setCoinbase(workCoinbase)
321         cbtxn.assemble()
322         
323         if blkhashn <= networkTarget:
324                 logfunc("Submitting upstream")
325                 RBDs.append( deepcopy( (data, txlist, share.get('blkdata', None), workMerkleTree) ) )
326                 if not moden:
327                         payload = assembleBlock(data, txlist)
328                 else:
329                         payload = share['data'] + share['blkdata']
330                 logfunc('Real block payload: %s' % (b2a_hex(payload).decode('utf8'),))
331                 RBPs.append(payload)
332                 threading.Thread(target=blockSubmissionThread, args=(payload, blkhash)).start()
333                 bcnode.submitBlock(payload)
334                 share['upstreamResult'] = True
335                 MM.updateBlock(blkhash)
336         
337         # Gotwork hack...
338         if gotwork and blkhashn <= config.GotWorkTarget:
339                 try:
340                         coinbaseMrkl = cbtxn.data
341                         coinbaseMrkl += blkhash
342                         steps = workMerkleTree._steps
343                         coinbaseMrkl += pack('B', len(steps))
344                         for step in steps:
345                                 coinbaseMrkl += step
346                         coinbaseMrkl += b"\0\0\0\0"
347                         info = {}
348                         info['hash'] = b2a_hex(blkhash).decode('ascii')
349                         info['header'] = b2a_hex(data).decode('ascii')
350                         info['coinbaseMrkl'] = b2a_hex(coinbaseMrkl).decode('ascii')
351                         thr = threading.Thread(target=submitGotwork, args=(info,))
352                         thr.daemon = True
353                         thr.start()
354                 except:
355                         checkShare.logger.warning('Failed to build gotwork request')
356         
357         if workTarget is None:
358                 workTarget = config.ShareTarget
359         if blkhashn > workTarget:
360                 raise RejectedShare('high-hash')
361         share['target'] = workTarget
362         share['_targethex'] = '%064x' % (workTarget,)
363         
364         shareTimestamp = unpack('<L', data[68:72])[0]
365         if shareTime < issueT - 120:
366                 raise RejectedShare('stale-work')
367         if shareTimestamp < shareTime - 300:
368                 raise RejectedShare('time-too-old')
369         if shareTimestamp > shareTime + 7200:
370                 raise RejectedShare('time-too-new')
371         
372         if config.DynamicTargetting and username in userStatus:
373                 # NOTE: userStatus[username] only doesn't exist across restarts
374                 status = userStatus[username]
375                 target = status[0] or config.ShareTarget
376                 if target == workTarget:
377                         userStatus[username][2] += 1
378                 else:
379                         userStatus[username][2] += float(target) / workTarget
380         
381         if moden:
382                 cbpre = cbtxn.getCoinbase()
383                 cbpreLen = len(cbpre)
384                 if coinbase[:cbpreLen] != cbpre:
385                         raise RejectedShare('bad-cb-prefix')
386                 
387                 # Filter out known "I support" flags, to prevent exploits
388                 for ff in (b'/P2SH/', b'NOP2SH', b'p2sh/CHV', b'p2sh/NOCHV'):
389                         if coinbase.find(ff) > max(-1, cbpreLen - len(ff)):
390                                 raise RejectedShare('bad-cb-flag')
391                 
392                 if len(coinbase) > 100:
393                         raise RejectedShare('bad-cb-length')
394                 
395                 cbtxn.setCoinbase(coinbase)
396                 cbtxn.assemble()
397                 if shareMerkleRoot != workMerkleTree.withFirst(cbtxn):
398                         raise RejectedShare('bad-txnmrklroot')
399                 
400                 allowed = assembleBlock(data, txlist)
401                 if allowed != share['data'] + share['blkdata']:
402                         raise RejectedShare('bad-txns')
403 checkShare.logger = logging.getLogger('checkShare')
404
405 def receiveShare(share):
406         # TODO: username => userid
407         try:
408                 checkShare(share)
409         except RejectedShare as rej:
410                 share['rejectReason'] = str(rej)
411                 raise
412         finally:
413                 if '_origdata' in share:
414                         share['solution'] = share['_origdata']
415                 else:
416                         share['solution'] = b2a_hex(swap32(share['data'])).decode('utf8')
417                 for i in loggersShare:
418                         i(share)
419
420 def newBlockNotification():
421         logging.getLogger('newBlockNotification').info('Received new block notification')
422         MM.updateMerkleTree()
423         # TODO: Force RESPOND TO LONGPOLLS?
424         pass
425
426 def newBlockNotificationSIGNAL(signum, frame):
427         # Use a new thread, in case the signal handler is called with locks held
428         thr = threading.Thread(target=newBlockNotification, name='newBlockNotification via signal %s' % (signum,))
429         thr.daemon = True
430         thr.start()
431
432 from signal import signal, SIGUSR1
433 signal(SIGUSR1, newBlockNotificationSIGNAL)
434
435
436 import os
437 import os.path
438 import pickle
439 import signal
440 import sys
441 from time import sleep
442 import traceback
443
444 SAVE_STATE_FILENAME = 'eloipool.worklog'
445
446 def stopServers():
447         logger = logging.getLogger('stopServers')
448         
449         if hasattr(stopServers, 'already'):
450                 logger.debug('Already tried to stop servers before')
451                 return
452         stopServers.already = True
453         
454         logger.info('Stopping servers...')
455         global bcnode, server
456         servers = (bcnode, server)
457         for s in servers:
458                 s.keepgoing = False
459         for s in servers:
460                 try:
461                         s.wakeup()
462                 except:
463                         logger.error('Failed to stop server %s\n%s' % (s, traceback.format_exc()))
464         i = 0
465         while True:
466                 sl = []
467                 for s in servers:
468                         if s.running:
469                                 sl.append(s.__class__.__name__)
470                 if not sl:
471                         break
472                 i += 1
473                 if i >= 0x100:
474                         logger.error('Servers taking too long to stop (%s), giving up' % (', '.join(sl)))
475                         break
476                 sleep(0.01)
477         
478         for s in servers:
479                 for fd in s._fd.keys():
480                         os.close(fd)
481
482 def saveState(t = None):
483         logger = logging.getLogger('saveState')
484         
485         # Then, save data needed to resume work
486         logger.info('Saving work state to \'%s\'...' % (SAVE_STATE_FILENAME,))
487         i = 0
488         while True:
489                 try:
490                         with open(SAVE_STATE_FILENAME, 'wb') as f:
491                                 pickle.dump(t, f)
492                                 pickle.dump(DupeShareHACK, f)
493                                 pickle.dump(workLog, f)
494                         break
495                 except:
496                         i += 1
497                         if i >= 0x10000:
498                                 logger.error('Failed to save work\n' + traceback.format_exc())
499                                 try:
500                                         os.unlink(SAVE_STATE_FILENAME)
501                                 except:
502                                         logger.error(('Failed to unlink \'%s\'; resume may have trouble\n' % (SAVE_STATE_FILENAME,)) + traceback.format_exc())
503
504 def exit():
505         t = time()
506         stopServers()
507         saveState(t)
508         logging.getLogger('exit').info('Goodbye...')
509         os.kill(os.getpid(), signal.SIGTERM)
510         sys.exit(0)
511
512 def restart():
513         t = time()
514         stopServers()
515         saveState(t)
516         logging.getLogger('restart').info('Restarting...')
517         try:
518                 os.execv(sys.argv[0], sys.argv)
519         except:
520                 logging.getLogger('restart').error('Failed to exec\n' + traceback.format_exc())
521
522 def restoreState():
523         if not os.path.exists(SAVE_STATE_FILENAME):
524                 return
525         
526         global workLog, DupeShareHACK
527         
528         logger = logging.getLogger('restoreState')
529         s = os.stat(SAVE_STATE_FILENAME)
530         logger.info('Restoring saved state from \'%s\' (%d bytes)' % (SAVE_STATE_FILENAME, s.st_size))
531         try:
532                 with open(SAVE_STATE_FILENAME, 'rb') as f:
533                         t = pickle.load(f)
534                         if type(t) == tuple:
535                                 if len(t) > 2:
536                                         # Future formats, not supported here
537                                         ver = t[3]
538                                         # TODO
539                                 
540                                 # Old format, from 2012-02-02 to 2012-02-03
541                                 workLog = t[0]
542                                 DupeShareHACK = t[1]
543                                 t = None
544                         else:
545                                 if isinstance(t, dict):
546                                         # Old format, from 2012-02-03 to 2012-02-03
547                                         DupeShareHACK = t
548                                         t = None
549                                 else:
550                                         # Current format, from 2012-02-03 onward
551                                         DupeShareHACK = pickle.load(f)
552                                 
553                                 if t + 120 >= time():
554                                         workLog = pickle.load(f)
555                                 else:
556                                         logger.debug('Skipping restore of expired workLog')
557         except:
558                 logger.error('Failed to restore state\n' + traceback.format_exc())
559                 return
560         logger.info('State restored successfully')
561         if t:
562                 logger.info('Total downtime: %g seconds' % (time() - t,))
563
564
565 from jsonrpcserver import JSONRPCListener, JSONRPCServer
566 import interactivemode
567 from networkserver import NetworkListener
568 import threading
569 import sharelogging
570 import imp
571
572 if __name__ == "__main__":
573         if not hasattr(config, 'ShareLogging'):
574                 config.ShareLogging = ()
575         if hasattr(config, 'DbOptions'):
576                 logging.getLogger('backwardCompatibility').warn('DbOptions configuration variable is deprecated; upgrade to ShareLogging var before 2013-03-05')
577                 config.ShareLogging = list(config.ShareLogging)
578                 config.ShareLogging.append( {
579                         'type': 'sql',
580                         'engine': 'postgres',
581                         'dbopts': config.DbOptions,
582                         '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'))",
583                 } )
584         for i in config.ShareLogging:
585                 if not hasattr(i, 'keys'):
586                         name, parameters = i
587                         logging.getLogger('backwardCompatibility').warn('Using short-term backward compatibility for ShareLogging[\'%s\']; be sure to update config before 2012-04-04' % (name,))
588                         if name == 'postgres':
589                                 name = 'sql'
590                                 i = {
591                                         'engine': 'postgres',
592                                         'dbopts': parameters,
593                                 }
594                         elif name == 'logfile':
595                                 i = {}
596                                 i['thropts'] = parameters
597                                 if 'filename' in parameters:
598                                         i['filename'] = parameters['filename']
599                                         i['thropts'] = dict(i['thropts'])
600                                         del i['thropts']['filename']
601                         else:
602                                 i = parameters
603                         i['type'] = name
604                 
605                 name = i['type']
606                 parameters = i
607                 try:
608                         fp, pathname, description = imp.find_module(name, sharelogging.__path__)
609                         m = imp.load_module(name, fp, pathname, description)
610                         lo = getattr(m, name)(**parameters)
611                         loggersShare.append(lo.logShare)
612                 except:
613                         logging.getLogger('sharelogging').error("Error setting up share logger %s: %s", name,  sys.exc_info())
614
615         LSbc = []
616         if not hasattr(config, 'BitcoinNodeAddresses'):
617                 config.BitcoinNodeAddresses = ()
618         for a in config.BitcoinNodeAddresses:
619                 LSbc.append(NetworkListener(bcnode, a))
620         
621         if hasattr(config, 'UpstreamBitcoindNode') and config.UpstreamBitcoindNode:
622                 BitcoinLink(bcnode, dest=config.UpstreamBitcoindNode)
623         
624         import jsonrpc_getblocktemplate
625         import jsonrpc_getwork
626         import jsonrpc_setworkaux
627         
628         server = JSONRPCServer()
629         if hasattr(config, 'JSONRPCAddress'):
630                 logging.getLogger('backwardCompatibility').warn('JSONRPCAddress configuration variable is deprecated; upgrade to JSONRPCAddresses list before 2013-03-05')
631                 if not hasattr(config, 'JSONRPCAddresses'):
632                         config.JSONRPCAddresses = []
633                 config.JSONRPCAddresses.insert(0, config.JSONRPCAddress)
634         LS = []
635         for a in config.JSONRPCAddresses:
636                 LS.append(JSONRPCListener(server, a))
637         if hasattr(config, 'SecretUser'):
638                 server.SecretUser = config.SecretUser
639         server.aux = MM.CoinbaseAux
640         server.getBlockHeader = getBlockHeader
641         server.getBlockTemplate = getBlockTemplate
642         server.receiveShare = receiveShare
643         server.RaiseRedFlags = RaiseRedFlags
644         server.ShareTarget = config.ShareTarget
645         
646         if hasattr(config, 'TrustedForwarders'):
647                 server.TrustedForwarders = config.TrustedForwarders
648         server.ServerName = config.ServerName
649         
650         MM.start()
651         
652         restoreState()
653         
654         bcnode_thr = threading.Thread(target=bcnode.serve_forever)
655         bcnode_thr.daemon = True
656         bcnode_thr.start()
657         
658         server.serve_forever()