|
3 | 3 | import asyncio |
4 | 4 | import logging |
5 | 5 | import random |
6 | | -from dataclasses import dataclass |
| 6 | +from dataclasses import dataclass, field |
7 | 7 | from typing import cast |
8 | 8 |
|
9 | 9 | import pytest |
10 | 10 | from chia_rs.sized_bytes import bytes32 |
| 11 | +from chia_rs.sized_ints import uint64 |
11 | 12 |
|
12 | | -from chia.full_node.tx_processing_queue import TransactionQueue, TransactionQueueEntry, TransactionQueueFull |
| 13 | +from chia.full_node.tx_processing_queue import PeerWithTx, TransactionQueue, TransactionQueueEntry, TransactionQueueFull |
13 | 14 | from chia.util.task_referencer import create_referenced_task |
14 | 15 |
|
15 | 16 | log = logging.getLogger(__name__) |
16 | 17 |
|
17 | 18 |
|
18 | 19 | @dataclass(frozen=True) |
19 | 20 | class FakeTransactionQueueEntry: |
20 | | - index: int |
21 | | - peer_id: bytes32 | None |
| 21 | + index: int = field(compare=False) |
| 22 | + peer_id: bytes32 | None = field(compare=False) |
| 23 | + peers_with_tx: dict[bytes32, PeerWithTx] | None = field(compare=False) |
22 | 24 |
|
23 | 25 |
|
24 | | -def get_transaction_queue_entry(peer_id: bytes32 | None, tx_index: int) -> TransactionQueueEntry: # easy shortcut |
25 | | - return cast(TransactionQueueEntry, FakeTransactionQueueEntry(index=tx_index, peer_id=peer_id)) |
| 26 | +def get_transaction_queue_entry( |
| 27 | + peer_id: bytes32 | None, tx_index: int, peers_with_tx: dict[bytes32, PeerWithTx] | None = None |
| 28 | +) -> TransactionQueueEntry: # easy shortcut |
| 29 | + if peers_with_tx is None: |
| 30 | + peers_with_tx = {} |
| 31 | + return cast(TransactionQueueEntry, FakeTransactionQueueEntry(tx_index, peer_id, peers_with_tx)) |
26 | 32 |
|
27 | 33 |
|
28 | 34 | @pytest.mark.anyio |
@@ -135,20 +141,79 @@ async def test_queue_cleanup_and_fairness(seeded_random: random.Random) -> None: |
135 | 141 | peer_b = bytes32.random(seeded_random) |
136 | 142 | peer_c = bytes32.random(seeded_random) |
137 | 143 |
|
| 144 | + higher_tx_cost = uint64(20) |
| 145 | + lower_tx_cost = uint64(10) |
| 146 | + higher_tx_fee = uint64(5) |
| 147 | + lower_tx_fee = uint64(1) |
138 | 148 | # 2 for a, 1 for b, 2 for c |
139 | | - peer_tx_a = [get_transaction_queue_entry(peer_a, i) for i in range(2)] |
140 | | - peer_tx_b = [get_transaction_queue_entry(peer_b, 0)] |
141 | | - peer_tx_c = [get_transaction_queue_entry(peer_c, i) for i in range(2)] |
| 149 | + peer_tx_a = [ |
| 150 | + get_transaction_queue_entry(peer_a, 0, {peer_a: PeerWithTx(str(peer_a), lower_tx_fee, higher_tx_cost)}), |
| 151 | + get_transaction_queue_entry(peer_a, 1, {peer_a: PeerWithTx(str(peer_a), higher_tx_fee, lower_tx_cost)}), |
| 152 | + ] |
| 153 | + peer_tx_b = [ |
| 154 | + get_transaction_queue_entry(peer_b, 0, {peer_b: PeerWithTx(str(peer_b), higher_tx_fee, lower_tx_cost)}) |
| 155 | + ] |
| 156 | + peer_tx_c = [ |
| 157 | + get_transaction_queue_entry(peer_c, 0, {peer_c: PeerWithTx(str(peer_c), higher_tx_fee, lower_tx_cost)}), |
| 158 | + get_transaction_queue_entry(peer_c, 1, {peer_c: PeerWithTx(str(peer_c), lower_tx_fee, higher_tx_cost)}), |
| 159 | + ] |
142 | 160 |
|
143 | 161 | list_txs = peer_tx_a + peer_tx_b + peer_tx_c |
144 | 162 | for tx in list_txs: |
145 | 163 | transaction_queue.put(tx, tx.peer_id) # type: ignore[attr-defined] |
146 | 164 |
|
147 | | - resulting_ids = [] |
| 165 | + entries = [] |
148 | 166 | for _ in range(3): # we validate we get one transaction per peer |
149 | | - resulting_ids.append((await transaction_queue.pop()).peer_id) # type: ignore[attr-defined] |
150 | | - assert [peer_a, peer_b, peer_c] == resulting_ids # all peers have been properly included in the queue. |
151 | | - second_resulting_ids = [] |
| 167 | + entry = await transaction_queue.pop() |
| 168 | + entries.append((entry.peer_id, entry.index)) # type: ignore[attr-defined] |
| 169 | + assert [(peer_a, 1), (peer_b, 0), (peer_c, 0)] == entries # all peers have been properly included in the queue. |
| 170 | + second_entries = [] |
152 | 171 | for _ in range(2): # we validate that we properly queue the last 2 transactions |
153 | | - second_resulting_ids.append((await transaction_queue.pop()).peer_id) # type: ignore[attr-defined] |
154 | | - assert [peer_a, peer_c] == second_resulting_ids |
| 172 | + entry = await transaction_queue.pop() |
| 173 | + second_entries.append((entry.peer_id, entry.index)) # type: ignore[attr-defined] |
| 174 | + assert [(peer_a, 0), (peer_c, 1)] == second_entries |
| 175 | + |
| 176 | + |
| 177 | +@pytest.mark.anyio |
| 178 | +async def test_peer_queue_prioritization_fallback() -> None: |
| 179 | + """ |
| 180 | + Tests prioritization fallback, when `peer_id` is not in `peers_with_tx` and |
| 181 | + we compute the fee per cost (for priority) using values from the peer with |
| 182 | + the highest advertised cost, even if that results in a lower fee per cost. |
| 183 | + """ |
| 184 | + queue = TransactionQueue(42, log) |
| 185 | + peer1 = bytes32.random() |
| 186 | + peer2 = bytes32.random() |
| 187 | + # We'll be using this peer to test the fallback, so we don't include it in |
| 188 | + # peers with transactions maps. |
| 189 | + peer3 = bytes32.random() |
| 190 | + peers_with_tx1 = { |
| 191 | + # This has FPC of 5.0 |
| 192 | + peer1: PeerWithTx(str(peer1), uint64(10), uint64(2)), |
| 193 | + # This has FPC of 2.0 but higher advertised cost |
| 194 | + peer2: PeerWithTx(str(peer2), uint64(20), uint64(10)), |
| 195 | + } |
| 196 | + tx1 = get_transaction_queue_entry(peer3, 0, peers_with_tx1) |
| 197 | + queue.put(tx1, peer3) |
| 198 | + peers_with_tx2 = { |
| 199 | + # This has FPC of 3.0 |
| 200 | + peer1: PeerWithTx(str(peer1), uint64(30), uint64(10)), |
| 201 | + # This has FPC of 4.0 but lower advertised cost |
| 202 | + peer2: PeerWithTx(str(peer2), uint64(20), uint64(5)), |
| 203 | + } |
| 204 | + tx2 = get_transaction_queue_entry(peer3, 1, peers_with_tx2) |
| 205 | + queue.put(tx2, peer3) |
| 206 | + tx3 = get_transaction_queue_entry(peer3, 2, {}) |
| 207 | + queue.put(tx3, peer3) |
| 208 | + # tx2 gets top priority with FPC 3.0 instead of 4.0 due to higher cost fallback |
| 209 | + assert queue._queue_dict[peer3].queue[0][0] == -3.0 |
| 210 | + entry = await queue.pop() |
| 211 | + assert entry.index == 1 # type: ignore[attr-defined] |
| 212 | + # tx1 comes next with FPC 2.0 instead of 5.0 due to higher cost fallback |
| 213 | + assert queue._queue_dict[peer3].queue[0][0] == -2.0 |
| 214 | + entry = await queue.pop() |
| 215 | + assert entry.index == 0 # type: ignore[attr-defined] |
| 216 | + # tx3 comes next with infinity priority due to no `peers_with_tx` |
| 217 | + assert queue._queue_dict[peer3].queue[0][0] == float("inf") |
| 218 | + entry = await queue.pop() |
| 219 | + assert entry.index == 2 # type: ignore[attr-defined] |
0 commit comments