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
示例#2
0
    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
示例#4
0
    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
示例#5
0
    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()
示例#6
0
    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)
示例#7
0
    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])
示例#8
0
    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
示例#9
0
    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
示例#10
0
    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)
示例#11
0
    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
示例#12
0
    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)
示例#13
0
    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) ])
示例#14
0
    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)
示例#15
0
    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)
示例#16
0
    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])
示例#17
0
    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)
示例#18
0
    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
示例#19
0
    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
示例#20
0
    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)
示例#21
0
    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)
示例#22
0
    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)
示例#23
0
    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
示例#24
0
    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