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