Merge branch 'bugfix_race_coinbase' into bugfix_race_coinbase_2
[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 copy import deepcopy
91 from struct import pack, unpack
92 from time import time
93 from util import RejectedShare, dblsha, hash2int
94 import jsonrpc
95
96 gotwork = None
97 if hasattr(config, 'GotWorkURI'):
98         gotwork = jsonrpc.ServiceProxy(config.GotWorkURI)
99
100 db = None
101 if hasattr(config, 'DbOptions'):
102         import psycopg2
103         db = psycopg2.connect(**config.DbOptions)
104
105 def getBlockHeader(username):
106         MRD = MM.getMRD()
107         (merkleRoot, merkleTree, coinbase, prevBlock, bits, rollPrevBlk) = MRD
108         timestamp = pack('<L', int(time()))
109         hdr = b'\1\0\0\0' + prevBlock + merkleRoot + timestamp + bits + b'iolE'
110         workLog.setdefault(username, {})[merkleRoot] = MRD
111         return hdr
112
113 def YN(b):
114         if b is None:
115                 return None
116         return 'Y' if b else 'N'
117
118 def logShare(share):
119         if db is None:
120                 return
121         dbc = db.cursor()
122         rem_host = share.get('remoteHost', '?')
123         username = share['username']
124         reason = share.get('rejectReason', None)
125         upstreamResult = share.get('upstreamResult', None)
126         solution = share['_origdata']
127         #solution = b2a_hex(solution).decode('utf8')
128         stmt = "insert into shares (rem_host, username, our_result, upstream_result, reason, solution) values (%s, %s, %s, %s, %s, decode(%s, 'hex'))"
129         params = (rem_host, username, YN(not reason), YN(upstreamResult), reason, solution)
130         dbc.execute(stmt, params)
131         db.commit()
132
133 def checkShare(share):
134         data = share['data']
135         data = data[:80]
136         (prevBlock, bits) = MM.currentBlock
137         sharePrevBlock = data[4:36]
138         if sharePrevBlock != prevBlock:
139                 if sharePrevBlock == MM.lastBlock[0]:
140                         raise RejectedShare('stale-prevblk')
141                 raise RejectedShare('bad-prevblk')
142         
143         shareMerkleRoot = data[36:68]
144         # TODO: use userid
145         username = share['username']
146         if username not in workLog:
147                 raise RejectedShare('unknown-user')
148         MWL = workLog[username]
149         if shareMerkleRoot not in MWL:
150                 raise RejectedShare('unknown-work')
151         MRD = MWL[shareMerkleRoot]
152         share['MRD'] = MRD
153         
154         if data in DupeShareHACK:
155                 raise RejectedShare('duplicate')
156         DupeShareHACK[data] = None
157         
158         shareTimestamp = unpack('<L', data[68:72])[0]
159         shareTime = share['time'] = time()
160         if shareTimestamp < shareTime - 300:
161                 raise RejectedShare('time-too-old')
162         if shareTimestamp > shareTime + 7200:
163                 raise RejectedShare('time-too-new')
164         if data[72:76] != bits:
165                 raise RejectedShare('bad-diffbits')
166         if data[:4] != b'\1\0\0\0':
167                 raise RejectedShare('bad-version')
168         
169         blkhash = dblsha(data)
170         if blkhash[28:] != b'\0\0\0\0':
171                 raise RejectedShare('H-not-zero')
172         blkhashn = hash2int(blkhash)
173         
174         global networkTarget
175         logfunc = getattr(checkShare.logger, 'info' if blkhashn <= networkTarget else 'debug')
176         logfunc('BLKHASH: %64x' % (blkhashn,))
177         logfunc(' TARGET: %64x' % (networkTarget,))
178         
179         txlist = MRD[1].data
180         txlist = [deepcopy(txlist[0]),] + txlist[1:]
181         t = txlist[0]
182         t.setCoinbase(MRD[2])
183         t.assemble()
184         
185         if blkhashn <= networkTarget:
186                 logfunc("Submitting upstream")
187                 UpstreamBitcoind.submitBlock(data, txlist)
188                 share['upstreamResult'] = True
189                 MM.updateBlock(blkhash)
190         
191         # Gotwork hack...
192         if gotwork:
193                 try:
194                         coinbaseMrkl = t.data
195                         coinbaseMrkl += blkhash
196                         steps = MRD[1]._steps
197                         coinbaseMrkl += pack('B', len(steps) + 1)
198                         coinbaseMrkl += t.txid
199                         for step in steps:
200                                 coinbaseMrkl += step
201                         coinbaseMrkl += b"\0\0\0\0"
202                         info = {}
203                         info['hash'] = b2a_hex(blkhash).decode('ascii')
204                         info['header'] = b2a_hex(data).decode('ascii')
205                         info['coinbaseMrkl'] = b2a_hex(coinbaseMrkl).decode('ascii')
206                         gotwork.gotwork(info)
207                 except:
208                         checkShare.logger.warning('Failed to submit gotwork')
209         
210         logShare(share)
211 checkShare.logger = logging.getLogger('checkShare')
212
213 def receiveShare(share):
214         # TODO: username => userid
215         try:
216                 checkShare(share)
217         except RejectedShare as rej:
218                 share['rejectReason'] = str(rej)
219                 logShare(share)
220                 raise
221         # TODO
222
223 def newBlockNotification(signum, frame):
224         MM.updateMerkleTree()
225         # TODO: Force RESPOND TO LONGPOLLS?
226         pass
227
228 from signal import signal, SIGUSR1
229 signal(SIGUSR1, newBlockNotification)
230
231
232 from jsonrpcserver import JSONRPCServer
233 import interactivemode
234
235 if __name__ == "__main__":
236         server = JSONRPCServer(config.JSONRPCAddress)
237         if hasattr(config, 'SecretUser'):
238                 server.SecretUser = config.SecretUser
239         server.aux = MM.CoinbaseAux
240         server.getBlockHeader = getBlockHeader
241         server.receiveShare = receiveShare
242         server.RaiseRedFlags = RaiseRedFlags
243         server.serve_forever()