Beispiel #1
0
    def __init__(self, cfg: Config) -> None:
        self.l = DBLogger(self, cfg)
        self._conn = sqlite3.connect(cfg.peer_db_path())

        self._conn.execute(CREATE_TABLE_SQL)
        self._conn.commit()

        self.self_peer = Peer(cfg.server_peer_id(),
                              cfg.server_advertize_addr(),
                              cfg.server_listen_port())

        self.add_peer(self.self_peer)

        try:
            gateway_id = Peer.request_id(cfg.gateway_address(),
                                         cfg.gateway_port())

            self.gateway_peer = Peer(gateway_id, cfg.gateway_address(),
                                     cfg.gateway_port())

            self.add_peer(self.gateway_peer)
        except requests.exceptions.ConnectionError as e:
            self.l.warn("Couldn't connect to gateway", cfg.gateway_address(),
                        cfg.gateway_port())
            self.gateway_peer = None
Beispiel #2
0
class TransactionRequestHandler(web.RequestHandler):
    def initialize(self, cfg: Config, chain: BlockChain):
        self.l = DBLogger(self, cfg)
        self.chain = chain

    def get(self) -> None:
        txns = self.chain.transaction_storage.get_all_transactions()
        ser_txns = list(map(lambda t: t.serializable(), txns))
        resp = {"transactions": ser_txns}
        self.set_status(200)
        self.write(resp)

    def post(self) -> None:
        ser = self.request.body.decode('utf-8')
        txn = SignedTransaction.deserialize(ser)

        if self.chain.transaction_is_valid(txn):
            self.l.info("New transaction", txn)
            self.chain.add_outstanding_transaction(txn)
            self.set_status(200)
            self.write(util.generic_ok_response())
        else:
            self.l.warn("Invalid transaction", txn)
            self.set_status(400)
            self.write(util.error_response("Invalid transaction"))
Beispiel #3
0
    def __init__(self, cfg: Config, password: str) -> None:
        self.l = DBLogger(self, cfg)

        kdf = pwhash.argon2i.kdf
        salt = utils.random(pwhash.argon2i.SALTBYTES)
        ops = pwhash.argon2i.OPSLIMIT_SENSITIVE
        mem = pwhash.argon2i.MEMLIMIT_SENSITIVE

        self.l.info(
            "Deriving wallet encryption key, might take a couple seconds")
        self.key = kdf(secret.SecretBox.KEY_SIZE, password, salt,
                                 opslimit=ops, memlimit=mem)
        self.box = secret.SecretBox(Alices_key)

        self._uxto_storage = SqliteUXTOStorage(cfg)

        self._chain = BlockChain(
                SqliteBlockChainStorage(cfg),
                SqliteTransactionStorage(cfg),
                self._uxto_storage,
                cfg)

        self._conn = sqlite3.connect(cfg.wallet_path())
        self._conn.execute(CREATE_TABLE_SQL)
        self._conn.commit()
Beispiel #4
0
    def __init__(self, cfg: Config) -> None:
        self.l = DBLogger(self, cfg)
        self.peer_list = PeerList(cfg)

        self.storage = SqliteBlockChainStorage(cfg)
        self.transaction_storage = SqliteTransactionStorage(cfg)
        self.uxto_storage = SqliteUXTOStorage(cfg)
        self.chain = BlockChain(self.storage, self.transaction_storage,
                                self.uxto_storage, cfg)

        self.peer_info = Peer(cfg.server_peer_id(),
                              cfg.server_advertize_addr(),
                              cfg.server_listen_port())
        self.advertize_self = cfg.advertize_self()

        self.app = web.Application([
            web.url(r"/", DefaultRequestHandler),
            web.url(r"/blocks", BlockRequestHandler, {
                "chain": self.chain,
                "cfg": cfg
            }),
            web.url(r"/outstanding_transactions", TransactionRequestHandler, {
                "chain": self.chain,
                "cfg": cfg
            }),
            web.url(r"/peers", PeerRequestHandler, {
                "peer_list": self.peer_list,
                "cfg": cfg
            }),
            web.url(r"/chain", ChainRequestHandler, {
                "cfg": cfg,
                "chain": self.chain
            }),
        ])
Beispiel #5
0
    def __init__(self, cfg: Config) -> None:
        self.l = DBLogger(self, cfg)
        self.l.info("Init")
        self.peer_list = PeerList(cfg)
        self.self_peer = Peer(cfg.server_peer_id(),
                              cfg.server_advertize_addr(),
                              cfg.server_listen_port())

        self.chain = BlockChain(SqliteBlockChainStorage(cfg),
                                SqliteTransactionStorage(cfg),
                                SqliteUXTOStorage(cfg), cfg)
        self.cfg = cfg
Beispiel #6
0
    def __init__(self, storage: BlockChainStorage,
                 transaction_storage: TransactionStorage,
                 uxto_storage: UXTOStorage, cfg: Config) -> None:

        self.storage = storage
        self.transaction_storage = transaction_storage
        self.uxto_storage = uxto_storage
        self.l = DBLogger(self, cfg)

        genesis = storage.get_genesis()
        if genesis is None:
            self.l.info("Storage didn't have genesis. Added.")
            storage.add_block(HashedBlock.genesis())
Beispiel #7
0
def difficulty_adjustment(block_times: Iterator[Timestamp],
                          l: DBLogger) -> int:
    """Computes how much the difficulty should be adjusted by (up or down).

    Uses the difference of the log of the mean block time delta and the log of
    the target delta.

    If the mean block time is 2x the target, the adjustment should be -1.
    If the mean block time is 4x the target, the adjustment should be -2.
    If the mean block time ix 1/8th the target, the adjustment should be +3.
    """

    unix_times = list(map(lambda t: t.unix_millis, block_times))

    deltas = []
    prev = unix_times[0]
    for t in unix_times[1:]:
        deltas.append(t - prev)
        prev = t

    mean = sum(deltas) / len(deltas)
    l.debug("Segment mean ms:", mean)
    l.debug("Target ms:", BLOCK_TIME_TARGET)

    log_mean = math.log2(mean)
    log_target = math.log2(BLOCK_TIME_TARGET)
    log_difference = log_target - log_mean
    l.debug("Log difference:", log_difference)

    adjustment = int(round(log_target - log_mean))
    l.debug("Recommended adjustment:", adjustment)
    return adjustment
Beispiel #8
0
    def __init__(self, cfg: Config) -> None:
        self.l = DBLogger(self, cfg)
        self._conn = sqlite3.connect(cfg.chain_db_path())

        c = self._conn.cursor()
        c.execute(CREATE_TABLE_SQL)
        c.execute(CREATE_TXN_HASH_INDEX_SQL)
        self._conn.commit()
Beispiel #9
0
class ChainServer(object):
    def __init__(self, cfg: Config) -> None:
        self.l = DBLogger(self, cfg)
        self.peer_list = PeerList(cfg)

        self.storage = SqliteBlockChainStorage(cfg)
        self.transaction_storage = SqliteTransactionStorage(cfg)
        self.uxto_storage = SqliteUXTOStorage(cfg)
        self.chain = BlockChain(self.storage, self.transaction_storage,
                                self.uxto_storage, cfg)

        self.peer_info = Peer(cfg.server_peer_id(),
                              cfg.server_advertize_addr(),
                              cfg.server_listen_port())
        self.advertize_self = cfg.advertize_self()

        self.app = web.Application([
            web.url(r"/", DefaultRequestHandler),
            web.url(r"/blocks", BlockRequestHandler, {
                "chain": self.chain,
                "cfg": cfg
            }),
            web.url(r"/outstanding_transactions", TransactionRequestHandler, {
                "chain": self.chain,
                "cfg": cfg
            }),
            web.url(r"/peers", PeerRequestHandler, {
                "peer_list": self.peer_list,
                "cfg": cfg
            }),
            web.url(r"/chain", ChainRequestHandler, {
                "cfg": cfg,
                "chain": self.chain
            }),
        ])

    def listen(self) -> None:
        if self.advertize_self:
            self.l.info("Advertizing self as peer")
            self.peer_list.add_peer(self.peer_info)
        else:
            self.l.info("Not advertizing self as peer")

        self.app.listen(self.peer_info.port, address="0.0.0.0")
Beispiel #10
0
    def __init__(self,
                 cfg: Config,
                 key_pair: Optional[KeyPair] = None) -> None:
        self.l = DBLogger(self, cfg)
        self.l.info("Init")
        self.cfg = cfg

        self.client = ChainClient(cfg)

        if key_pair is None:
            self.key_pair = KeyPair.new()
        else:
            self.key_pair = key_pair

        self.storage = SqliteBlockChainStorage(cfg)
        self.transaction_storage = SqliteTransactionStorage(cfg)
        self.uxto_storage = SqliteUXTOStorage(cfg)
        self.chain = BlockChain(self.storage, self.transaction_storage,
                                self.uxto_storage, cfg)
Beispiel #11
0
 def __init__(self, cfg: Config) -> None:
     super().__init__()
     self.l = DBLogger(self, cfg)
     self._conn = sqlite3.connect(cfg.chain_db_path())
     with self._conn:
         cursor = self._conn.cursor()
         cursor.execute(CREATE_TABLE_SQL)
         cursor.execute(CREATE_HASH_INDEX_SQL)
         cursor.execute(CREATE_PARENT_HASH_INDEX_SQL)
         cursor.execute(CREATE_BLOCK_NUM_INDEX_SQL)
         cursor.execute(CREATE_HEAD_INDEX_SQL)
Beispiel #12
0
class PeerRequestHandler(web.RequestHandler):
    def initialize(self, peer_list: PeerList, cfg: Config) -> None:
        self.l = DBLogger(self, cfg)
        self.peer_list = peer_list
        self.cfg = cfg

    def get(self) -> None:
        peers = list(
            map(lambda p: p.serializable(),
                self.peer_list.get_all_active_peers()))
        resp = {
            "peers": peers,
            "peer_id": self.cfg.server_peer_id(),
        }
        self.set_status(200)
        self.write(resp)

    def post(self) -> None:
        peers = json.loads(self.request.body.decode('utf-8'))

        maybe_new_peers = list(
            map(lambda p: Peer(p["peer_id"], p["address"], p["port"]),
                peers["peers"]))
        new_peers: List[Peer] = []

        for peer in maybe_new_peers:
            if self.peer_list.has_peer(peer):
                self.l.info("Already have peer", peer)
                continue

            new_peers.append(peer)
            self.l.info("New peer", peer)
            self.peer_list.add_peer(peer)

        self.set_status(200)
        self.write(util.generic_ok_response())
Beispiel #13
0
class BlockChain(object):
    def __init__(self, storage: BlockChainStorage,
                 transaction_storage: TransactionStorage,
                 uxto_storage: UXTOStorage, cfg: Config) -> None:

        self.storage = storage
        self.transaction_storage = transaction_storage
        self.uxto_storage = uxto_storage
        self.l = DBLogger(self, cfg)

        genesis = storage.get_genesis()
        if genesis is None:
            self.l.info("Storage didn't have genesis. Added.")
            storage.add_block(HashedBlock.genesis())

    def get_difficulty(self, head: Optional[HashedBlock] = None) -> int:
        if head is None:
            h = self.get_head()
        else:
            h = head

        if (h.block_num() + 1) < TUNING_SEGMENT_LENGTH:
            return difficulty.DEFAULT_DIFFICULTY
        elif (h.block_num() + 1) % TUNING_SEGMENT_LENGTH == 0:
            diff = self.tuning_segment_difficulty(
                h.block.block_config.difficulty, h.block_num())
            self.l.info("Head ({}) is at end of segment, retune to {}".format(
                h.block_num(), diff))
            return diff
        else:
            return h.block.block_config.difficulty

    def get_head(self) -> HashedBlock:
        return self.storage.get_head()

    def add_block(self, block: HashedBlock) -> None:
        if self.storage.has_hash(block.mining_hash()):
            self.l.debug("Already have block", block)
            return
        elif self.block_is_valid(block):
            self.l.debug("Store block", block)
            self.storage.add_block(block)
            for txn in block.block.transactions:
                for out in txn.transaction.outputs:
                    self.l.debug("Add UXTO", txn.txn_hash(), out.output_id)
                    self.uxto_storage.add_output(txn.txn_hash(), out.to_addr,
                                                 out.output_id)

                for inp in txn.transaction.inputs:
                    out = self.get_transaction_output(
                        inp.output_block_hash, inp.output_transaction_hash,
                        inp.output_id)

                    self.l.debug("Claim UXTO", inp.output_transaction_hash,
                                 inp.output_id)

                    self.uxto_storage.mark_claimed(inp.output_transaction_hash,
                                                   inp.output_id)

            self._cleanup_outstanding_transactions(block)
            self._abandon_blocks()
        else:
            raise InvalidBlockError("Block is invalid")

    def add_outstanding_transaction(self, txn: SignedTransaction) -> None:
        if self.transaction_storage.has_transaction(txn.txn_hash()):
            self.l.debug("Already have txn", txn)
            return
        elif self.transaction_is_valid(txn):
            self.l.debug("Store transaction", txn)
            self.transaction_storage.add_transaction(txn)
        else:
            raise InvalidTransactionError("Transaction is invalid")

    @staticmethod
    def genesis_is_valid(block: HashedBlock, l: DBLogger) -> bool:
        return block.mining_hash() == HashedBlock.genesis().mining_hash()

    def block_is_valid(self, block: HashedBlock) -> bool:
        if self.storage.has_hash(block.parent_mining_hash()):
            parent = self.storage.get_by_hash(block.parent_mining_hash())
        else:
            self.l.warn("Parent with hash {} not known".format(
                block.parent_mining_hash().hex()))
            return False

        if self.block_should_be_abandoned(block):
            self.l.warn("Block should be abandoned", block)
            return False

        difficulty = self.get_difficulty(parent)
        if block.block.block_config.difficulty != difficulty:
            self.l.warn(
                "Unexpected difficulty {} for block {} ({}), expected {}".
                format(block.block.block_config.difficulty, block.block_num(),
                       block.mining_hash().hex(), difficulty))
            return False

        if not block.hash_meets_difficulty():
            self.l.warn("Block hash doesn't meet the set difficulty")
            return False

        if block.block_num() != parent.block_num() + 1:
            self.l.warn("Block number isn't parent+1")
            return False

        n_rewards = 0
        for transaction in block.block.transactions:
            if not self.transaction_is_valid(transaction):
                self.l.warn("Transaction is invalid")
                return False

            if transaction.is_reward():
                n_rewards += 1

        if n_rewards != 1:
            self.l.warn(
                "Invalid number of rewards ({}) in transaction w/ sig{}".
                format(n_rewards, transaction.signature))
            return False

        return True

    def transaction_is_valid(self, signed: SignedTransaction) -> bool:
        if signed.is_reward():
            return self.reward_is_valid(signed)

        if not signed.signature_is_valid():
            self.l.warn("Transaction signature is invalid (sig {})".format(
                signed.signature))
            return False

        output_sum = Amount(0)
        for output in signed.transaction.outputs:
            output_sum += output.amount

        claimed_prev_outputs: List[TransactionOutput] = []
        claimed_sum = Amount(0)
        for inp in signed.transaction.inputs:
            out = self.get_transaction_output(inp.output_block_hash,
                                              inp.output_transaction_hash,
                                              inp.output_id)

            if out is None:
                self.l.warn("Output was unknown", out)
                return False

            if self.uxto_storage.output_is_claimed(inp.output_transaction_hash,
                                                   inp.output_id):
                self.l.warn("Output already claimed", out)
                return False

            claimed_sum += out.amount

            if out.to_addr != signed.transaction.claimer:
                self.l.warn("Output {} can't be claimed by address {}".format(
                    out, signed.transaction.claimer))
                return False

        if output_sum != claimed_sum:
            self.l.warn("Input/output amount mismatch {} != {}".format(
                output_sum, claimed_sum))
            return False

        return True

    def reward_is_valid(self, reward: SignedTransaction) -> bool:
        if not reward.signature_is_valid():
            self.l.warn("Reward signature is invalid (sig {})".format(
                reward.signature))
            return False

        if len(reward.transaction.outputs) != 1:
            self.l.warn("Reward has n_outputs != 1", reward)
            return False

        if len(reward.transaction.inputs) != 0:
            self.l.warn("Reward has inputs", reward)
            return False

        if reward.transaction.outputs[0].amount != REWARD_AMOUNT:
            self.l.warn("Reward has invalid amout", reward)
            return False

        return True

    def get_transaction_output(self, block_hash: Hash, txn_hash: Hash,
                               output_id: int) -> Optional[TransactionOutput]:

        block = self.storage.get_by_hash(block_hash)

        if block is None:
            self.l.warn(
                "Don't have block hash {} when getting txn output".format(
                    block_hash.hex()))
            return None

        for signed in block.block.transactions:
            if signed.txn_hash() != txn_hash:
                continue

            for output in signed.transaction.outputs:
                if output.output_id == output_id:
                    return output

        self.l.warn("No output with ID", output_id)
        return None

    def tuning_segment_difficulty(self, current_difficulty: int,
                                  height: int) -> int:
        self.l.debug("Calculating tuning segment difficulty using height",
                     height)

        seg_stop = (
            (height // TUNING_SEGMENT_LENGTH) + 1) * TUNING_SEGMENT_LENGTH
        seg_start = seg_stop - TUNING_SEGMENT_LENGTH

        if seg_start == 0:
            self.l.debug("Omitting genesis block from tuning calculation")
            seg_start = 1

        self.l.debug("Getting segment [{}, {})".format(seg_start, seg_stop))

        segment = self.storage.get_range(seg_start, seg_stop)
        times = map(lambda b: b.mining_timestamp, segment)
        adjustment = difficulty_adjustment(times, self.l)
        new_difficulty = current_difficulty + adjustment
        self.l.debug("Tuning difficulty:", new_difficulty)

        if new_difficulty < 0:
            self.l.warn(
                "Attempted to set new difficulty to {}, clamping to 0".format(
                    new_difficulty))
            new_difficulty = 0
        elif new_difficulty > 255:
            self.l.warn(
                "Attempted to set new difficulty to {}, clamping to 255".
                format(new_difficulty))
            new_difficulty = 255

        return new_difficulty

    def block_should_be_abandoned(self, block: HashedBlock) -> bool:
        """
        Either the block is in the master chain or it's within 10 blocks of
        the current head.
        """
        return not (self.block_is_in_master_chain(block) or
                    (self.get_head().block_num() - block.block_num() <
                     ABANDONMENT_DEPTH))

    def block_is_in_master_chain(self, block: HashedBlock) -> bool:
        """
        Use BFS to see if the current head can be reached from the block in 
        question.
        """
        head = self.get_head()
        crawl_queue: List[HashedBlock] = [block]

        while len(crawl_queue) > 0:
            cur = crawl_queue.pop(0)
            if cur == head:
                return True
            else:
                crawl_queue.extend(
                    self.storage.get_by_parent_hash(cur.mining_hash()))

        return False

    def _cleanup_outstanding_transactions(self, block: HashedBlock) -> None:
        self.l.debug("Cleaning up outstanding transactions in block", block)
        for txn in block.block.transactions:
            if self.transaction_storage.has_transaction(txn.txn_hash()):
                self.l.debug("Removing outstanding transaction", txn)
                self.transaction_storage.remove_transaction(txn.txn_hash())
            else:
                self.l.debug("Transaction wasn't oustanding", txn)

    def _abandon_blocks(self):
        head = self.get_head()
        abandon_height = head.block_num() - ABANDONMENT_DEPTH
        abandon_candidates = self.storage.get_by_block_num(abandon_height)
        for block in abandon_candidates:
            if self.block_should_be_abandoned(block):
                self.l.debug("Abandon block", block)
                self.storage.abandon_block(block)

                for txn in block.block.transactions:
                    if self.transaction_is_valid(txn):
                        self.l.debug(
                            "Adding abandoned transaction back to pool", txn)
                        self.transaction_storage.add_transaction(txn)
                    else:
                        self.l.debug("Abandoned transaction no longer valid")
Beispiel #14
0
class BlockMiner(object):
    def __init__(self,
                 cfg: Config,
                 key_pair: Optional[KeyPair] = None) -> None:
        self.l = DBLogger(self, cfg)
        self.l.info("Init")
        self.cfg = cfg

        self.client = ChainClient(cfg)

        if key_pair is None:
            self.key_pair = KeyPair.new()
        else:
            self.key_pair = key_pair

        self.storage = SqliteBlockChainStorage(cfg)
        self.transaction_storage = SqliteTransactionStorage(cfg)
        self.uxto_storage = SqliteUXTOStorage(cfg)
        self.chain = BlockChain(self.storage, self.transaction_storage,
                                self.uxto_storage, cfg)

    def mine_forever(self) -> None:
        self.l.info("Miner running")

        while True:
            head = self.chain.get_head()
            self.l.debug("Mining on block {}".format(head.block_num()))
            new_block = self.mine_on(head, self.chain.get_difficulty())

            if new_block:
                self.l.info("Found block {} {}".format(
                    new_block.block_num(),
                    new_block.mining_hash().hex()))
                self.chain.add_block(new_block)
            elif head != self.chain.get_head():
                self.l.info("Preempted! Mining on new block", head)

    def mine_on(self, parent: HashedBlock,
                difficulty: int) -> Optional[HashedBlock]:
        reward = self.make_reward()
        config = BlockConfig(difficulty)

        txns = self.transaction_storage.get_all_transactions()
        txns.append(reward)

        self.l.debug("Mining on {} txns".format(len(txns)))

        block = Block(parent.block_num() + 1, parent.mining_hash(), config,
                      txns)

        hb = HashedBlock(block)

        start = time.time()
        while time.time() - start < 1.0:
            hb.replace_mining_entropy(os.urandom(32))
            if hb.hash_meets_difficulty():
                return hb

        return None

    def transactions(self) -> List[SignedTransaction]:
        candidate_txns = self.transaction_storage.get_all_transactions()
        mine_txns: List[SignedTransaction] = []

        for txn in candidate_txns:
            if self.chain.transaction_is_valid(txn):
                self.l.debug("Txn is is valid for mining", txn)
                mine_txns.append(txn)
            else:
                self.l.warn("Txn is not valid for mining", txn)

        mine_txns.append(self.make_reward())
        return mine_txns

    def make_reward(self) -> SignedTransaction:
        reward = Transaction.reward(REWARD_AMOUNT, self.key_pair.address())
        return SignedTransaction.sign(reward, self.key_pair)

    def make_genesis(self) -> HashedBlock:
        b = Block(
            0,  # block num 
            None,  # parent hash
            BlockConfig(DEFAULT_DIFFICULTY),
            [])
        hb = HashedBlock(b)
        while not hb.hash_meets_difficulty():
            hb.replace_mining_entropy(os.urandom(32))
        return hb
Beispiel #15
0
class PeerList(object):
    def __init__(self, cfg: Config) -> None:
        self.l = DBLogger(self, cfg)
        self._conn = sqlite3.connect(cfg.peer_db_path())

        self._conn.execute(CREATE_TABLE_SQL)
        self._conn.commit()

        self.self_peer = Peer(cfg.server_peer_id(),
                              cfg.server_advertize_addr(),
                              cfg.server_listen_port())

        self.add_peer(self.self_peer)

        try:
            gateway_id = Peer.request_id(cfg.gateway_address(),
                                         cfg.gateway_port())

            self.gateway_peer = Peer(gateway_id, cfg.gateway_address(),
                                     cfg.gateway_port())

            self.add_peer(self.gateway_peer)
        except requests.exceptions.ConnectionError as e:
            self.l.warn("Couldn't connect to gateway", cfg.gateway_address(),
                        cfg.gateway_port())
            self.gateway_peer = None

    def add_peer(self, peer: Peer) -> None:
        if peer == self.self_peer:
            self.l.debug("Not adding self peer", self.self_peer, peer)
            return
        elif self.has_peer(peer):
            self.l.debug("Update peer:", peer)
            self._update_peer(peer)
        else:
            self.l.debug("Insert peer:", peer)
            self._ins_peer(peer)

    def has_peer(self, peer: Peer) -> bool:
        args = {
            "peer_id": peer.peer_id,
        }

        c = self._conn.cursor()
        c.execute(HAS_PEER_SQL, args)
        return c.fetchone() is not None

    def mark_peer_inactive(self, peer: Peer) -> None:
        if peer == self.gateway_peer:
            self.l.warn("Not marking gateway as inactive")
            return
        elif not self.has_peer(peer):
            self.l.debug("Not marking unknown peer {} inactive".format(peer))
            return

        args = {
            "address": peer.address,
            "port": peer.port,
        }

        self._conn.execute(MARK_PEER_INACTIVE_SQL, args)
        self._conn.commit()

    def get_all_active_peers(self) -> List[Peer]:
        c = self._conn.cursor()
        c.execute(GET_ALL_ACTIVE_PEERS_SQL)
        return list(map(lambda row: Peer(*row), c))

    def random_peer(self) -> Peer:
        return random.choice(self.get_all_active_peers())

    def peer_sample(self, n: int) -> List[Peer]:
        return random.sample(self.get_all_active_peers(), n)

    def _update_peer(self, peer: Peer) -> None:
        args = {
            "last_seen_unix_millis": int(time.time() * 1000),
            "active": 1,
            "peer_id": peer.peer_id,
            "address": peer.address,
            "port": peer.port,
        }
        self._conn.execute(UPDATE_PEER_SQL, args)
        self._conn.commit()

    def _ins_peer(self, peer: Peer) -> None:
        args = {
            "last_seen_unix_millis": int(time.time() * 1000),
            "active": 1,
            "peer_id": peer.peer_id,
            "address": peer.address,
            "port": peer.port,
        }
        self._conn.execute(INSERT_PEER_SQL, args)
        self._conn.commit()
Beispiel #16
0
class ChainClient(object):
    def __init__(self, cfg: Config) -> None:
        self.l = DBLogger(self, cfg)
        self.l.info("Init")
        self.peer_list = PeerList(cfg)
        self.self_peer = Peer(cfg.server_peer_id(),
                              cfg.server_advertize_addr(),
                              cfg.server_listen_port())

        self.chain = BlockChain(SqliteBlockChainStorage(cfg),
                                SqliteTransactionStorage(cfg),
                                SqliteUXTOStorage(cfg), cfg)
        self.cfg = cfg

    def poll_forever(self) -> None:
        while True:
            peers = self.our_peers()
            random.shuffle(peers)
            peer_sample = peers[:self.cfg.peer_sample_size()]
            for peer in peer_sample:
                self.l.debug("Syncing with peer", peer)
                self.sync(peer)
            time.sleep(self.cfg.poll_delay())

    def our_peers(self) -> List[Peer]:
        peers = self.peer_list.get_all_active_peers()
        peers = list(filter(lambda p: p != self.self_peer, peers))
        self.l.debug("Sampled peers", peers)
        return peers

    def sync(self, peer: Peer) -> None:
        peers = self.request_peers(peer)

        if peers is None:
            self.l.debug("Peer not responding", peer)
            return

        self.l.debug("Peer {} knows about {} peers".format(peer, len(peers)))

        for new_peer in peers:
            if not self.peer_list.has_peer(
                    new_peer) and new_peer != self.self_peer:
                self.l.info(
                    "Peer {} previously unknown. Adding.".format(new_peer))
                self.peer_list.add_peer(new_peer)

        if self.cfg.advertize_self() and not self.self_peer in peers:
            self.l.info(
                "Peer {} doesn't know about us, telling it.".format(peer))
            resp = self._peer_post(peer, "/peer",
                                   {"peers": [self.self_peer.serializable()]})
            if resp is None:
                self.l.info("Peer didn't respond", peer)

        transactions = self.request_transactions(peer)
        if transactions is None:
            self.l.info("Peer not responding", peer)
            return

        for txn in transactions:
            if not self.chain.transaction_storage.has_transaction(
                    txn.sha256()):
                if self.chain.transaction_is_valid(txn):
                    self.l.info("New transaction", txn)
                    self.chain.add_outstanding_transaction(txn)
                else:
                    self.l.warn("Peer sent us an invalid transaction", peer,
                                txn)

        peer_head = self.request_head(peer)
        if not peer_head:
            self.l.debug("Peer {} didn't give us a head block.".format(peer))
            return

        if self.chain.storage.has_hash(peer_head.mining_hash()):
            self.l.debug("Already have peer's head block")
            return

        if peer_head.block_num() == 0:
            self.l.warn(
                "Peer returned a different genesis block than expected", peer,
                peer_head)
            return

        parent = self.request_block(peer_head.parent_mining_hash(), peer)

        if parent is None:
            self.l.debug("Wat. Peer didn't have parent:",
                         peer_head.parent_mining_hash())
            return

        while not self.chain.storage.has_hash(parent.mining_hash()):
            parent = self.request_block(parent.parent_mining_hash(), peer)

            if parent is None:
                self.l.debug("Wat. Peer didn't have parent:",
                             peer_head.parent_mining_hash())
                return

        to_request = [parent]
        while len(to_request) > 0:
            parent = to_request.pop(0)
            successors = self.request_successors(parent.mining_hash(), peer)

            if successors is None:
                self.l.debug("Peer is not responding", peer)
                return

            for succ in successors:  # ( ͡° ͜ʖ ͡°)
                self.l.info("New block:", succ)
                to_request.append(succ)
                self.chain.add_block(succ)

    def request_block(self, block_hash: Hash,
                      peer: Peer) -> Optional[HashedBlock]:
        obj = self._peer_get(peer, "/block", {"hex_hash": block_hash.hex()})
        if obj is None:
            self.l.debug("No HTTP response from peer {}".format(peer))
            return None

        if obj is None:
            self.l.debug("No block from peer", peer)
            return None

        try:
            hb = HashedBlock.from_dict(obj)
        except KeyError as e:
            self.l.debug("Invalid serialized block from peer {}".format(peer),
                         exc=e)
            return None

        return hb

    def request_head(self, peer: Peer) -> Optional[HashedBlock]:
        head_hash = self.request_head_hash(peer)

        if head_hash is None:
            self.l.debug("Can't get head block from peer", peer)
            return None

        obj = self._peer_get(peer, "/block", {"hex_hash": head_hash.hex()})

        try:
            h = HashedBlock.from_dict(obj)
        except KeyError as e:
            self.l.debug("Invalid block from peer {}".format(peer), exc=e)
            return None

        return h

    def request_head_hash(self, peer: Peer) -> Optional[Hash]:
        obj = self._peer_get(peer, "/chain", {})

        if obj is None:
            self.l.debug("No head hash from peer", peer)
            return None

        try:
            h = Hash.from_dict(obj["head_hash"])
        except KeyError as e:
            self.l.debug("No head hash from peer {}".format(peer), exc=e)
            return None

        return h

    def request_successors(self, parent_hash: Hash,
                           peer: Peer) -> Optional[List[HashedBlock]]:
        payload = {"parent_hex_hash": parent_hash.hex()}
        obj = self._peer_get(peer, "/blocks", payload)

        if obj is None:
            self.l.debug("No successors from peer", peer)
            return None

        if "blocks" not in obj:
            self.l.debug("Couldn't turn response into list of blocks", peer,
                         obj)
            return None

        new_blocks: List[HashedBlock] = []
        for block_obj in obj["blocks"]:
            try:
                b = HashedBlock.from_dict(block_obj)
            except KeyError as e:
                self.l.debug("Invalid block object from peer", peer, obj)
                return None

            new_blocks.append(b)

        return new_blocks

    def request_peers(self, peer: Peer) -> Optional[List[Peer]]:
        obj = self._peer_get(peer, "/peers", {})
        if obj is None:
            self.l.debug("No peer response from peer", peer)
            return None

        if not "peers" in obj:
            self.l.debug("No peers in response from peer", peer, obj)
            return None

        new_peers: List[Peer] = []
        for peer_obj in obj["peers"]:
            try:
                new_peer = Peer.from_dict(peer_obj)
            except KeyError as e:
                self.l.debug("Invalid peer from peer", peer, new_peer)
                return []
            new_peers.append(new_peer)
        return new_peers

    def request_transactions(self,
                             peer: Peer) -> Optional[List[SignedTransaction]]:
        obj = self._peer_get(peer, "/outstanding_transactions", {})
        if obj is None:
            self.l.debug("No peer response from peer", peer)
            return None

        if not "transactions" in obj:
            self.l.debug("No transaction response from peer", peer, obj)
            return None

        new_transactions: List[SignedTransaction] = []
        for txn_obj in obj["transactions"]:
            try:
                new_txn = SignedTransaction.from_dict(txn_obj)
            except KeyError as e:
                self.l.debug("Invalid transaction from peer",
                             peer,
                             txn_obj,
                             exc=e)
            else:
                new_transactions.append(new_txn)
        return new_transactions

    def _peer_get(self, peer: Peer, path: str,
                  params: Dict[str, Any]) -> Optional[Dict[str, Any]]:

        url = peer.http_url(path)
        self.l.debug("get", url, params)

        try:
            r = requests.get(url, params=params)
            resp = r.content
        except requests.exceptions.ConnectionError as e:
            # todo: mark peer as inactive
            self.l.debug("No response from peer", peer, exc=e)
            return None

        try:
            obj = json.loads(resp)
        except ValueError as e:
            self.l.debug("Invalid JSON response from peer", peer, exc=e)
            return None

        return obj

    def _peer_post(self, peer: Peer, path: str,
                   payload: Dict[str, Any]) -> Optional[Dict[str, Any]]:
        url = peer.http_url(path)
        self.l.debug("post", url, payload)

        try:
            r = requests.post(url, json=payload)
            resp = r.content
        except requests.exceptions.ConnectionError as e:
            # todo: mark peer as inactive
            self.l.debug("No response from peer", peer, exc=e)
            return None

        try:
            obj = json.loads(resp)
        except ValueError as e:
            self.l.debug("Invalid JSON response from peer", peer, exc=e)
            return None

        return obj
Beispiel #17
0
    def initialize(self, chain: BlockChain, cfg: Config) -> None:

        self.chain = chain
        self.l = DBLogger(self, cfg)
Beispiel #18
0
class BlockRequestHandler(web.RequestHandler):
    def initialize(self, chain: BlockChain, cfg: Config) -> None:

        self.chain = chain
        self.l = DBLogger(self, cfg)

    def get(self) -> None:
        requested_hash = self.get_query_argument("hex_hash", None)
        requested_block_num = self.get_query_argument("block_num", None)
        parent_hash = self.get_query_argument("parent_hex_hash", None)

        if requested_hash is not None:
            self.get_by_hash(Hash.fromhex(requested_hash))
        elif requested_block_num is not None:
            self.get_by_block_num(int(requested_block_num))
        elif parent_hash is not None:
            self.get_by_parent_hash(Hash.fromhex(parent_hash))
        else:
            self.set_status(400)
            self.write(
                util.error_response(
                    "missing 'requested_hash' or 'requested_block_num' params")
            )

    def post(self) -> None:
        ser = self.request.body.decode('utf-8')
        hb = HashedBlock.deserialize(ser)

        if self.chain.storage.has_hash(hb.mining_hash()):
            self.set_status(200)
            self.write(util.generic_ok_response("already have that one"))
            return

        if not self.chain.storage.has_hash(hb.parent_mining_hash()):
            self.set_status(400)
            self.write(util.error_response("unknown parent"))
            return

        self.l.info("New block", hb.block_num(), hb.mining_hash())
        self.chain.add_block(hb)
        self.set_status(200)
        self.write(util.generic_ok_response())

    def get_by_hash(self, mining_hash: Hash) -> None:
        block = self.chain.storage.get_by_hash(mining_hash)

        if block:
            self.set_status(200)
            self.write(block.serializable())
        else:
            self.set_status(404)
            self.write(util.error_response("no block with given hash"))

    def get_by_block_num(self, block_num: int) -> None:
        blocks = self.chain.storage.get_by_block_num(block_num)
        ser_blocks = list(map(lambda b: b.serializable(), blocks))
        response = {"blocks": ser_blocks}
        self.set_status(200)
        self.write(response)

    def get_by_parent_hash(self, parent_mining_hash: Hash):
        blocks = self.chain.storage.get_by_parent_hash(parent_mining_hash)
        self.set_status(200)
        ser_blocks = list(map(lambda b: b.serializable(), blocks))
        resp = {"blocks": ser_blocks}
        self.write(resp)
Beispiel #19
0
 def initialize(self, cfg: Config, chain: BlockChain):
     self.l = DBLogger(self, cfg)
     self.chain = chain
Beispiel #20
0
class Wallet(object):
    def __init__(self, cfg: Config, password: str) -> None:
        self.l = DBLogger(self, cfg)

        kdf = pwhash.argon2i.kdf
        salt = utils.random(pwhash.argon2i.SALTBYTES)
        ops = pwhash.argon2i.OPSLIMIT_SENSITIVE
        mem = pwhash.argon2i.MEMLIMIT_SENSITIVE

        self.l.info(
            "Deriving wallet encryption key, might take a couple seconds")
        self.key = kdf(secret.SecretBox.KEY_SIZE, password, salt,
                                 opslimit=ops, memlimit=mem)
        self.box = secret.SecretBox(Alices_key)

        self._uxto_storage = SqliteUXTOStorage(cfg)

        self._chain = BlockChain(
                SqliteBlockChainStorage(cfg),
                SqliteTransactionStorage(cfg),
                self._uxto_storage,
                cfg)

        self._conn = sqlite3.connect(cfg.wallet_path())
        self._conn.execute(CREATE_TABLE_SQL)
        self._conn.commit()

    def create_wallet(self, wallet_name: str) -> None:
        kp = KeyPair.new()
        self.add_existing_key_pair(wallet_name, kp)

    def add_existing_key_pair(
            self, wallet_name: str, key_pair: KeyPair) -> None:
        nonce = utils.random(secret.SecretBox.NONCE_SIZE)
        plaintext = bytes(key_pair._signing_key)
        encrypted = self.box.encrypt(plaintext, nonce)

        args = {
            "wallet_name": wallet_name,
            "ed25519_pub_key_hex": key_pair.address().hex(),
            "encrypted_ed25519_priv_key": encrypted,
        }

        self._conn.execute(ADD_KEY_PAIR_SQL, args)
        self._conn.commit()

    def list_wallets(self) -> List[str]:
        c = self._conn.cursor()
        c.execute(LIST_WALLETS_SQL)
        return map(lambda r: r[0], c)

    def get_balance(self, wallet_name: str) -> Amount:
        key_pairs = self.get_key_pairs(wallet_name)
        uxtos: List[UXTO] = []

        for kp in key_pairs:
            uxtos.extend(self._uxto_storage.unclaimed_outputs(kp.address()))
        return Amount.units(0)

    def create_transaction(
            self, wallet_name: str, to_addr: Address) -> None:
        pass

    def get_key_pairs(self, wallet_name: str) -> List[KeyPair]:
        args = {"wallet_name": wallet_name}
        c = self._conn.cursor()
        c.execute(GET_KEY_PAIRS_FOR_WALLET, args)
        return list(map(lambda row: KeyPair.from_seed(row[0]), c))
Beispiel #21
0
 def initialize(self, peer_list: PeerList, cfg: Config) -> None:
     self.l = DBLogger(self, cfg)
     self.peer_list = peer_list
     self.cfg = cfg