Ejemplo n.º 1
0
    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)
Ejemplo n.º 2
0
 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")
Ejemplo n.º 3
0
    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])
Ejemplo n.º 4
0
    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/")
Ejemplo n.º 5
0
    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
Ejemplo n.º 6
0
    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()
Ejemplo n.º 7
0
    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)
Ejemplo n.º 8
0
    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))
Ejemplo n.º 9
0
    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
Ejemplo n.º 10
0
    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
Ejemplo n.º 11
0
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 {}
Ejemplo n.º 12
0
    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()
Ejemplo n.º 13
0
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()