def test_get_entry_cached_already(self) -> None: metadata = TxData(position=11, date_added=1, date_updated=1) flags = TxFlags.HasPosition def _read(*args, **kwargs) -> Tuple[bytes, Optional[bytes], TxFlags, TxData]: nonlocal metadata, flags return [(b"tx_hash", None, flags, metadata)] def _read_metadata(*args, **kwargs) -> Tuple[bytes, TxFlags, TxData]: nonlocal metadata, flags return [(b"tx_hash", flags, metadata)] mock_store = MockTransactionStore() mock_store.read = _read mock_store.read_metadata = _read_metadata cache = TransactionCache(mock_store, 0) # Verify that we do not hit the store for our cached entry. our_entry = TransactionCacheEntry(metadata, TxFlags.HasPosition) cache._cache[b"tx_hash"] = our_entry their_entry = cache.get_entry(b"tx_hash") assert our_entry.metadata == their_entry.metadata assert our_entry.flags == their_entry.flags
def test_add_missing_transaction(self): cache = TransactionCache(self.store) tx_bytes_1 = bytes.fromhex(tx_hex_1) tx_hash_1 = bitcoinx.double_sha256(tx_bytes_1) with SynchronousWriter() as writer: cache.add_missing_transaction( tx_hash_1, 100, 94, completion_callback=writer.get_callback()) assert writer.succeeded() assert cache.is_cached(tx_hash_1) entry = cache.get_entry(tx_hash_1) assert TxFlags.HasFee | TxFlags.HasHeight, entry.flags & TxFlags.METADATA_FIELD_MASK assert entry.bytedata is None tx_bytes_2 = bytes.fromhex(tx_hex_2) tx_hash_2 = bitcoinx.double_sha256(tx_bytes_2) with SynchronousWriter() as writer: cache.add_missing_transaction( tx_hash_2, 200, completion_callback=writer.get_callback()) assert writer.succeeded() assert cache.is_cached(tx_hash_2) entry = cache.get_entry(tx_hash_2) assert TxFlags.HasHeight == entry.flags & TxFlags.METADATA_FIELD_MASK assert entry.bytedata is None
def test_add_transaction_update(self): cache = TransactionCache(self.store) tx = Transaction.from_hex(tx_hex_1) data = [ tx.hash(), TxData(height=1295924, position=4, fee=None, date_added=1, date_updated=1), None, TxFlags.Unset, None ] with SynchronousWriter() as writer: cache.add([data], completion_callback=writer.get_callback()) assert writer.succeeded() entry = cache.get_entry(tx.hash()) assert entry is not None assert TxFlags.Unset == entry.flags & TxFlags.STATE_MASK with SynchronousWriter() as writer: cache.add_transaction(tx, TxFlags.StateCleared, completion_callback=writer.get_callback()) assert writer.succeeded() tx_hash = tx.hash() entry = cache.get_entry(tx_hash) assert entry is not None assert cache.have_transaction_data_cached(tx_hash) assert TxFlags.StateCleared == entry.flags & TxFlags.StateCleared
def test_add_then_update(self): cache = TransactionCache(self.store) bytedata_1 = bytes.fromhex(tx_hex_1) tx_hash_1 = bitcoinx.double_sha256(bytedata_1) metadata_1 = TxData(position=11) with SynchronousWriter() as writer: cache.add( [(tx_hash_1, metadata_1, bytedata_1, TxFlags.StateDispatched)], completion_callback=writer.get_callback()) assert writer.succeeded() assert cache.is_cached(tx_hash_1) entry = cache.get_entry(tx_hash_1) assert TxFlags.HasByteData | TxFlags.HasPosition | TxFlags.StateDispatched == entry.flags assert entry.bytedata is not None metadata_2 = TxData(fee=10, height=88) propagate_flags = TxFlags.HasFee | TxFlags.HasHeight with SynchronousWriter() as writer: cache.update([(tx_hash_1, metadata_2, None, propagate_flags | TxFlags.HasPosition)], completion_callback=writer.get_callback()) assert writer.succeeded() entry = cache.get_entry(tx_hash_1) expected_flags = propagate_flags | TxFlags.StateDispatched | TxFlags.HasByteData assert expected_flags == entry.flags, \ f"{TxFlags.to_repr(expected_flags)} != {TxFlags.to_repr(entry.flags)}" assert entry.bytedata is not None
def test_get_transaction(self): bytedata = bytes.fromhex(tx_hex_1) tx_hash = bitcoinx.double_sha256(bytedata) metadata = TxData(height=1, fee=2, position=None, date_added=1, date_updated=1) with SynchronousWriter() as writer: self.store.create([ (tx_hash, metadata, bytedata, TxFlags.Unset, None) ], completion_callback=writer.get_callback()) assert writer.succeeded() cache = TransactionCache(self.store) tx = cache.get_transaction(tx_hash) assert tx is not None assert tx_hash == tx.hash()
def test_get_unverified_entries_too_high(self): cache = TransactionCache(self.store) tx_1 = Transaction.from_hex(tx_hex_1) tx_hash_1 = tx_1.hash() data = TxData(height=11, position=22, date_added=1, date_updated=1) with SynchronousWriter() as writer: cache.add([ (tx_hash_1, data, tx_1, TxFlags.StateSettled, None) ], completion_callback=writer.get_callback()) assert writer.succeeded() results = cache.get_unverified_entries(100) assert 0 == len(results)
def test_entry_visible(self): cache = TransactionCache(self.store) combos = [ (TxFlags.Unset, None, None, True), (TxFlags.Unset, None, TxFlags.HasHeight, False), (TxFlags.HasHeight, None, TxFlags.HasHeight, True), (TxFlags.HasHeight, TxFlags.HasHeight, None, True), (TxFlags.HasHeight, TxFlags.HasHeight, TxFlags.HasFee, False), (TxFlags.HasHeight, TxFlags.HasHeight, TxFlags.HasHeight, True), (TxFlags.HasFee, TxFlags.HasHeight, TxFlags.HasHeight, False), ] for i, (flag_bits, flags, mask, result) in enumerate(combos): actual_result = cache._entry_visible(flag_bits, flags, mask) assert result == actual_result, str(combos[i])
def test_get_transactions(self): tx_hashes = [] for tx_hex in (tx_hex_1, tx_hex_2): tx_bytes = bytes.fromhex(tx_hex) tx_hash = bitcoinx.double_sha256(tx_bytes) data = TxData(height=1, fee=2, position=None, date_added=1, date_updated=1) with SynchronousWriter() as writer: self.store.create([ (tx_hash, data, tx_bytes, TxFlags.Unset, None) ], completion_callback=writer.get_callback()) assert writer.succeeded() tx_hashes.append(tx_hash) cache = TransactionCache(self.store) for (tx_hash, tx) in cache.get_transactions(tx_hashes=tx_hashes): assert tx is not None assert tx_hash in tx_hashes
def test_get_entry_cached_on_demand(self) -> None: metadata = TxData(position=11, date_added=1, date_updated=1) flags = TxFlags.HasPosition def _read(*args, **kwargs) -> Tuple[bytes, Optional[bytes], TxFlags, TxData]: nonlocal metadata, flags return [ (b"tx_hash", None, flags, metadata) ] def _read_metadata(*args, **kwargs) -> Tuple[bytes, TxFlags, TxData]: nonlocal metadata, flags return [ (b"tx_hash", flags, metadata) ] mock_store = MockTransactionStore() mock_store.read = _read mock_store.read_metadata = _read_metadata cache = TransactionCache(mock_store, 0) their_entry = cache.get_entry(b"tx_hash") assert their_entry.metadata == metadata assert their_entry.flags == flags
def test_get_metadata(self): # Full entry caching for non-settled transactions, otherwise only metadata. bytedata_set_1 = bytes.fromhex(tx_hex_1) tx_hash_1 = bitcoinx.double_sha256(bytedata_set_1) metadata_set_1 = TxData(height=None, fee=2, position=None, date_added=1, date_updated=1) bytedata_set_2 = bytes.fromhex(tx_hex_2) tx_hash_2 = bitcoinx.double_sha256(bytedata_set_2) metadata_set_2 = TxData(height=1, fee=2, position=10, date_added=1, date_updated=1) with SynchronousWriter() as writer: self.store.create([ (tx_hash_1, metadata_set_1, bytedata_set_1, TxFlags.Unset, None), (tx_hash_2, metadata_set_2, bytedata_set_2, TxFlags.StateSettled, None), ], completion_callback=writer.get_callback()) assert writer.succeeded() cache = TransactionCache(self.store) metadata_get = cache.get_metadata(tx_hash_1) assert metadata_set_1.height == metadata_get.height assert metadata_set_1.fee == metadata_get.fee assert metadata_set_1.position == metadata_get.position metadata_get = cache.get_metadata(tx_hash_2) assert metadata_set_2.height == metadata_get.height assert metadata_set_2.fee == metadata_get.fee assert metadata_set_2.position == metadata_get.position entry = cache._cache[tx_hash_1] assert cache.have_transaction_data_cached(tx_hash_1) entry = cache._cache[tx_hash_2] assert not cache.have_transaction_data_cached(tx_hash_2)
def test_get_transaction_after_metadata(self): # Getting an entry for a settled transaction should update from metadata-only to full. bytedata_set = bytes.fromhex(tx_hex_1) tx_hash = bitcoinx.double_sha256(bytedata_set) metadata_set = TxData(height=1, fee=2, position=None, date_added=1, date_updated=1) with SynchronousWriter() as writer: self.store.create([ (tx_hash, metadata_set, bytedata_set, TxFlags.StateSettled, None) ], completion_callback=writer.get_callback()) assert writer.succeeded() cache = TransactionCache(self.store) metadata_get = cache.get_metadata(tx_hash) assert metadata_get is not None # Initial priming of cache will be only metadata. cached_entry_1 = cache._cache[tx_hash] assert not cache.have_transaction_data_cached(tx_hash) # Entry request will hit the database. entry = cache.get_entry(tx_hash) assert cache.have_transaction_data_cached(tx_hash) cached_entry_2 = cache._cache[tx_hash] assert entry.metadata == cached_entry_2.metadata assert entry.flags == cached_entry_2.flags
def test_add_transaction(self): cache = TransactionCache(self.store) tx = Transaction.from_hex(tx_hex_1) tx_hash = tx.hash() with SynchronousWriter() as writer: cache.add_transaction(tx_hash, tx, completion_callback=writer.get_callback()) assert writer.succeeded() assert cache.is_cached(tx_hash) entry = cache.get_entry(tx_hash) assert TxFlags.HasByteData == entry.flags & TxFlags.HasByteData assert cache.have_transaction_data_cached(tx_hash)
def test_uncleared_bytedata_requirements(self) -> None: cache = TransactionCache(self.store) tx_1 = Transaction.from_hex(tx_hex_1) tx_hash_1 = tx_1.hash() data = TxData(position=11) for state_flag in TRANSACTION_FLAGS: with pytest.raises(wallet_database.InvalidDataError): cache.add([ (tx_hash_1, data, None, state_flag, None) ]) with SynchronousWriter() as writer: cache.add([ (tx_hash_1, data, tx_1, TxFlags.StateSigned, None) ], completion_callback=writer.get_callback()) assert writer.succeeded() # We are applying a clearing of the bytedata, this should be invalid given uncleared. for state_flag in TRANSACTION_FLAGS: with pytest.raises(wallet_database.InvalidDataError): cache.update([ (tx_hash_1, data, None, state_flag | TxFlags.HasByteData) ])
def test_get_flags(self): cache = TransactionCache(self.store) assert cache.get_flags(os.urandom(10).hex()) is None tx_1 = Transaction.from_hex(tx_hex_1) tx_hash_1 = tx_1.hash() data = TxData(position=11) with SynchronousWriter() as writer: cache.add([ (tx_hash_1, data, tx_1, TxFlags.StateDispatched, None) ], completion_callback=writer.get_callback()) assert writer.succeeded() assert cache.is_cached(tx_hash_1) assert TxFlags.StateDispatched | TxFlags.HasByteData | TxFlags.HasPosition == \ cache.get_flags(tx_hash_1)
def test_delete(self): cache = TransactionCache(self.store) tx_1 = Transaction.from_hex(tx_hex_1) tx_hash_1 = tx_1.hash() data = TxData(position=11) with SynchronousWriter() as writer: cache.add([ (tx_hash_1, data, tx_1, TxFlags.StateDispatched, None) ], completion_callback=writer.get_callback()) assert writer.succeeded() assert len(self.store.read_metadata(tx_hashes=[ tx_hash_1 ])) assert cache.is_cached(tx_hash_1) with SynchronousWriter() as writer: cache.delete(tx_hash_1, completion_callback=writer.get_callback()) assert writer.succeeded() assert not len(self.store.read_metadata(tx_hashes=[ tx_hash_1 ])) assert not cache.is_cached(tx_hash_1)
def test_update_or_add(self): cache = TransactionCache(self.store) # Add. bytedata_1 = bytes.fromhex(tx_hex_1) tx_hash_1 = bitcoinx.double_sha256(bytedata_1) metadata_1 = TxData() with SynchronousWriter() as writer: cache.update_or_add( [(tx_hash_1, metadata_1, bytedata_1, TxFlags.StateSettled)], completion_callback=writer.get_callback()) assert writer.succeeded() assert cache.is_cached(tx_hash_1) entry = cache.get_entry(tx_hash_1) assert TxFlags.HasByteData | TxFlags.StateSettled == entry.flags assert entry.bytedata is not None # Update. metadata_2 = TxData(position=22) with SynchronousWriter() as writer: updated_ids = cache.update_or_add( [(tx_hash_1, metadata_2, None, TxFlags.HasPosition | TxFlags.StateDispatched)], completion_callback=writer.get_callback()) assert writer.succeeded() entry = cache.get_entry(tx_hash_1) _tx_hash, store_flags, _metadata = self.store.read_metadata( tx_hashes=[tx_hash_1])[0] # State flags if present get set in an update otherwise they remain the same. expected_flags = TxFlags.HasPosition | TxFlags.HasByteData | TxFlags.StateDispatched assert expected_flags == store_flags, \ f"{TxFlags.to_repr(expected_flags)} != {TxFlags.to_repr(store_flags)}" assert expected_flags == entry.flags, \ f"{TxFlags.to_repr(expected_flags)} != {TxFlags.to_repr(entry.flags)}" assert bytedata_1 == entry.bytedata assert metadata_2.position == entry.metadata.position assert updated_ids == set([tx_hash_1])
def test_get_unsynced_hashes(self): cache = TransactionCache(self.store) tx_1 = Transaction.from_hex(tx_hex_1) tx_hash_1 = tx_1.hash() metadata_1 = TxData(height=11) with SynchronousWriter() as writer: cache.add([ (tx_hash_1, metadata_1, None, TxFlags.Unset, None) ], completion_callback=writer.get_callback()) assert writer.succeeded() results = cache.get_unsynced_hashes() assert 1 == len(results) metadata_2 = TxData() with SynchronousWriter() as writer: cache.update([ (tx_hash_1, metadata_2, tx_1, TxFlags.HasByteData) ], completion_callback=writer.get_callback()) assert writer.succeeded() results = cache.get_unsynced_hashes() assert 0 == len(results)
def test_get_entry(self): cache = TransactionCache(self.store) bytedata_1 = bytes.fromhex(tx_hex_1) tx_hash_1 = bitcoinx.double_sha256(bytedata_1) data = TxData(position=11) with SynchronousWriter() as writer: cache.add([(tx_hash_1, data, bytedata_1, TxFlags.StateSettled)], completion_callback=writer.get_callback()) assert writer.succeeded() entry = cache.get_entry(tx_hash_1, TxFlags.StateDispatched) assert entry is None entry = cache.get_entry(tx_hash_1, TxFlags.StateSettled) assert entry is not None
def test_get_entry(self): cache = TransactionCache(self.store) tx_1 = Transaction.from_hex(tx_hex_1) tx_hash_1 = tx_1.hash() data = TxData(position=11) with SynchronousWriter() as writer: cache.add([ (tx_hash_1, data, tx_1, TxFlags.StateSettled, None) ], completion_callback=writer.get_callback()) assert writer.succeeded() entry = cache.get_entry(tx_hash_1, TxFlags.StateDispatched) assert entry is None entry = cache.get_entry(tx_hash_1, TxFlags.StateSettled) assert entry is not None
def test_get_unverified_entries(self) -> None: cache = TransactionCache(self.store) tx_bytes_1 = bytes.fromhex(tx_hex_1) tx_hash_1 = bitcoinx.double_sha256(tx_bytes_1) data = TxData(height=11, date_added=1, date_updated=1) with SynchronousWriter() as writer: cache.add([(tx_hash_1, data, tx_bytes_1, TxFlags.StateSettled)], completion_callback=writer.get_callback()) assert writer.succeeded() results = cache.get_unverified_entries(10) assert 0 == len(results) results = cache.get_unverified_entries(11) assert 1 == len(results)
def test_apply_reorg(self) -> None: common_height = 5 cache = TransactionCache(self.store) # Add the transaction that should be reset back to settled, with data fields cleared. tx_y1 = Transaction.from_hex(tx_hex_1) tx_hash_y1 = tx_y1.hash() data_y1 = TxData(height=common_height+1, position=33, fee=44, date_added=1, date_updated=1) with SynchronousWriter() as writer: cache.add([ (tx_hash_y1, data_y1, tx_y1, TxFlags.StateSettled, None) ], completion_callback=writer.get_callback()) assert writer.succeeded() # Add the transaction that would be reset but is below the common height. tx_n1 = Transaction.from_hex(tx_hex_2) tx_hash_n1 = tx_n1.hash() data_n1 = TxData(height=common_height-1, position=33, fee=44, date_added=1, date_updated=1) with SynchronousWriter() as writer: cache.add([ (tx_hash_n1, data_n1, tx_n1, TxFlags.StateSettled, None) ], completion_callback=writer.get_callback()) assert writer.succeeded() # Add the transaction that would be reset but is the common height. tx_n2 = Transaction.from_hex(tx_hex_3) tx_hash_n2 = tx_n2.hash() data_n2 = TxData(height=common_height, position=33, fee=44, date_added=1, date_updated=1) with SynchronousWriter() as writer: cache.add([ (tx_hash_n2, data_n2, tx_n2, TxFlags.StateSettled, None) ], completion_callback=writer.get_callback()) assert writer.succeeded() # Add a canary transaction that should remain untouched due to non-cleared state. tx_n3 = Transaction.from_hex(tx_hex_4) tx_hash_n3 = tx_n3.hash() data_n3 = TxData(height=111, position=333, fee=444, date_added=1, date_updated=1) with SynchronousWriter() as writer: cache.add([ (tx_hash_n3, data_n3, tx_n3, TxFlags.StateDispatched, None) ], completion_callback=writer.get_callback()) assert writer.succeeded() # Delete as if a reorg happened above the suitable but excluded canary transaction. with SynchronousWriter() as writer: cache.apply_reorg(5, completion_callback=writer.get_callback()) assert writer.succeeded() metadata_entries = cache.get_entries(TxFlags.HasByteData, TxFlags.HasByteData) assert 4 == len(metadata_entries) # Affected, canary above common height. y1 = [ m[1] for m in metadata_entries if m[0] == tx_hash_y1 ][0] assert 0 == y1.metadata.height assert None is y1.metadata.position assert data_y1.fee == y1.metadata.fee assert TxFlags.StateCleared | TxFlags.HasByteData | TxFlags.HasFee == y1.flags, \ TxFlags.to_repr(y1.flags) expected_flags = (TxFlags.HasByteData | TxFlags.HasFee | TxFlags.HasHeight | TxFlags.HasPosition) # Skipped, old enough to survive. n1 = [ m[1] for m in metadata_entries if m[0] == tx_hash_n1 ][0] assert data_n1.height == n1.metadata.height assert data_n1.position == n1.metadata.position assert data_n1.fee == n1.metadata.fee assert TxFlags.StateSettled | expected_flags == n1.flags, TxFlags.to_repr(n1.flags) # Skipped, canary common height. n2 = [ m[1] for m in metadata_entries if m[0] == tx_hash_n2 ][0] assert data_n2.height == n2.metadata.height assert data_n2.position == n2.metadata.position assert data_n2.fee == n2.metadata.fee assert TxFlags.StateSettled | expected_flags == n2.flags, TxFlags.to_repr(n2.flags) # Skipped, canary non-cleared. n3 = [ m[1] for m in metadata_entries if m[0] == tx_hash_n3 ][0] assert data_n3.height == n3.metadata.height assert data_n3.position == n3.metadata.position assert data_n3.fee == n3.metadata.fee assert TxFlags.StateDispatched | expected_flags == n3.flags, TxFlags.to_repr(n3.flags)
def test_get_height(self): cache = TransactionCache(self.store) tx_1 = Transaction.from_hex(tx_hex_1) tx_hash_1 = tx_1.hash() metadata_1 = TxData(height=11) with SynchronousWriter() as writer: cache.add([ (tx_hash_1, metadata_1, tx_1, TxFlags.StateSettled, None) ], completion_callback=writer.get_callback()) assert writer.succeeded() assert 11 == cache.get_height(tx_hash_1) cache.update_flags(tx_hash_1, TxFlags.StateCleared, TxFlags.HasByteData) assert 11 == cache.get_height(tx_hash_1) cache.update_flags(tx_hash_1, TxFlags.StateReceived, TxFlags.HasByteData) assert None is cache.get_height(tx_hash_1)
def test_get_height(self): cache = TransactionCache(self.store) bytedata_1 = bytes.fromhex(tx_hex_1) tx_hash_1 = bitcoinx.double_sha256(bytedata_1) metadata_1 = TxData(height=11) with SynchronousWriter() as writer: cache.add( [(tx_hash_1, metadata_1, bytedata_1, TxFlags.StateSettled)], completion_callback=writer.get_callback()) assert writer.succeeded() assert 11 == cache.get_height(tx_hash_1) cache.update_flags(tx_hash_1, TxFlags.StateCleared, TxFlags.HasByteData) assert 11 == cache.get_height(tx_hash_1) cache.update_flags(tx_hash_1, TxFlags.StateReceived, TxFlags.HasByteData) assert cache.get_height(tx_hash_1) is None
def test_add_then_update(self): cache = TransactionCache(self.store) tx_1 = Transaction.from_hex(tx_hex_1) tx_hash_1 = tx_1.hash() metadata_1 = TxData(position=11) with SynchronousWriter() as writer: cache.add([ (tx_hash_1, metadata_1, tx_1, TxFlags.StateDispatched, None) ], completion_callback=writer.get_callback()) assert writer.succeeded() assert cache.is_cached(tx_hash_1) entry = cache.get_entry(tx_hash_1) assert TxFlags.HasByteData | TxFlags.HasPosition | TxFlags.StateDispatched == entry.flags assert cache.have_transaction_data_cached(tx_hash_1) # NOTE: We are not updating bytedata, and it should remain the same. The flags we pass # into update are treated specially to achieve this. metadata_2 = TxData(fee=10, height=88) propagate_flags = TxFlags.HasFee | TxFlags.HasHeight with SynchronousWriter() as writer: cache.update([ (tx_hash_1, metadata_2, None, propagate_flags | TxFlags.HasPosition) ], completion_callback=writer.get_callback()) assert writer.succeeded() # Check the cache to see that the flags are correct and that bytedata is cached. entry = cache.get_entry(tx_hash_1) expected_flags = propagate_flags | TxFlags.StateDispatched | TxFlags.HasByteData assert expected_flags == entry.flags, \ f"{TxFlags.to_repr(expected_flags)} != {TxFlags.to_repr(entry.flags)}" assert cache.have_transaction_data_cached(tx_hash_1) # Check the store to see that the flags are correct and the bytedata is retained. rows = self.store.read(tx_hashes=[tx_hash_1]) assert 1 == len(rows) get_tx_hash, bytedata_get, flags_get, metadata_get = rows[0] assert tx_1.to_bytes() == bytedata_get assert flags_get & TxFlags.HasByteData != 0