class AccountDB(BaseAccountDB): logger = logging.getLogger('evm.db.account.AccountDB') def __init__(self, db, state_root=BLANK_ROOT_HASH): r""" Internal implementation details (subject to rapid change): Database entries go through several pipes, like so... .. code:: -> hash-trie -> storage lookups / db -----------------------> _journaldb ----------------> code lookups \ > _trie -> _trie_cache -> _journaltrie --------------> account lookups Journaling sequesters writes here ^, until persist is called. _journaldb is a journaling of the keys and values used to store code and account storage. _trie is a hash-trie, used to generate the state root _trie_cache is a cache tied to the state root of the trie. It is important that this cache is checked *after* looking for the key in _journaltrie, because the cache is only invalidated after a state root change. _journaltrie is a journaling of the accounts (an address->rlp mapping, rather than the nodes stored by the trie). This enables a squashing of all account changes before pushing them into the trie. .. NOTE:: There is an opportunity to do something similar for storage AccountDB synchronizes the snapshot/revert/persist of both of the journals. """ self._journaldb = JournalDB(db) self._trie = HashTrie(HexaryTrie(db, state_root)) self._trie_cache = CacheDB(self._trie) self._journaltrie = JournalDB(self._trie_cache) @property def state_root(self): return self._trie.root_hash @state_root.setter def state_root(self, value): self._trie_cache.reset_cache() self._trie.root_hash = value # # Storage # def get_storage(self, address, slot): validate_canonical_address(address, title="Storage Address") validate_uint256(slot, title="Storage Slot") account = self._get_account(address) storage = HashTrie(HexaryTrie(self._journaldb, account.storage_root)) slot_as_key = pad32(int_to_big_endian(slot)) if slot_as_key in storage: encoded_value = storage[slot_as_key] return rlp.decode(encoded_value, sedes=rlp.sedes.big_endian_int) else: return 0 def set_storage(self, address, slot, value): validate_uint256(value, title="Storage Value") validate_uint256(slot, title="Storage Slot") validate_canonical_address(address, title="Storage Address") account = self._get_account(address) storage = HashTrie(HexaryTrie(self._journaldb, account.storage_root)) slot_as_key = pad32(int_to_big_endian(slot)) if value: encoded_value = rlp.encode(value) storage[slot_as_key] = encoded_value else: del storage[slot_as_key] self._set_account(address, account.copy(storage_root=storage.root_hash)) def delete_storage(self, address): validate_canonical_address(address, title="Storage Address") account = self._get_account(address) self._set_account(address, account.copy(storage_root=BLANK_ROOT_HASH)) # # Balance # def get_balance(self, address): validate_canonical_address(address, title="Storage Address") account = self._get_account(address) return account.balance def set_balance(self, address, balance): validate_canonical_address(address, title="Storage Address") validate_uint256(balance, title="Account Balance") account = self._get_account(address) self._set_account(address, account.copy(balance=balance)) # # Nonce # def get_nonce(self, address): validate_canonical_address(address, title="Storage Address") account = self._get_account(address) return account.nonce def set_nonce(self, address, nonce): validate_canonical_address(address, title="Storage Address") validate_uint256(nonce, title="Nonce") account = self._get_account(address) self._set_account(address, account.copy(nonce=nonce)) def increment_nonce(self, address): current_nonce = self.get_nonce(address) self.set_nonce(address, current_nonce + 1) # # Code # def get_code(self, address): validate_canonical_address(address, title="Storage Address") try: return self._journaldb[self.get_code_hash(address)] except KeyError: return b"" def set_code(self, address, code): validate_canonical_address(address, title="Storage Address") validate_is_bytes(code, title="Code") account = self._get_account(address) code_hash = keccak(code) self._journaldb[code_hash] = code self._set_account(address, account.copy(code_hash=code_hash)) def get_code_hash(self, address): validate_canonical_address(address, title="Storage Address") account = self._get_account(address) return account.code_hash def delete_code(self, address): validate_canonical_address(address, title="Storage Address") account = self._get_account(address) self._set_account(address, account.copy(code_hash=EMPTY_SHA3)) # # Account Methods # def account_has_code_or_nonce(self, address): return self.get_nonce(address) != 0 or self.get_code_hash( address) != EMPTY_SHA3 def delete_account(self, address): validate_canonical_address(address, title="Storage Address") del self._journaltrie[address] def account_exists(self, address): validate_canonical_address(address, title="Storage Address") return self._journaltrie.get(address, b'') != b'' def touch_account(self, address): validate_canonical_address(address, title="Storage Address") account = self._get_account(address) self._set_account(address, account) def account_is_empty(self, address): return not self.account_has_code_or_nonce( address) and self.get_balance(address) == 0 # # Internal # def _get_account(self, address): rlp_account = self._journaltrie.get(address, b'') if rlp_account: account = rlp.decode(rlp_account, sedes=Account) else: account = Account() return account def _set_account(self, address, account): rlp_account = rlp.encode(account, sedes=Account) self._journaltrie[address] = rlp_account # # Record and discard API # def record(self) -> Tuple[UUID, UUID]: return (self._journaldb.record(), self._journaltrie.record()) def discard(self, changeset: Tuple[UUID, UUID]) -> None: db_changeset, trie_changeset = changeset self._journaldb.discard(db_changeset) self._journaltrie.discard(trie_changeset) def commit(self, changeset: Tuple[UUID, UUID]) -> None: db_changeset, trie_changeset = changeset self._journaldb.commit(db_changeset) self._journaltrie.commit(trie_changeset) def persist(self) -> None: self.logger.debug("Persisting AccountDB...") self._journaldb.persist() self._journaltrie.persist() def _log_pending_accounts(self) -> None: accounts_displayed = set() # type: Set[bytes] queued_changes = self._journaltrie.journal.journal_data.items() # mypy bug for ordered dict reversibility: https://github.com/python/typeshed/issues/2078 for checkpoint, accounts in reversed(queued_changes): # type: ignore for address in accounts: if address in accounts_displayed: continue else: accounts_displayed.add(address) account = self._get_account(address) self.logger.debug( "Account %s: balance %d, nonce %d, storage root %s, code hash %s", encode_hex(address), account.balance, account.nonce, encode_hex(account.storage_root), encode_hex(account.code_hash), )
class ChainDB(BaseChainDB): def __init__(self, db, trie_class=HexaryTrie): self.db = JournalDB(db) self.set_trie(trie_class) # # Canonical chain API # def get_canonical_head(self): if not self.exists(CANONICAL_HEAD_HASH_DB_KEY): raise CanonicalHeadNotFound("No canonical head set for this chain") return self.get_block_header_by_hash(self.db.get(CANONICAL_HEAD_HASH_DB_KEY)) def get_canonical_block_header_by_number(self, block_number): """ Returns the block header with the given number in the canonical chain. Raises BlockNotFound if there's no block header with the given number in the canonical chain. """ validate_uint256(block_number, title="Block Number") return self.get_block_header_by_hash(self.lookup_block_hash(block_number)) # # Block Header API # def get_block_header_by_hash(self, block_hash): """ Returns the requested block header as specified by block hash. Raises BlockNotFound if it is not present in the db. """ validate_word(block_hash, title="Block Hash") try: block = self.db.get(block_hash) except KeyError: raise BlockNotFound("No block with hash {0} found".format( encode_hex(block_hash))) return rlp.decode(block, sedes=BlockHeader) def header_exists(self, block_hash): """Returns True if the header with the given block hash is in our DB.""" return self.db.exists(block_hash) # TODO: This method sould take a chain of headers as that's the most common use case # and it'd be much faster than inserting each header individually. def persist_header_to_db(self, header): """ :returns: iterable of headers newly on the canonical chain """ if header.parent_hash != GENESIS_PARENT_HASH and not self.header_exists(header.parent_hash): raise ParentNotFound( "Cannot persist block header ({}) with unknown parent ({})".format( encode_hex(header.hash), encode_hex(header.parent_hash))) self.db.set( header.hash, rlp.encode(header), ) if header.parent_hash == GENESIS_PARENT_HASH: score = header.difficulty else: score = self.get_score(header.parent_hash) + header.difficulty self.db.set( make_block_hash_to_score_lookup_key(header.hash), rlp.encode(score, sedes=rlp.sedes.big_endian_int)) try: head_score = self.get_score(self.get_canonical_head().hash) except CanonicalHeadNotFound: new_headers = self._set_as_canonical_chain_head(header) else: if score > head_score: new_headers = self._set_as_canonical_chain_head(header) else: new_headers = [] return new_headers def _set_as_canonical_chain_head(self, header): """ :returns: iterable of headers newly on the canonical head """ try: self.get_block_header_by_hash(header.hash) except BlockNotFound: raise ValueError("Cannot use unknown block hash as canonical head: {}".format( header.hash)) new_canonical_headers = tuple(reversed(self._find_new_ancestors(header))) # remove transaction lookups for blocks that are no longer canonical for h in new_canonical_headers: try: old_hash = self.lookup_block_hash(h.block_number) except KeyError: # no old block, and no more possible break else: old_header = self.get_block_header_by_hash(old_hash) for transaction_hash in self.get_block_transaction_hashes(old_header): self._remove_transaction_from_canonical_chain(transaction_hash) # TODO re-add txn to internal pending pool (only if local sender) pass for h in new_canonical_headers: self._add_block_number_to_hash_lookup(h) self.db.set(CANONICAL_HEAD_HASH_DB_KEY, header.hash) return new_canonical_headers @to_tuple def _find_new_ancestors(self, header): """ Returns the chain leading up from the given header until (but not including) the first ancestor it has in common with our canonical chain. If D is the canonical head in the following chain, and F is the new header, then this function returns (F, E). A - B - C - D \ E - F """ h = header while True: try: orig = self.get_canonical_block_header_by_number(h.block_number) except KeyError: # This just means the block is not on the canonical chain. pass else: if orig.hash == h.hash: # Found the common ancestor, stop. break # Found a new ancestor yield h if h.parent_hash == GENESIS_PARENT_HASH: break else: h = self.get_block_header_by_hash(h.parent_hash) def _add_block_number_to_hash_lookup(self, header): block_number_to_hash_key = make_block_number_to_hash_lookup_key( header.block_number ) self.db.set( block_number_to_hash_key, rlp.encode(header.hash, sedes=rlp.sedes.binary), ) # # Block API # def get_score(self, block_hash): return rlp.decode( self.db.get(make_block_hash_to_score_lookup_key(block_hash)), sedes=rlp.sedes.big_endian_int) def lookup_block_hash(self, block_number): """ Return the block hash for the given block number. """ validate_uint256(block_number, title="Block Number") number_to_hash_key = make_block_number_to_hash_lookup_key(block_number) return rlp.decode( self.db.get(number_to_hash_key), sedes=rlp.sedes.binary, ) def persist_block_to_db(self, block): ''' Chain must do follow-up work to persist transactions to db ''' new_canonical_headers = self.persist_header_to_db(block.header) # Persist the transaction bodies transaction_db = self.trie_class(self.db, root_hash=self.empty_root_hash) for i, transaction in enumerate(block.transactions): index_key = rlp.encode(i, sedes=rlp.sedes.big_endian_int) transaction_db[index_key] = rlp.encode(transaction) assert transaction_db.root_hash == block.header.transaction_root for header in new_canonical_headers: for index, transaction_hash in enumerate(self.get_block_transaction_hashes(header)): self._add_transaction_to_canonical_chain(transaction_hash, header, index) # Persist the uncles list self.db.set( block.header.uncles_hash, rlp.encode(block.uncles, sedes=rlp.sedes.CountableList(type(block.header))), ) def get_block_uncles(self, uncles_hash): validate_word(uncles_hash, title="Uncles Hash") return rlp.decode(self.db.get(uncles_hash), sedes=rlp.sedes.CountableList(BlockHeader)) # # Transaction and Receipt API # @to_list def get_receipts(self, header, receipt_class): receipt_db = self.trie_class(db=self.db, root_hash=header.receipt_root) for receipt_idx in itertools.count(): receipt_key = rlp.encode(receipt_idx) if receipt_key in receipt_db: receipt_data = receipt_db[receipt_key] yield rlp.decode(receipt_data, sedes=receipt_class) else: break def _get_block_transaction_data(self, block_header): ''' :returns: iterable of encoded transactions for the given block header ''' transaction_db = self.trie_class(self.db, root_hash=block_header.transaction_root) for transaction_idx in itertools.count(): transaction_key = rlp.encode(transaction_idx) if transaction_key in transaction_db: yield transaction_db[transaction_key] else: break @to_list def get_block_transaction_hashes(self, block_header): for encoded_transaction in self._get_block_transaction_data(block_header): yield keccak(encoded_transaction) @to_list def get_block_transactions(self, block_header, transaction_class): for encoded_transaction in self._get_block_transaction_data(block_header): yield rlp.decode(encoded_transaction, sedes=transaction_class) def get_transaction_by_index(self, block_number, transaction_index, transaction_class): try: block_header = self.get_canonical_block_header_by_number(block_number) except KeyError: raise TransactionNotFound("Block {} is not in the canonical chain".format(block_number)) transaction_db = self.trie_class(self.db, root_hash=block_header.transaction_root) encoded_index = rlp.encode(transaction_index) if encoded_index in transaction_db: encoded_transaction = transaction_db[encoded_index] return rlp.decode(encoded_transaction, sedes=transaction_class) else: raise TransactionNotFound( "No transaction is at index {} of block {}".format(transaction_index, block_number)) def get_pending_transaction(self, transaction_hash, transaction_class): try: data = self.db.get(make_transaction_hash_to_data_lookup_key(transaction_hash)) return rlp.decode(data, sedes=transaction_class) except KeyError: raise TransactionNotFound( "Transaction with hash {} not found".format(encode_hex(transaction_hash))) def get_transaction_index(self, transaction_hash): try: encoded_key = self.db.get(make_transaction_hash_to_block_lookup_key(transaction_hash)) except KeyError: raise TransactionNotFound( "Transaction {} not found in canonical chain".format(encode_hex(transaction_hash))) transaction_key = rlp.decode(encoded_key, sedes=TransactionKey) return (transaction_key.block_number, transaction_key.index) def _remove_transaction_from_canonical_chain(self, transaction_hash): self.db.delete(make_transaction_hash_to_block_lookup_key(transaction_hash)) def _add_transaction_to_canonical_chain(self, transaction_hash, block_header, index): """ :param bytes transaction_hash: the hash of the transaction to add the lookup for :param block_header: The header of the block with the txn that is in the canonical chain :param int index: the position of the transaction in the block - add lookup from transaction hash to the block number and index that the body is stored at - remove transaction hash to body lookup in the pending pool """ transaction_key = TransactionKey(block_header.block_number, index) self.db.set( make_transaction_hash_to_block_lookup_key(transaction_hash), rlp.encode(transaction_key), ) # because transaction is now in canonical chain, can now remove from pending txn lookups lookup_key = make_transaction_hash_to_data_lookup_key(transaction_hash) if self.db.exists(lookup_key): self.db.delete(lookup_key) def add_pending_transaction(self, transaction): self.db.set( make_transaction_hash_to_data_lookup_key(transaction.hash), rlp.encode(transaction), ) def add_transaction(self, block_header, index_key, transaction): transaction_db = self.trie_class(self.db, root_hash=block_header.transaction_root) transaction_db[index_key] = rlp.encode(transaction) return transaction_db.root_hash def add_receipt(self, block_header, index_key, receipt): receipt_db = self.trie_class(db=self.db, root_hash=block_header.receipt_root) receipt_db[index_key] = rlp.encode(receipt) return receipt_db.root_hash # # Raw Database API # def exists(self, key): return self.db.exists(key) def persist_trie_data_dict_to_db(self, trie_data_dict): """ Store raw trie data to db from a dict """ for key, value in trie_data_dict.items(): self.db[key] = value # # Snapshot and revert API # def snapshot(self): return self.db.snapshot() def revert(self, checkpoint): self.db.revert(checkpoint) def commit(self, checkpoint): self.db.commit(checkpoint) def clear(self): self.db.clear() # # State Database API # def get_state_db(self, state_root, read_only): return AccountStateDB(db=self.db, root_hash=state_root, read_only=read_only)
class AccountDB(BaseAccountDB): def __init__(self, db, state_root=BLANK_ROOT_HASH): # Keep a reference to the original db instance to use it as part of _get_account()'s cache # key. self._unwrapped_db = db self.db = JournalDB(db) self._trie = HashTrie(HexaryTrie(self.db, state_root)) @property def state_root(self): return self._trie.root_hash @state_root.setter def state_root(self, value): self._trie.root_hash = value # # Storage # def get_storage(self, address, slot): validate_canonical_address(address, title="Storage Address") validate_uint256(slot, title="Storage Slot") account = self._get_account(address) storage = HashTrie(HexaryTrie(self.db, account.storage_root)) slot_as_key = pad32(int_to_big_endian(slot)) if slot_as_key in storage: encoded_value = storage[slot_as_key] return rlp.decode(encoded_value, sedes=rlp.sedes.big_endian_int) else: return 0 def set_storage(self, address, slot, value): validate_uint256(value, title="Storage Value") validate_uint256(slot, title="Storage Slot") validate_canonical_address(address, title="Storage Address") account = self._get_account(address) storage = HashTrie(HexaryTrie(self.db, account.storage_root)) slot_as_key = pad32(int_to_big_endian(slot)) if value: encoded_value = rlp.encode(value) storage[slot_as_key] = encoded_value else: del storage[slot_as_key] self._set_account(address, account.copy(storage_root=storage.root_hash)) def delete_storage(self, address): validate_canonical_address(address, title="Storage Address") account = self._get_account(address) self._set_account(address, account.copy(storage_root=BLANK_ROOT_HASH)) # # Balance # def get_balance(self, address): validate_canonical_address(address, title="Storage Address") account = self._get_account(address) return account.balance def set_balance(self, address, balance): validate_canonical_address(address, title="Storage Address") validate_uint256(balance, title="Account Balance") account = self._get_account(address) self._set_account(address, account.copy(balance=balance)) # # Nonce # def get_nonce(self, address): validate_canonical_address(address, title="Storage Address") account = self._get_account(address) return account.nonce def set_nonce(self, address, nonce): validate_canonical_address(address, title="Storage Address") validate_uint256(nonce, title="Nonce") account = self._get_account(address) self._set_account(address, account.copy(nonce=nonce)) def increment_nonce(self, address): current_nonce = self.get_nonce(address) self.set_nonce(address, current_nonce + 1) # # Code # def get_code(self, address): validate_canonical_address(address, title="Storage Address") try: return self.db[self.get_code_hash(address)] except KeyError: return b"" def set_code(self, address, code): validate_canonical_address(address, title="Storage Address") validate_is_bytes(code, title="Code") account = self._get_account(address) code_hash = keccak(code) self.db[code_hash] = code self._set_account(address, account.copy(code_hash=code_hash)) def get_code_hash(self, address): validate_canonical_address(address, title="Storage Address") account = self._get_account(address) return account.code_hash def delete_code(self, address): validate_canonical_address(address, title="Storage Address") account = self._get_account(address) self._set_account(address, account.copy(code_hash=EMPTY_SHA3)) # # Account Methods # def account_has_code_or_nonce(self, address): return self.get_nonce(address) != 0 or self.get_code_hash( address) != EMPTY_SHA3 def delete_account(self, address): validate_canonical_address(address, title="Storage Address") del self._trie[address] def account_exists(self, address): validate_canonical_address(address, title="Storage Address") return bool(self._trie[address]) def touch_account(self, address): validate_canonical_address(address, title="Storage Address") account = self._get_account(address) self._set_account(address, account) def account_is_empty(self, address): return not self.account_has_code_or_nonce( address) and self.get_balance(address) == 0 # # Internal # def _get_account(self, address): cache_key = (id(self._unwrapped_db), self.state_root, address) if cache_key not in account_cache: account_cache[cache_key] = self._trie[address] rlp_account = account_cache[cache_key] if rlp_account: account = rlp.decode(rlp_account, sedes=Account) else: account = Account() return account def _set_account(self, address, account): rlp_account = rlp.encode(account, sedes=Account) self._trie[address] = rlp_account cache_key = (id(self._unwrapped_db), self.state_root, address) account_cache[cache_key] = rlp_account # # Record and discard API # def record(self) -> UUID: return self.db.record() def discard(self, changeset_id: UUID) -> None: return self.db.discard(changeset_id) def commit(self, changeset_id: UUID) -> None: return self.db.commit(changeset_id) def persist(self) -> None: return self.db.persist() def clear(self) -> None: return self.db.reset()
class BaseChainDB: def __init__(self, db): self.db = JournalDB(db) def exists(self, key): return self.db.exists(key) def get_canonical_head(self): if not self.exists(CANONICAL_HEAD_HASH_DB_KEY): raise CanonicalHeadNotFound("No canonical head set for this chain") return self.get_block_header_by_hash( self.db.get(CANONICAL_HEAD_HASH_DB_KEY)) def get_canonical_block_header_by_number(self, block_number): """ Returns the block header with the given number in the canonical chain. Raises BlockNotFound if there's no block header with the given number in the canonical chain. """ validate_uint256(block_number, title="Block Number") return self.get_block_header_by_hash( self.lookup_block_hash(block_number)) def get_score(self, block_hash): return rlp.decode(self.db.get( make_block_hash_to_score_lookup_key(block_hash)), sedes=rlp.sedes.big_endian_int) def _set_as_canonical_chain_head(self, header): """ :returns: iterable of headers newly on the canonical head """ try: self.get_block_header_by_hash(header.hash) except BlockNotFound: raise ValueError( "Cannot use unknown block hash as canonical head: {}".format( header.hash)) new_canonical_headers = tuple( reversed(self._find_new_ancestors(header))) # remove transaction lookups for blocks that are no longer canonical for h in new_canonical_headers: try: old_hash = self.lookup_block_hash(h.block_number) except KeyError: # no old block, and no more possible break else: old_header = self.get_block_header_by_hash(old_hash) for transaction_hash in self.get_block_transaction_hashes( old_header): self._remove_transaction_from_canonical_chain( transaction_hash) # TODO re-add txn to internal pending pool (only if local sender) pass for h in new_canonical_headers: self.add_block_number_to_hash_lookup(h) self.db.set(CANONICAL_HEAD_HASH_DB_KEY, header.hash) return new_canonical_headers @to_tuple def _find_new_ancestors(self, header): """ Returns the chain leading up from the given header until (but not including) the first ancestor it has in common with our canonical chain. If D is the canonical head in the following chain, and F is the new header, then this function returns (F, E). A - B - C - D \ E - F """ h = header while True: try: orig = self.get_canonical_block_header_by_number( h.block_number) except KeyError: # This just means the block is not on the canonical chain. pass else: if orig.hash == h.hash: # Found the common ancestor, stop. break # Found a new ancestor yield h if h.parent_hash == GENESIS_PARENT_HASH: break else: h = self.get_block_header_by_hash(h.parent_hash) def get_block_header_by_hash(self, block_hash): """ Returns the requested block header as specified by block hash. Raises BlockNotFound if it is not present in the db. """ validate_word(block_hash, title="Block Hash") try: block = self.db.get(block_hash) except KeyError: raise BlockNotFound("No block with hash {0} found".format( encode_hex(block_hash))) return rlp.decode(block, sedes=BlockHeader) def header_exists(self, block_hash): """Returns True if the header with the given block hash is in our DB.""" return self.db.exists(block_hash) def lookup_block_hash(self, block_number): """ Return the block hash for the given block number. """ validate_uint256(block_number, title="Block Number") number_to_hash_key = make_block_number_to_hash_lookup_key(block_number) return rlp.decode( self.db.get(number_to_hash_key), sedes=rlp.sedes.binary, ) def get_block_uncles(self, uncles_hash): validate_word(uncles_hash, title="Uncles Hash") return rlp.decode(self.db.get(uncles_hash), sedes=rlp.sedes.CountableList(BlockHeader)) @to_list def get_receipts(self, header, receipt_class): receipt_db = HexaryTrie(db=self.db, root_hash=header.receipt_root) for receipt_idx in itertools.count(): receipt_key = rlp.encode(receipt_idx) if receipt_key in receipt_db: receipt_data = receipt_db[receipt_key] yield rlp.decode(receipt_data, sedes=receipt_class) else: break def _get_block_transaction_data(self, block_header): ''' :returns: iterable of encoded transactions for the given block header ''' transaction_db = HexaryTrie(self.db, root_hash=block_header.transaction_root) for transaction_idx in itertools.count(): transaction_key = rlp.encode(transaction_idx) if transaction_key in transaction_db: yield transaction_db[transaction_key] else: break @to_list def get_block_transaction_hashes(self, block_header): for encoded_transaction in self._get_block_transaction_data( block_header): yield keccak(encoded_transaction) @to_list def get_block_transactions(self, block_header, transaction_class): for encoded_transaction in self._get_block_transaction_data( block_header): yield rlp.decode(encoded_transaction, sedes=transaction_class) def get_transaction_by_index(self, block_number, transaction_index, transaction_class): try: block_header = self.get_canonical_block_header_by_number( block_number) except KeyError: raise TransactionNotFound( "Block {} is not in the canonical chain".format(block_number)) transaction_db = HexaryTrie(self.db, root_hash=block_header.transaction_root) encoded_index = rlp.encode(transaction_index) if encoded_index in transaction_db: encoded_transaction = transaction_db[encoded_index] return rlp.decode(encoded_transaction, sedes=transaction_class) else: raise TransactionNotFound( "No transaction is at index {} of block {}".format( transaction_index, block_number)) def get_pending_transaction(self, transaction_hash, transaction_class): try: data = self.db.get( make_transaction_hash_to_data_lookup_key(transaction_hash)) return rlp.decode(data, sedes=transaction_class) except KeyError: raise TransactionNotFound( "Transaction with hash {} not found".format( encode_hex(transaction_hash))) def get_transaction_index(self, transaction_hash): try: encoded_key = self.db.get( make_transaction_hash_to_block_lookup_key(transaction_hash)) except KeyError: raise TransactionNotFound( "Transaction {} not found in canonical chain".format( encode_hex(transaction_hash))) transaction_key = rlp.decode(encoded_key, sedes=TransactionKey) return (transaction_key.block_number, transaction_key.index) def add_block_number_to_hash_lookup(self, header): block_number_to_hash_key = make_block_number_to_hash_lookup_key( header.block_number) self.db.set( block_number_to_hash_key, rlp.encode(header.hash, sedes=rlp.sedes.binary), ) # TODO: This method sould take a chain of headers as that's the most common use case # and it'd be much faster than inserting each header individually. def persist_header_to_db(self, header): """ :returns: iterable of headers newly on the canonical chain """ if header.parent_hash != GENESIS_PARENT_HASH and not self.header_exists( header.parent_hash): raise ParentNotFound( "Cannot persist block header ({}) with unknown parent ({})". format(encode_hex(header.hash), encode_hex(header.parent_hash))) self.db.set( header.hash, rlp.encode(header), ) if header.parent_hash == GENESIS_PARENT_HASH: score = header.difficulty else: score = self.get_score(header.parent_hash) + header.difficulty self.db.set(make_block_hash_to_score_lookup_key(header.hash), rlp.encode(score, sedes=rlp.sedes.big_endian_int)) try: head_score = self.get_score(self.get_canonical_head().hash) except CanonicalHeadNotFound: new_headers = self._set_as_canonical_chain_head(header) else: if score > head_score: new_headers = self._set_as_canonical_chain_head(header) else: new_headers = [] return new_headers def persist_block_to_db(self, block): ''' Chain must do follow-up work to persist transactions to db ''' new_canonical_headers = self.persist_header_to_db(block.header) # Persist the transaction bodies transaction_db = HexaryTrie(self.db, root_hash=BLANK_ROOT_HASH) for i, transaction in enumerate(block.transactions): index_key = rlp.encode(i, sedes=rlp.sedes.big_endian_int) transaction_db[index_key] = rlp.encode(transaction) assert transaction_db.root_hash == block.header.transaction_root for header in new_canonical_headers: for index, transaction_hash in enumerate( self.get_block_transaction_hashes(header)): self._add_transaction_to_canonical_chain( transaction_hash, header, index) # Persist the uncles list self.db.set( block.header.uncles_hash, rlp.encode(block.uncles, sedes=rlp.sedes.CountableList(type(block.header))), ) def _remove_transaction_from_canonical_chain(self, transaction_hash): self.db.delete( make_transaction_hash_to_block_lookup_key(transaction_hash)) def _add_transaction_to_canonical_chain(self, transaction_hash, block_header, index): """ :param bytes transaction_hash: the hash of the transaction to add the lookup for :param block_header: The header of the block with the txn that is in the canonical chain :param int index: the position of the transaction in the block - add lookup from transaction hash to the block number and index that the body is stored at - remove transaction hash to body lookup in the pending pool """ transaction_key = TransactionKey(block_header.block_number, index) self.db.set( make_transaction_hash_to_block_lookup_key(transaction_hash), rlp.encode(transaction_key), ) # because transaction is now in canonical chain, can now remove from pending txn lookups lookup_key = make_transaction_hash_to_data_lookup_key(transaction_hash) if self.db.exists(lookup_key): self.db.delete(lookup_key) def add_pending_transaction(self, transaction): self.db.set( make_transaction_hash_to_data_lookup_key(transaction.hash), rlp.encode(transaction), ) def add_transaction(self, block_header, index_key, transaction): transaction_db = HexaryTrie(self.db, root_hash=block_header.transaction_root) transaction_db[index_key] = rlp.encode(transaction) return transaction_db.root_hash def add_receipt(self, block_header, index_key, receipt): receipt_db = HexaryTrie(db=self.db, root_hash=block_header.receipt_root) receipt_db[index_key] = rlp.encode(receipt) return receipt_db.root_hash def snapshot(self): return self.db.snapshot() def revert(self, checkpoint): self.db.revert(checkpoint) def commit(self, checkpoint): self.db.commit(checkpoint) def clear(self): self.db.clear() def get_state_db(self, state_root, read_only): return AccountStateDB(db=self.db, root_hash=state_root, read_only=read_only)
class BaseChainDB: def __init__(self, db): self.db = JournalDB(db) def exists(self, key): return self.db.exists(key) def get_canonical_head(self): if not self.exists(CANONICAL_HEAD_HASH_DB_KEY): raise CanonicalHeadNotFound("No canonical head set for this chain") return self.get_block_header_by_hash( self.db.get(CANONICAL_HEAD_HASH_DB_KEY)) def get_canonical_block_header_by_number(self, block_number): """ Returns the block header with the given number in the canonical chain. Raises BlockNotFound if there's no block header with the given number in the canonical chain. """ validate_uint256(block_number, title="Block Number") return self.get_block_header_by_hash( self.lookup_block_hash(block_number)) def get_score(self, block_hash): return rlp.decode(self.db.get( make_block_hash_to_score_lookup_key(block_hash)), sedes=rlp.sedes.big_endian_int) def set_as_canonical_chain_head(self, header): """ Sets the header as the canonical chain HEAD. """ for h in reversed(self.find_common_ancestor(header)): self.add_block_number_to_hash_lookup(h) try: self.get_block_header_by_hash(header.hash) except BlockNotFound: raise ValueError( "Cannot use unknown block hash as canonical head: {}".format( header.hash)) self.db.set(CANONICAL_HEAD_HASH_DB_KEY, header.hash) @to_tuple def find_common_ancestor(self, header): """ Returns the chain leading up from the given header until the first ancestor it has in common with our canonical chain. """ h = header while True: yield h if h.parent_hash == GENESIS_PARENT_HASH: break try: orig = self.get_canonical_block_header_by_number( h.block_number) except KeyError: # This just means the block is not on the canonical chain. pass else: if orig.hash == h.hash: # Found the common ancestor, stop. break h = self.get_block_header_by_hash(h.parent_hash) def get_block_header_by_hash(self, block_hash): """ Returns the requested block header as specified by block hash. Raises BlockNotFound if it is not present in the db. """ validate_word(block_hash, title="Block Hash") try: block = self.db.get(block_hash) except KeyError: raise BlockNotFound("No block with hash {0} found".format( encode_hex(block_hash))) return rlp.decode(block, sedes=BlockHeader) def header_exists(self, block_hash): """Returns True if the header with the given block hash is in our DB.""" return self.db.exists(block_hash) def lookup_block_hash(self, block_number): """ Return the block hash for the given block number. """ validate_uint256(block_number, title="Block Number") number_to_hash_key = make_block_number_to_hash_lookup_key(block_number) # TODO: can raise KeyError block_hash = rlp.decode( self.db.get(number_to_hash_key), sedes=rlp.sedes.binary, ) return block_hash def get_block_uncles(self, uncles_hash): validate_word(uncles_hash, title="Uncles Hash") return rlp.decode(self.db.get(uncles_hash), sedes=rlp.sedes.CountableList(BlockHeader)) @to_list def get_receipts(self, header, receipt_class): receipt_db = Trie(db=self.db, root_hash=header.receipt_root) for receipt_idx in itertools.count(): receipt_key = rlp.encode(receipt_idx) if receipt_key in receipt_db: receipt_data = receipt_db[receipt_key] yield rlp.decode(receipt_data, sedes=receipt_class) else: break @to_list def get_block_transactions(self, block_header, transaction_class): transaction_db = Trie(self.db, root_hash=block_header.transaction_root) for transaction_idx in itertools.count(): transaction_key = rlp.encode(transaction_idx) if transaction_key in transaction_db: transaction_data = transaction_db[transaction_key] yield rlp.decode(transaction_data, sedes=transaction_class) else: break def add_block_number_to_hash_lookup(self, header): block_number_to_hash_key = make_block_number_to_hash_lookup_key( header.block_number) self.db.set( block_number_to_hash_key, rlp.encode(header.hash, sedes=rlp.sedes.binary), ) # TODO: This method sould take a chain of headers as that's the most common use case # and it'd be much faster than inserting each header individually. def persist_header_to_db(self, header): if header.parent_hash != GENESIS_PARENT_HASH and not self.header_exists( header.parent_hash): raise ParentNotFound( "Cannot persist block header ({}) with unknown parent ({})". format(encode_hex(header.hash), encode_hex(header.parent_hash))) self.db.set( header.hash, rlp.encode(header), ) if header.parent_hash == GENESIS_PARENT_HASH: score = header.difficulty else: score = self.get_score(header.parent_hash) + header.difficulty self.db.set(make_block_hash_to_score_lookup_key(header.hash), rlp.encode(score, sedes=rlp.sedes.big_endian_int)) try: head_score = self.get_score(self.get_canonical_head().hash) except CanonicalHeadNotFound: self.set_as_canonical_chain_head(header) else: if score > head_score: self.set_as_canonical_chain_head(header) def persist_block_to_db(self, block): self.persist_header_to_db(block.header) # Persist the transactions transaction_db = Trie(self.db, root_hash=BLANK_ROOT_HASH) for i in range(len(block.transactions)): index_key = rlp.encode(i, sedes=rlp.sedes.big_endian_int) transaction_db[index_key] = rlp.encode(block.transactions[i]) assert transaction_db.root_hash == block.header.transaction_root # Persist the uncles list self.db.set( block.header.uncles_hash, rlp.encode(block.uncles, sedes=rlp.sedes.CountableList(type(block.header))), ) def add_transaction(self, block_header, index_key, transaction): transaction_db = Trie(self.db, root_hash=block_header.transaction_root) transaction_db[index_key] = rlp.encode(transaction) return transaction_db.root_hash def add_receipt(self, block_header, index_key, receipt): receipt_db = Trie(db=self.db, root_hash=block_header.receipt_root) receipt_db[index_key] = rlp.encode(receipt) return receipt_db.root_hash def snapshot(self): return self.db.snapshot() def revert(self, checkpoint): self.db.revert(checkpoint) def commit(self, checkpoint): self.db.commit(checkpoint) def clear(self): self.db.clear() def get_state_db(self, state_root, read_only): return State(db=self.db, root_hash=state_root, read_only=read_only)