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"))
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")
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())
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")
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
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)
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
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))