def connectDcrdata(self): if self.blockchain: self.blockchain.close() dcrdataPath = "https://explorer.dcrdata.org" if self.netParams == nets.mainnet else "https://testnet.dcrdata.org" if useLocalDcrdata: dcrdataPath = "http://localhost:7777" if self.netParams == nets.mainnet else "http://localhost:17778" self.blockchain = DcrdataBlockchain(DcrdataDBPath, self.netParams, dcrdataPath) baseEmitter = self.blockchain.dcrdata.emitter def emit(sig): if sig == WS_DONE and not doneEvent.is_set(): log.warning(f"lost pubsub connection detected: re-initializing in 5 seconds") time.sleep(5) try: self.blockchain.dcrdata.ps = None self.blockchain.subscribeAddresses(self.challenges.keys(), self.addrEvent) updateThread = threading.Thread(target=self.updateChallenges) updateThread.start() self.addThread(updateThread) except Exception as e: print("failed dcrdata reconnect:", e) return baseEmitter(sig) self.blockchain.dcrdata.emitter = emit self.blockchain.subscribeBlocks(self.processBlock)
def test_broadcast(self, http_get_post): preload_api_list(http_get_post) http_get_post(f"{API_URL}/block/best", dict(height=1)) ddb = DcrdataBlockchain(":memory:", testnet, BASE_URL) with pytest.raises(KeyError): ddb.broadcast("some_addr") http_get_post((f"{INSIGHT_URL}/tx/send", "{'rawtx': 'some_addr'}"), None) assert ddb.broadcast("some_addr")
def test_addrsHaveTxs(self, http_get_post): preload_api_list(http_get_post) http_get_post(f"{API_URL}/block/best", dict(height=1)) addr = "someaddr" res = dict(items=[1]) http_get_post(f"{INSIGHT_URL}/addrs/{addr}/txs?from=0&to=1", res) ddb = DcrdataBlockchain(":memory:", testnet, BASE_URL) assert ddb.addrsHaveTxs([addr]) res["items"] = [] http_get_post(f"{INSIGHT_URL}/addrs/{addr}/txs?from=0&to=1", res) assert not ddb.addrsHaveTxs([addr])
def test_changeServer(self, http_get_post, monkeypatch, MockWebSocketClient): monkeypatch.setattr(ws, "Client", MockWebSocketClient) preload_api_list(http_get_post) http_get_post(f"{API_URL}/block/best", dict(height=1)) ddb = DcrdataBlockchain(":memory:", testnet, BASE_URL) ddb.subscribeBlocks(lambda sig: True) ddb.subscribeAddresses(["addr_1", "addr_2"], lambda a, tx: True) preload_api_list(http_get_post, baseURL="https://thisurl.org/api") http_get_post(f"https://thisurl.org/api/block/best", dict(height=1)) ddb.changeServer("https://thisurl.org/") assert ddb.dcrdata.baseURL == "https://thisurl.org/" preload_api_list(http_get_post, baseURL="https://thisurl.org/api") with pytest.raises(DecredError): ddb.changeServer("https://thisurl.org/")
def test_misc(self, http_get_post): preload_api_list(http_get_post) http_get_post(f"{API_URL}/block/best", dict(height=1)) ddb = DcrdataBlockchain(":memory:", testnet, BASE_URL) assert ddb.tipHeight == 1 # getAgendasInfo http_get_post(f"{API_URL}/stake/vote/info", AGENDAS_INFO_RAW) agsinfo = ddb.getAgendasInfo() assert isinstance(agsinfo, agenda.AgendasInfo) # ticketPoolInfo http_get_post(f"{API_URL}/stake/pool", self.stakePool) assert ddb.ticketPoolInfo().height == self.stakePool["height"] # nextStakeDiff http_get_post(f"{API_URL}/stake/diff", {"estimates": {"expected": 1}}) assert ddb.nextStakeDiff() == 1e8
def __init__(self, walletDir, pw, network, signals=None, allowCreate=False): """ Args: dir (str): A directory for wallet database files. pw (str): The user password. network (str): The network name. signals (Signals): A signal processor. """ signals = signals if signals else DefaultSignals netParams = nets.parse(network) netDir, dbPath, dcrPath = paths(walletDir, netParams.Name) if not Path(netDir).exists(): mkdir(netDir) dcrdataDB = database.KeyValueDatabase(dcrPath) # The initialized DcrdataBlockchain will not be connected, as that is a # blocking operation. It will be called when the wallet is open. dcrdataURL = DCRDATA_PATHS[netParams.Name] self.dcrdata = DcrdataBlockchain(dcrdataDB, netParams, dcrdataURL) chains.registerChain("dcr", self.dcrdata) walletExists = Path(dbPath).is_file() if not walletExists and not allowCreate: raise DecredError("Wallet does not exist at %s", dbPath) super().__init__(dbPath) # words is only set the first time a wallet is created. if not walletExists: seed = rando.newKeyRaw() self.initialize(seed, pw.encode(), netParams) self.words = mnemonic.encode(seed) cryptoKey = self.cryptoKey(pw) acctMgr = self.accountManager(chains.BipIDs.decred, signals) self.account = acctMgr.openAccount(0, cryptoKey) self.account.sync()
def test_for_tx(self, http_get_post): preload_api_list(http_get_post) http_get_post(f"{API_URL}/block/best", dict(height=1)) ddb = DcrdataBlockchain(":memory:", testnet, BASE_URL) # tinyBlockForTx # Preload the broken decoded tx. txURL = f"{API_URL}/tx/{self.txs[2][0]}" decodedTx = {"block": {}} http_get_post(txURL, decodedTx) assert ddb.tinyBlockForTx(self.txs[2][0]) is None # Preload the right decoded tx. txURL = f"{API_URL}/tx/{self.txs[2][0]}" decodedTx = {"block": {"blockhash": self.blockHash}} http_get_post(txURL, decodedTx) # Preload the block header. headerURL = f"{API_URL}/block/hash/{self.blockHash}/header/raw" http_get_post(headerURL, self.blockHeader) assert ddb.tinyBlockForTx(self.txs[2][0]).hash == reversed( ByteArray(self.blockHash)) # ticketForTx # Preload the non-ticket tx. txURL = f"{API_URL}/tx/hex/{self.txs[2][0]}" http_get_post(txURL, self.txs[2][1]) with pytest.raises(DecredError): ddb.ticketForTx(self.txs[2][0], nets.mainnet) # Preload the ticket decoded tx. blockHash = self.tinfo["purchase_block"]["hash"] txURL = f"{API_URL}/tx/{self.txs[1][0]}" decodedTx = {"block": {"blockhash": blockHash}} http_get_post(txURL, decodedTx) # Preload tx and tinfo. txURL = f"{API_URL}/tx/hex/{self.txs[1][0]}" http_get_post(txURL, self.txs[1][1]) tinfoURL = f"{API_URL}/tx/{self.utxos[1]['txid']}/tinfo" http_get_post(tinfoURL, self.tinfo) # Preload the block header. headerURL = f"{API_URL}/block/hash/{blockHash}/header/raw" http_get_post(headerURL, self.blockHeader) assert ddb.ticketForTx(self.txs[1][0], nets.mainnet).txid == self.txs[1][0] # ticketInfoForSpendingTx # Preload the txs. for txid, tx in self.txs: txURL = f"{API_URL}/tx/hex/{txid}" http_get_post(txURL, tx) txURL = f"{API_URL}/tx/{self.txs[3][0]}" blockHash = "00000000000000002847702f35b9227d27191d1858a7eccb94858c8f58f1066b" decodedTx = {"block": {"blockhash": blockHash}} http_get_post(txURL, decodedTx) blockHeader = { "hex": ("0700000037ef9650679e18f27a50f7395999b799917d567f7bcab60900000" "000000000008f4526c6c52f88a4b78177da200fe59e73ffd43d5d48cfecca" "63cbcdd6d5fbf8f8139bc051bd24ceecb4a5d569ee1c2c98ec74e52314cb0" "45fb3aea70d998de601006eb074d99aa405000200c2a4000038d93118b341" "394b030000001184060012070100d9a4565e25e9ec1324cbad03e593b65e3" "b1e0002000000000000000000000000000000000000000007000000"), } headerURL = f"{API_URL}/block/hash/{blockHash}/header/raw" http_get_post(headerURL, blockHeader) assert (ddb.ticketInfoForSpendingTx( self.txs[2][0], nets.mainnet).maturityHeight == self.blockHeight - 1)
def test_blocks(self, http_get_post): preload_api_list(http_get_post) http_get_post(f"{API_URL}/block/best", dict(height=1)) ddb = DcrdataBlockchain(":memory:", testnet, BASE_URL) # blockHeader with pytest.raises(DecredError): ddb.blockHeader(self.blockHash) # blockHeaderByHeight with pytest.raises(DecredError): ddb.blockHeaderByHeight(self.blockHeight).id() # Preload the block header. headerURL = f"{API_URL}/block/{self.blockHeight}/header/raw" http_get_post(headerURL, self.blockHeader) assert ddb.blockHeaderByHeight(self.blockHeight).id() == self.blockHash # Exercise the database code. assert ddb.blockHeaderByHeight(self.blockHeight).id() == self.blockHash # blockForTx # Preload the first broken decoded tx. txURL = f"{API_URL}/tx/{self.txs[2][0]}" decodedTx = {"block": {}} http_get_post(txURL, decodedTx) assert ddb.blockForTx(self.txs[2][0]) is None # Preload the second broken decoded tx. txURL = f"{API_URL}/tx/{self.txs[2][0]}" decodedTx = {"block": {"blockhash": ""}} http_get_post(txURL, decodedTx) assert ddb.blockForTx(self.txs[2][0]) is None # Preload the right decoded tx. txURL = f"{API_URL}/tx/{self.txs[2][0]}" decodedTx = {"block": {"blockhash": self.blockHash}} http_get_post(txURL, decodedTx) assert ddb.blockForTx(self.txs[2][0]).height == self.blockHeight # Preload the block header. headerURL = f"{API_URL}/block/hash/{self.blockHash}/header/raw" http_get_post(headerURL, self.blockHeader) assert ddb.blockForTx(self.txs[2][0]).hash() == reversed( ByteArray(self.blockHash)) # Exercise the database code. assert ddb.blockForTx(self.txs[2][0]).hash() == reversed( ByteArray(self.blockHash))
def test_utxos(self, http_get_post): preload_api_list(http_get_post) http_get_post(f"{API_URL}/block/best", dict(height=1)) ddb = DcrdataBlockchain(":memory:", testnet, BASE_URL) # txVout error with pytest.raises(DecredError): ddb.txVout(self.txs[2][0], 0).satoshis # processNewUTXO # Preload tx and tinfo. txURL = f"{API_URL}/tx/hex/{self.txs[1][0]}" http_get_post(txURL, self.txs[1][1]) tinfoURL = f"{API_URL}/tx/{self.utxos[1]['txid']}/tinfo" http_get_post(tinfoURL, self.tinfo) utxo = ddb.processNewUTXO(self.utxos[1]) assert utxo.tinfo.purchaseBlock.hash == reversed( ByteArray(self.tinfo["purchase_block"]["hash"])) # UTXOs assert len(ddb.UTXOs([])) == 0 # Precompute the UTXO data. addrs = [utxo["address"] for utxo in self.utxos] addrStr = ",".join(addrs) utxoURL = f"{INSIGHT_URL}/addr/{addrStr}/utxo" # Preload the UTXOs but not the txs. http_get_post(utxoURL, self.utxos) with pytest.raises(DecredError): ddb.UTXOs(addrs) # Preload both the UTXOs and the txs. http_get_post(utxoURL, self.utxos) for txid, tx in self.txs: txURL = f"{API_URL}/tx/hex/{txid}" http_get_post(txURL, tx) assert len(ddb.UTXOs(addrs)) == 3 # txidsForAddr txsURL = f"{INSIGHT_URL}/addr/the_address" # No transactions for an address. http_get_post(txsURL, {}) assert ddb.txidsForAddr("the_address") == [] # Some transactions for an address. http_get_post(txsURL, {"transactions": ["tx1"]}) assert ddb.txidsForAddr("the_address") == ["tx1"] # txVout success assert ddb.txVout(self.txs[2][0], 0).satoshis == 14773017964 # approveUTXO utxo = account.UTXO.parse(self.utxos[1]) utxo.maturity = 2 assert ddb.approveUTXO(utxo) is False utxo.maturity = None assert ddb.approveUTXO(utxo) is False utxo = account.UTXO.parse(self.utxos[0]) assert ddb.approveUTXO(utxo) is True # confirmUTXO # No confirmation. utxo = account.UTXO.parse(self.utxos[2]) assert ddb.confirmUTXO(utxo) is False # Confirmation. txURL = f"{API_URL}/tx/{self.txs[2][0]}" decodedTx = {"block": {"blockhash": self.blockHash}} http_get_post(txURL, decodedTx) headerURL = f"{API_URL}/block/hash/{self.blockHash}/header/raw" http_get_post(headerURL, self.blockHeader) assert ddb.confirmUTXO(utxo) is True
def test_subscriptions(self, http_get_post, tweakedDcrdataClient): # Exception in updateTip. preload_api_list(http_get_post) with pytest.raises(DecredError): DcrdataBlockchain(":memory:", testnet, BASE_URL) # Successful creation. preload_api_list(http_get_post) http_get_post(f"{API_URL}/block/best", dict(height=1)) ddb = DcrdataBlockchain(":memory:", testnet, BASE_URL, skipConnect=True) ddb.dcrdata = tweakedDcrdataClient() dcrdata._subcounter = 0 # Receiver block_queue = [] def blockReceiver(sig): block_queue.append(sig) # Receiver addr_queue = [] def addrReceiver(addr, txid): addr_queue.append((addr, txid)) # subscribeBlocks ddb.subscribeBlocks(blockReceiver) assert ddb.dcrdata.ps.sent[0] == ( '{"event": "subscribe", ' '"message": {"request_id": 1, "message": "newblock"}}') # subscribeAddresses ddb.subscribeAddresses(["new_one"], addrReceiver) assert ddb.dcrdata.ps.sent[1] == ( '{"event": "subscribe", ' '"message": {"request_id": 1, "message": "address:new_one"}}') # pubsubSignal assert ddb.pubsubSignal(dcrdata.WS_DONE) is None assert ddb.pubsubSignal(dict(event="subscribeResp")) is None assert ddb.pubsubSignal(dict(event="ping")) is None assert ddb.pubsubSignal(dict(event="unknown")) is None # pubsubSignal address sig = dict( event="address", message=dict(address="the_address", transaction="transaction"), ) assert ddb.pubsubSignal(sig) is None ddb.subscribeAddresses(["the_address"], addrReceiver) ddb.pubsubSignal(sig) assert addr_queue[0] == ("the_address", "transaction") # pubsubSignal newblock sig = dict(event="newblock", message=dict(block=dict(height=1))) ddb.pubsubSignal(sig) assert block_queue[0] == sig
class ChallengeManager: def __init__(self, netParams): log.info("spawning a new ChallengeManager") self.netParams = netParams self.challenges = {} self.threads = [] self.blockchain = None self.requestHandlers = { "index": self.handleIndex, "liveChallengeList": self.handleLiveChallengeList, "contract": self.handleContract, "challenge": self.handleChallenge, "solve": self.handleSolve, "relay": self.handleRelay, "challenges": self.handleChallenges, "flag": self.handleFlag, } self.twitter_ = None self.cxnPool = tcp.ConnectionPool(PG.dsn(netParams), constructor=DBConn) self.db(self.createTables_) ClientHandler.mgr = self os.unlink(SockAddr) self.server = ChallengeServer(SockAddr, ClientHandler) self.serverThread = threading.Thread(None, self.server.serve_forever) self.serverThread.start() serverBound.wait() self.redis = redis.Redis(host='localhost', port=6379) self.publishQueue = Queue() self.publishThread = threading.Thread(None, self.publishLoop) self.publishThread.start() self.init() def addThread(self, thread): self.threads.append(thread) ts = [t for t in self.threads if t.is_alive()] self.threads = ts def init(self): self.challenges.clear() self.loadFundedChallenges() self.loadUnfundedChallenges() self.connectDcrdata() log.info(f"subscribing to {len(self.challenges)} challenge addresses") self.blockchain.subscribeAddresses(self.challenges.keys(), self.addrEvent) updateThread = threading.Thread(target=self.updateChallenges) updateThread.start() self.addThread(updateThread) def connectDcrdata(self): if self.blockchain: self.blockchain.close() dcrdataPath = "https://explorer.dcrdata.org" if self.netParams == nets.mainnet else "https://testnet.dcrdata.org" if useLocalDcrdata: dcrdataPath = "http://localhost:7777" if self.netParams == nets.mainnet else "http://localhost:17778" self.blockchain = DcrdataBlockchain(DcrdataDBPath, self.netParams, dcrdataPath) baseEmitter = self.blockchain.dcrdata.emitter def emit(sig): if sig == WS_DONE and not doneEvent.is_set(): log.warning(f"lost pubsub connection detected: re-initializing in 5 seconds") time.sleep(5) try: self.blockchain.dcrdata.ps = None self.blockchain.subscribeAddresses(self.challenges.keys(), self.addrEvent) updateThread = threading.Thread(target=self.updateChallenges) updateThread.start() self.addThread(updateThread) except Exception as e: print("failed dcrdata reconnect:", e) return baseEmitter(sig) self.blockchain.dcrdata.emitter = emit self.blockchain.subscribeBlocks(self.processBlock) def publishLoop(self): while True: msg = self.publishQueue.get() if msg == b'': log.debug("quitting publish loop") return self.redis.publish(FEED_CHANNEL, json.dumps(msg)) def createTables_(self, cursor): cursor.execute(CreateChallenges) cursor.execute(CreateFunds) cursor.execute(CreateFlags) def close(self): doneEvent.set() self.publishQueue.put(b'') if self.cxnPool: self.cxnPool.close() self.cxnPool = None if self.server: self.server.server_close() self.server.shutdown() self.serverThread.join() self.server = None if self.publishThread: self.publishThread.join() if self.blockchain: self.blockchain.close() def db(self, f, *a): # When used as a context manager, a psycopg2.connection will # auto-commit. with self.cxnPool.conn() as conn: with conn.cursor() as cursor: return f(cursor, *a) def processBlock(self, sig): block = sig["message"]["block"] blockHeight = block["height"] log.info(f"block received at height {blockHeight}") self.publishQueue.put({ "event": "block", "blockHeight": blockHeight, }) self.updateRedeemTimes() self.refreshScoreIndex() def refreshScoreIndex(self): for ch in self.challenges.values(): ch.calcScore() self.scoreIndex = sorted([ch for ch in self.challenges.values() if ch.displayable()], key=lambda ch: -ch.score) def updateRedeemTimes(self): # Get redemptions that don't have a redemption time. funding = self.db(self.selectMempoolFunds_) count, found = 0, 0 # for addr, txHash, vout, redemption in funding: for funds in funding: count += 1 txid = funds.redemption.rhex() header = self.blockchain.blockForTx(txid) if not header: continue found += 1 redeemTime = header.timestamp self.db(self.updateRedeemTime_, funds.txHash.bytes(), funds.vout, redeemTime) ch = self.challenges.get(funds.addr) if not ch: continue fundID = funds.txHash + funds.vout funds = ch.funds.get(fundID) if not funds: log.error(f"no funds found for addr = {funds.addr}, txid = {reversed(funds.txHash)}, vout = {funds.vout}") continue funds.redeemTime = redeemTime if count > 0: log.info(f"updated {found} of {count} redemption block times") def addrEvent(self, addr, txid): ch = self.challenges.get(addr) if not ch: log.error("received address event for unknown challenge %s", addr) return log.info(f"address event received for addr = {addr}, txid = {txid}") tx = self.blockchain.tx(txid) self.processTransactionIO(addr, ch, tx) self.refreshScoreIndex() self.publishQueue.put({ "event": "addr", "addr": ch.addr, "funds": ch.totalFunds(), "fmtVal": ch.fmtVal(), }) if len(ch.funds) == 0: del self.challenges[addr] def updateChallenges(self): # Get a dict of all current funds. log.info("updating challenges") fundsTracker = {} for ch in self.challenges.values(): for funds in ch.funds.values(): fundsTracker[funds.id] = funds utxos = self.blockchain.UTXOs(list(self.challenges.keys())) log.info(f"processing {len(utxos)} utxos") new = 0 for utxo in utxos: if doneEvent.is_set(): return challenge = self.challenges.get(utxo.address) if not challenge: log.error(f"received utxo for unknown address {utxo.address}") continue outputID = utxo.txHash + utxo.vout fundsTracker.pop(outputID, None) # If we already know about these funds, there's nothing left to do. if outputID in challenge.funds: continue new += 1 # This is new funding. store it in the database. funds = FundingOutput(utxo.address, utxo.txHash, utxo.vout, utxo.satoshis, utxo.ts) challenge.addFunds(funds) self.db(self.insertFunds_, funds) log.info(f"found {new} new funding utxos") # If there are funds remaining in the fundsTracker, they've been spent # and we need to locate the spends. # 1. Reduce the fundsTracker to a set of addresses. updateAddrs = set() for funds in fundsTracker.values(): if funds.redemption: continue updateAddrs.add(funds.addr) log.info(f"looking for redemptions for {len(updateAddrs)} challenges") # 2. Get the transaction inputs that spend the address's outputs. updates = set() for addr in updateAddrs: ch = self.challenges[addr] for txid in self.blockchain.txidsForAddr(addr): if doneEvent.is_set(): return tx = self.blockchain.tx(txid) # Presumably, we don't need to look for outputs, because if they # aren't spent, we've already added them with the utxos loop # above. So just look for spends of the funding outputs we know # about. self.processTransactionInputs(ch, tx) if len(ch.funds) == 0: updates.add(funds) # 3. Check if any of the updated challenges can be deleted from the # challenges cache. for ch in updates: if len(ch.funds) == 0: del self.challenges[ch.addr] self.refreshScoreIndex() log.info("done updating challenges") def processTransactionInputs(self, ch, tx): for vin, txIn in enumerate(tx.txIn): pt = txIn.previousOutPoint fundsID = pt.hash + pt.index funds = ch.funds.get(fundsID) if not funds: continue # Attempt to get the block time for the redemption. log.info(f"redemption found for challenge {funds.addr} worth {funds.value/1e8:8f}DCR") funds.redemption = tx.cachedHash() header = self.blockchain.blockForTx(tx.id()) if header: funds.redeemTime = header.timestamp self.db(self.updateRedeem_, funds, tx.cachedHash(), vin, funds.redeemTime) def processTransactionIO(self, addr, ch, tx): self.processTransactionInputs(ch, tx) pkScript = txscript.payToAddrScript(decodeAddress(addr, self.netParams)) found = 0 for vout, txOut in enumerate(tx.txOut): if txOut.pkScript == pkScript: funds = FundingOutput(addr, tx.cachedHash(), vout, txOut.value, int(time.time())) if funds.id in ch.funds: continue found += 1 ch.addFunds(funds) self.db(self.insertFunds_, funds) log.info(f"found {found} outputs that pay to challenge {addr}") def updateRedeem_(self, cursor, funds, redeemTxHash, vin, redeemTime): cursor.execute(UpdateRedemption, (redeemTxHash.bytes(), vin, redeemTime, funds.txHash.bytes(), funds.vout)) def insertChallenge_(self, cursor, addr, doubleHash, nonce, proof, signingKey, prompt, registerTime, imgPath): cursor.execute(InsertChallenge, (addr, doubleHash, nonce, proof, signingKey, prompt, registerTime, imgPath)) def insertFunds_(self, cursor, funds): cursor.execute(InsertFunds, (funds.addr, funds.txHash.bytes(), funds.vout, funds.value, funds.txTime)) def addNewChallenge(self, ch, proof, signingKey): self.db(self.insertChallenge_, ch.addr, ch.doubleHash.bytes(), ch.nonce.bytes(), proof.bytes(), signingKey.bytes(), ch.prompt, ch.registerTime, ch.imgPath, ) log.debug(f"subscribing to address {ch.addr}") self.blockchain.subscribeAddresses([ch.addr], self.addrEvent) self.challenges[ch.addr] = ch def selectFunded_(self, cursor): cursor.execute(SelectFunded) funding = [] for addr, txHash, vout, value, txTime in cursor.fetchall(): funding.append(FundingOutput(addr, ByteArray(bytes(txHash)), vout, value, txTime)) return funding def selectUnfunded_(self, cursor, oldest): cursor.execute(SelectUnfunded, (oldest,)) chs = [] for row in cursor.fetchall(): chs.append(Challenge.fromDBRow(row)) return chs def selectChallenge_(self, cursor, addr): cursor.execute(SelectChallenge, (addr,)) return Challenge.fromDBRow(cursor.fetchone()) def selectKeyByProof_(self, cursor, addr, proof): cursor.execute(SelectKeyByProof, (addr, proof)) return cursor.fetchone() def insertFlag_(self, cursor, addr, reason, stamp): cursor.execute(InsertFlag, (addr, reason, stamp)) cursor.execute(FlagChallenge, (addr,)) def updateRedeemTime_(self, cursor, txHash, vout, redeemTime): cursor.execute(UpdateRedeemTime, (redeemTime, txHash, vout)) def selectMempoolFunds_(self, cursor): cursor.execute(SelectMempoolFunds) funding = [] for addr, txHash, vout, value, txTime, redemption, vin, redeemTime in cursor.fetchall(): funding.append(FundingOutput( addr, ByteArray(bytes(txHash)), vout, value, txTime, ByteArray(bytes(redemption)) if redemption else None, vin, redeemTime, )) return funding def challenge(self, addr): ch = self.challenges.get(addr) if ch: return ch return self.db(self.selectChallenge_, addr) def submitProof(self, addr, proof, redemptionAddr): addrStr = addr.string() signingKeyB, doubleHashB = self.db(self.selectKeyByProof_, addrStr, proof.b) signingKey = crypto.privKeyFromBytes(ByteArray(bytes(signingKeyB))) # Prepare the redemption script redeemScript = ByteArray(opcode.OP_SHA256) redeemScript += txscript.addData(bytes(doubleHashB)) redeemScript += opcode.OP_EQUALVERIFY redeemScript += txscript.addData(signingKey.pub.serializeCompressed()) redeemScript += opcode.OP_CHECKSIG # Collect all outputs for the address. We could do this from the cache, # but we'll generate a new call to dcrdata instead to make sure we have # the freshest data. utxos = self.blockchain.UTXOs([addrStr]) if len(utxos) == 0: return dict( error="challenge is either unfunded or has already been redeemed", code=1, ) rewardTx = msgtx.MsgTx.new() reward = 0 for utxo in utxos: reward += utxo.satoshis prevOut = msgtx.OutPoint(utxo.txHash, utxo.vout, msgtx.TxTreeRegular) rewardTx.addTxIn(msgtx.TxIn(prevOut, valueIn=utxo.satoshis)) # sigScript = txscript.addData(answerHash) + txscript.addData(script) # rewardTx.addTxIn(msgtx.TxIn(prevOut, signatureScript=sigScript)) # Add the reward output with zero value for now. txout = msgtx.TxOut(pkScript=txscript.payToAddrScript(redemptionAddr)) rewardTx.addTxOut(txout) # Get the serialized size of the transaction. Since there are no signatures # involved, the size is known exactly. Use the size to calculate transaction # fees. # 1 + 73 = signature # 1 + 32 = answer hash # 1 + 70 = redeem script sigScriptSize = 1 + 73 + 1 + 32 + 1 + 70 maxSize = rewardTx.serializeSize() + len(utxos)*sigScriptSize fees = feeRate * maxSize if reward <= fees: return makeErr(f"reward, {reward}, must cover fees {fees}, tx size = {maxSize} bytes, fee rate = {feeRate} atoms/byte") netReward = reward - fees # Set the value on the reward output. txout.value = netReward for idx, txIn in enumerate(rewardTx.txIn): sig = txscript.rawTxInSignature(rewardTx, idx, redeemScript, txscript.SigHashAll, signingKey.key) sigScript = txscript.addData(sig) + txscript.addData(DummyHash) + txscript.addData(redeemScript) txIn.signatureScript = sigScript return { "txHex": rewardTx.serialize().hex(), } def handleContract(self, payload): # Build the script. The first opcode says to hash the input in-place on the # stack. prompt = payload.get("prompt") doubleHash = ByteArray(payload.get("doubleHash")) nonce = ByteArray(payload.get("nonce")) proof = ByteArray(payload.get("proof")) imgPath = payload.get("imgPath") redeemScript = ByteArray(opcode.OP_SHA256) # Add the doubleHash to the stack. redeemScript += txscript.addData(doubleHash) # Start with OP_EQUALVERIFY because we don't want to leave a TRUE/FALSE on the # stack, we just want to fail if the answer is wrong. redeemScript += opcode.OP_EQUALVERIFY # We need to generate a key pair for the game key. priv = generateKey() # The "Game Key". # The rest of the script is like a p2pk. redeemScript += txscript.addData(priv.pub.serializeCompressed()) redeemScript += opcode.OP_CHECKSIG gameKey = scriptVersion(self.netParams) + doubleHash + priv.key gameKeyEnc = b58encode(gameKey.bytes()).decode() # Create the address. p2shAddr = AddressScriptHash.fromScript(redeemScript, self.netParams) addr = p2shAddr.string() ch = Challenge(addr, doubleHash, nonce, prompt, int(time.time()), imgPath) self.addNewChallenge(ch, proof, priv.key) log.info(f"new challenge! address = {addr}, with_image = {bool(imgPath)}, prompt = {prompt}") return { "address": addr, "gameKey": gameKeyEnc, } def handleChallenge(self, payload): chid = payload.get("chid") ch = self.challenge(chid) if not ch: return makeErr(f"unknown challenge address: {chid}") return ch.jsondict() def handleSolve(self, payload): addrStr = payload["addr"] proof = ByteArray(payload["proof"]) redemptionAddrStr = payload["redemptionAddr"] addr = decodeAddress(addrStr, self.netParams) if not isinstance(addr, AddressScriptHash): return makeErr("unsupported address type "+str(type(addr))) redemptionAddr = decodeAddress(redemptionAddrStr, self.netParams) return self.submitProof(addr, proof, redemptionAddr) def handleRelay(self, payload): # Looking for exception tx = msgtx.MsgTx.deserialize(ByteArray(payload["txHex"])) self.blockchain.broadcast(payload["txHex"]) return tx.id() def handleChallenges(self, payload): chs = [] for addr in payload["challenges"]: ch = self.challenge(addr) chs.append({ "addr": ch.addr, "fmtVal": ch.fmtVal(), "truncatedPrompt": ch.truncatedPrompt(), "imgPath": ch.imgPath, "registerTime": ch.registerTime, }) return chs def handleLiveChallengeList(self, payload): return [ch.addr for ch in self.scoreIndex] def handleFlag(self, payload): # raise an exception if this isn't a valid address addr = payload["addr"] reason = payload["reason"] if "reason" in payload else "" # decodeAddress(addr, self.netParams) ch = self.challenge(addr) if not ch: return makeErr(f"cannot flag. unknown address {addr}") ch.flagged = True self.db(self.insertFlag_, addr, reason, int(time.time())) self.refreshScoreIndex() return True def loadFundedChallenges(self): rows = 0 for utxo in self.db(self.selectFunded_): rows += 1 ch = self.challenges.get(utxo.addr) if not ch: ch = self.db(self.selectChallenge_, utxo.addr) self.challenges[ch.addr] = ch ch.addFunds(utxo) log.info(f"{rows} unspent challenge funding utxos loaded") def loadUnfundedChallenges(self): maxAge = 60 * 60 * 24 * 7 # Go back one week. oldest = int(time.time()) - maxAge chs = self.db(self.selectUnfunded_, oldest) for ch in chs: self.challenges[ch.addr] = ch log.info(f"{len(chs)} recent unfunded challenges loaded") def handleRequest(self, req): route = req["route"] handler = self.requestHandlers.get(route, None) if not handler: return { "error": f"unknown request route {route}", } try: return handler(req.get("payload", {})) except Exception as e: log.error(f"exception encountered with request {req}: {e}") print(helpers.formatTraceback(e)) return { "error": "internal server error", } def handleIndex(self, payload): return {}
def __init__(self, qApp): """ Args: qApp (QApplication): An initialized QApplication. """ super().__init__() self.qApp = qApp self.cfg = config.load() self.log = self.initLogging() self.wallet = None # trackedCssItems are CSS-styled elements to be updated if dark mode is # enabled/disabled. self.trackedCssItems = [] st = self.sysTray = QtWidgets.QSystemTrayIcon(QtGui.QIcon(DCR.FAVICON)) self.contextMenu = ctxMenu = QtWidgets.QMenu() ctxMenu.addAction("minimize").triggered.connect(self.minimizeApp) ctxMenu.addAction("quit").triggered.connect(self.shutdown) st.setContextMenu(ctxMenu) st.activated.connect(self.sysTrayActivated) # The signalRegistry maps a signal to any number of receivers. Signals # are routed through a Qt Signal. self.signalRegistry = {} self.qRawSignal.connect(self.signal_) self.blockchainSignals = TinySignals( balance=self.balanceSync, working=lambda: self.emitSignal(ui.WORKING_SIGNAL), done=lambda: self.emitSignal(ui.DONE_SIGNAL), spentTickets=lambda: self.emitSignal(ui.SPENT_TICKETS_SIGNAL), ) self.netDirectory = os.path.join(config.DATA_DIR, self.cfg.netParams.Name) helpers.mkdir(self.netDirectory) self.appDB = database.KeyValueDatabase( os.path.join(self.netDirectory, "app.db") ) self.settings = self.appDB.child("settings") self.loadSettings() dcrdataDB = database.KeyValueDatabase(self.assetDirectory("dcr") / "dcrdata.db") # The initialized DcrdataBlockchain will not be connected, as that is a # blocking operation. It will be called when the wallet is open. self.dcrdata = DcrdataBlockchain( dcrdataDB, self.cfg.netParams, self.settings[DB.dcrdata].decode(), skipConnect=True, ) chains.registerChain("dcr", self.dcrdata) # appWindow is the main application window. The TinyDialog class has # methods for organizing a stack of Screen widgets. self.appWindow = screens.TinyDialog(self) self.homeSig.connect(self.home_) def gohome(screen=None): self.homeSig.emit(screen) self.home = gohome self.homeScreen = None self.pwDialog = screens.PasswordDialog() self.waitingScreen = screens.WaitingScreen() # Set waiting screen as initial home screen. self.appWindow.stack(self.waitingScreen) self.confirmScreen = screens.ConfirmScreen() self.walletSig.connect(self.setWallet_) def setwallet(wallet): self.walletSig.emit(wallet) self.setWallet = setwallet self.sysTray.show() self.appWindow.show() self.initialize()
class SimpleWallet(Wallet): """ SimpleWallet is a single-account Decred wallet. """ def __init__(self, walletDir, pw, network, signals=None, allowCreate=False): """ Args: dir (str): A directory for wallet database files. pw (str): The user password. network (str): The network name. signals (Signals): A signal processor. """ signals = signals if signals else DefaultSignals netParams = nets.parse(network) netDir, dbPath, dcrPath = paths(walletDir, netParams.Name) if not Path(netDir).exists(): mkdir(netDir) dcrdataDB = database.KeyValueDatabase(dcrPath) # The initialized DcrdataBlockchain will not be connected, as that is a # blocking operation. It will be called when the wallet is open. dcrdataURL = DCRDATA_PATHS[netParams.Name] self.dcrdata = DcrdataBlockchain(dcrdataDB, netParams, dcrdataURL) chains.registerChain("dcr", self.dcrdata) walletExists = Path(dbPath).is_file() if not walletExists and not allowCreate: raise DecredError("Wallet does not exist at %s", dbPath) super().__init__(dbPath) # words is only set the first time a wallet is created. if not walletExists: seed = rando.newKeyRaw() self.initialize(seed, pw.encode(), netParams) self.words = mnemonic.encode(seed) cryptoKey = self.cryptoKey(pw) acctMgr = self.accountManager(chains.BipIDs.decred, signals) self.account = acctMgr.openAccount(0, cryptoKey) self.account.sync() def __getattr__(self, name): """Delegate unknown methods to the account.""" return getattr(self.account, name) @staticmethod def create(walletDir, pw, network, signals=None): """ Create a new wallet. Will not overwrite an existing wallet file. All arguments are the same as the SimpleWallet constructor. """ netParams = nets.parse(network) _, dbPath, _ = paths(walletDir, netParams.Name) if Path(dbPath).is_file(): raise DecredError("wallet already exists at %s" % dbPath) wallet = SimpleWallet(walletDir, pw, network, signals, True) words = wallet.words wallet.words.clear() return wallet, words def close(self): self.dcrdata.close()