Bugfix: Check fd still has a handler object, in case it has been destroyed in the...
[bitcoin:eloipool.git] / eloipool.py
1 #!/usr/bin/python3
2 import config
3
4
5 import logging
6
7 logging.basicConfig(level=logging.DEBUG)
8 for infoOnly in ('checkShare', 'JSONRPCHandler', 'merkleMaker'):
9         logging.getLogger(infoOnly).setLevel(logging.INFO)
10
11 def RaiseRedFlags(reason):
12         logging.getLogger('redflag').critical(reason)
13         return reason
14
15
16 from bitcoin.node import BitcoinLink
17 UpstreamBitcoind = BitcoinLink( config.UpstreamBitcoindNode, config.UpstreamNetworkId )
18
19
20 from bitcoin.script import BitcoinScript
21 from bitcoin.txn import Txn
22 from base58 import b58decode
23 from struct import pack
24 import subprocess
25 from time import time
26
27 def makeCoinbaseTxn(coinbaseValue, useCoinbaser = True):
28         t = Txn.new()
29         
30         if useCoinbaser and hasattr(config, 'CoinbaserCmd'):
31                 coinbased = 0
32                 try:
33                         cmd = config.CoinbaserCmd
34                         cmd = cmd.replace('%d', str(coinbaseValue))
35                         p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
36                         nout = int(p.stdout.readline())
37                         for i in range(nout):
38                                 amount = int(p.stdout.readline())
39                                 addr = p.stdout.readline().rstrip(b'\n').decode('utf8')
40                                 pkScript = BitcoinScript.toAddress(addr)
41                                 t.addOutput(amount, pkScript)
42                                 coinbased += amount
43                 except:
44                         coinbased = coinbaseValue + 1
45                 if coinbased >= coinbaseValue:
46                         logging.getLogger('makeCoinbaseTxn').error('Coinbaser failed!')
47                         t.outputs = []
48                 else:
49                         coinbaseValue -= coinbased
50         
51         pkScript = BitcoinScript.toAddress(config.TrackerAddr)
52         t.addOutput(coinbaseValue, pkScript)
53         
54         # TODO
55         # TODO: red flag on dupe coinbase
56         return t
57
58
59 from util import Bits2Target
60
61 workLog = {}
62 networkTarget = None
63 DupeShareHACK = {}
64
65 server = None
66 def updateBlocks():
67         if server:
68                 server.wakeLongpoll()
69
70 def blockChanged():
71         global DupeShareHACK
72         DupeShareHACK = {}
73         global MM, networkTarget, server
74         networkTarget = Bits2Target(MM.currentBlock[1])
75         workLog.clear()
76         updateBlocks()
77
78
79 from merklemaker import merkleMaker
80 MM = merkleMaker()
81 MM.__dict__.update(config.__dict__)
82 MM.clearCoinbaseTxn = makeCoinbaseTxn(5000000000, False)  # FIXME
83 MM.clearCoinbaseTxn.assemble()
84 MM.makeCoinbaseTxn = makeCoinbaseTxn
85 MM.onBlockChange = blockChanged
86 MM.onBlockUpdate = updateBlocks
87 MM._THISISUGLY = UpstreamBitcoind
88 MM.start()
89
90
91 from binascii import b2a_hex
92 from struct import pack, unpack
93 from time import time
94 from util import RejectedShare, dblsha, hash2int
95 import jsonrpc
96 import threading
97
98 gotwork = None
99 if hasattr(config, 'GotWorkURI'):
100         gotwork = jsonrpc.ServiceProxy(config.GotWorkURI)
101
102 def submitGotwork(info):
103         try:
104                 gotwork.gotwork(info)
105         except:
106                 checkShare.logger.warning('Failed to submit gotwork')
107
108 db = None
109 if hasattr(config, 'DbOptions'):
110         import psycopg2
111         db = psycopg2.connect(**config.DbOptions)
112
113 def getBlockHeader(username):
114         MRD = MM.getMRD()
115         (merkleRoot, merkleTree, coinbase, prevBlock, bits, rollPrevBlk) = MRD
116         timestamp = pack('<L', int(time()))
117         hdr = b'\1\0\0\0' + prevBlock + merkleRoot + timestamp + bits + b'iolE'
118         workLog.setdefault(username, {})[merkleRoot] = MRD
119         return hdr
120
121 def YN(b):
122         if b is None:
123                 return None
124         return 'Y' if b else 'N'
125
126 def logShare(share):
127         if db is None:
128                 return
129         dbc = db.cursor()
130         rem_host = share.get('remoteHost', '?')
131         username = share['username']
132         reason = share.get('rejectReason', None)
133         upstreamResult = share.get('upstreamResult', None)
134         solution = share['_origdata']
135         #solution = b2a_hex(solution).decode('utf8')
136         stmt = "insert into shares (rem_host, username, our_result, upstream_result, reason, solution) values (%s, %s, %s, %s, %s, decode(%s, 'hex'))"
137         params = (rem_host, username, YN(not reason), YN(upstreamResult), reason, solution)
138         dbc.execute(stmt, params)
139         db.commit()
140
141 def checkShare(share):
142         data = share['data']
143         data = data[:80]
144         (prevBlock, bits) = MM.currentBlock
145         sharePrevBlock = data[4:36]
146         if sharePrevBlock != prevBlock:
147                 if sharePrevBlock == MM.lastBlock[0]:
148                         raise RejectedShare('stale-prevblk')
149                 raise RejectedShare('bad-prevblk')
150         
151         shareMerkleRoot = data[36:68]
152         # TODO: use userid
153         username = share['username']
154         if username not in workLog:
155                 raise RejectedShare('unknown-user')
156         
157         if data[72:76] != bits:
158                 raise RejectedShare('bad-diffbits')
159         if data[:4] != b'\1\0\0\0':
160                 raise RejectedShare('bad-version')
161         
162         MWL = workLog[username]
163         if shareMerkleRoot not in MWL:
164                 raise RejectedShare('unknown-work')
165         MRD = MWL[shareMerkleRoot]
166         share['MRD'] = MRD
167         
168         if data in DupeShareHACK:
169                 raise RejectedShare('duplicate')
170         DupeShareHACK[data] = None
171         
172         shareTimestamp = unpack('<L', data[68:72])[0]
173         shareTime = share['time'] = time()
174         if shareTimestamp < shareTime - 300:
175                 raise RejectedShare('time-too-old')
176         if shareTimestamp > shareTime + 7200:
177                 raise RejectedShare('time-too-new')
178         
179         blkhash = dblsha(data)
180         if blkhash[28:] != b'\0\0\0\0':
181                 raise RejectedShare('H-not-zero')
182         blkhashn = hash2int(blkhash)
183         
184         global networkTarget
185         logfunc = getattr(checkShare.logger, 'info' if blkhashn <= networkTarget else 'debug')
186         logfunc('BLKHASH: %64x' % (blkhashn,))
187         logfunc(' TARGET: %64x' % (networkTarget,))
188         
189         txlist = MRD[1].data
190         t = txlist[0]
191         t.setCoinbase(MRD[2])
192         t.assemble()
193         
194         if blkhashn <= networkTarget:
195                 logfunc("Submitting upstream")
196                 UpstreamBitcoind.submitBlock(data, txlist)
197                 share['upstreamResult'] = True
198                 MM.updateBlock(blkhash)
199         
200         # Gotwork hack...
201         if gotwork and blkhashn <= config.GotWorkTarget:
202                 try:
203                         coinbaseMrkl = t.data
204                         coinbaseMrkl += blkhash
205                         steps = MRD[1]._steps
206                         coinbaseMrkl += pack('B', len(steps) + 1)
207                         coinbaseMrkl += t.txid
208                         for step in steps:
209                                 coinbaseMrkl += step
210                         coinbaseMrkl += b"\0\0\0\0"
211                         info = {}
212                         info['hash'] = b2a_hex(blkhash).decode('ascii')
213                         info['header'] = b2a_hex(data).decode('ascii')
214                         info['coinbaseMrkl'] = b2a_hex(coinbaseMrkl).decode('ascii')
215                         thr = threading.Thread(target=submitGotwork, args=(info,))
216                         thr.daemon = True
217                         thr.start()
218                 except:
219                         checkShare.logger.warning('Failed to build gotwork request')
220         
221         logShare(share)
222 checkShare.logger = logging.getLogger('checkShare')
223
224 def receiveShare(share):
225         # TODO: username => userid
226         try:
227                 checkShare(share)
228         except RejectedShare as rej:
229                 share['rejectReason'] = str(rej)
230                 logShare(share)
231                 raise
232         # TODO
233
234 def newBlockNotification(signum, frame):
235         MM.updateMerkleTree()
236         # TODO: Force RESPOND TO LONGPOLLS?
237         pass
238
239 from signal import signal, SIGUSR1
240 signal(SIGUSR1, newBlockNotification)
241
242
243 from jsonrpcserver import JSONRPCServer
244 import interactivemode
245
246 if __name__ == "__main__":
247         server = JSONRPCServer(config.JSONRPCAddress)
248         if hasattr(config, 'SecretUser'):
249                 server.SecretUser = config.SecretUser
250         server.aux = MM.CoinbaseAux
251         server.getBlockHeader = getBlockHeader
252         server.receiveShare = receiveShare
253         server.RaiseRedFlags = RaiseRedFlags
254         server.serve_forever()