Skip to content

Commit 2d4594a

Browse files
committed
Implement round trip API for fetching receipts
1 parent ad21f73 commit 2d4594a

File tree

15 files changed

+554
-125
lines changed

15 files changed

+554
-125
lines changed

eth/db/trie.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
import functools
2-
from typing import Dict, List, Tuple, Union
2+
from typing import Dict, Tuple, Union
33

44
import rlp
55
from trie import (
66
HexaryTrie,
77
)
88

9+
from eth_typing import Hash32
10+
911
from eth.constants import (
1012
BLANK_ROOT_HASH,
1113
)
1214
from eth.rlp.receipts import Receipt
1315
from eth.rlp.transactions import BaseTransaction
1416

17+
TransactionsOrReceipts = Union[Tuple[Receipt, ...], Tuple[BaseTransaction, ...]]
18+
TrieRootAndData = Tuple[Hash32, Dict[Hash32, bytes]]
19+
1520

16-
def make_trie_root_and_nodes(
17-
items: Union[List[Receipt], List[BaseTransaction]]) -> Tuple[bytes, Dict[bytes, bytes]]:
21+
def make_trie_root_and_nodes(items: TransactionsOrReceipts) -> TrieRootAndData:
1822
return _make_trie_root_and_nodes(tuple(rlp.encode(item) for item in items))
1923

2024

@@ -23,8 +27,8 @@ def make_trie_root_and_nodes(
2327
# as it's common for them to have duplicate receipt_roots. Given that, it probably makes sense to
2428
# use a relatively small cache size here.
2529
@functools.lru_cache(128)
26-
def _make_trie_root_and_nodes(items: Tuple[bytes, ...]) -> Tuple[bytes, Dict[bytes, bytes]]:
27-
kv_store = {} # type: Dict[bytes, bytes]
30+
def _make_trie_root_and_nodes(items: Tuple[bytes, ...]) -> TrieRootAndData:
31+
kv_store = {} # type: Dict[Hash32, bytes]
2832
trie = HexaryTrie(kv_store, BLANK_ROOT_HASH)
2933
with trie.squash_changes() as memory_trie:
3034
for index, item in enumerate(items):

p2p/peer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,8 +1067,8 @@ async def request_stuff() -> None:
10671067
hashes = [header.hash for header in headers]
10681068
if peer_class == ETHPeer:
10691069
peer = cast(ETHPeer, peer)
1070-
peer.sub_proto.send_get_block_bodies(hashes)
1071-
peer.sub_proto.send_get_receipts(hashes)
1070+
peer.sub_proto._send_get_block_bodies(hashes)
1071+
peer.sub_proto._send_get_receipts(hashes)
10721072
else:
10731073
peer = cast(LESPeer, peer)
10741074
request_id = 1

tests/trinity/core/p2p-proto/test_node_data_request_object.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,28 @@ def test_node_data_request_empty_response_is_valid():
2828
node_keys, _ = mk_node_data(10)
2929
request = NodeDataRequest(node_keys)
3030

31-
request.validate_response(tuple())
31+
request.validate_response(tuple(), tuple())
3232

3333

3434
def test_node_data_request_with_full_response():
3535
node_keys, nodes = mk_node_data(10)
3636
request = NodeDataRequest(node_keys)
3737
node_data = tuple(zip(node_keys, nodes))
3838

39-
request.validate_response(node_data)
39+
request.validate_response(nodes, node_data)
4040

4141

4242
def test_node_data_request_with_partial_response():
4343
node_keys, nodes = mk_node_data(10)
4444
request = NodeDataRequest(node_keys)
4545
node_data = tuple(zip(node_keys, nodes))
4646

47-
request.validate_response(node_data[3:])
48-
request.validate_response(node_data[:3])
49-
request.validate_response((node_data[1], node_data[8], node_data[4]))
47+
request.validate_response(nodes[3:], node_data[3:])
48+
request.validate_response(nodes[:3], node_data[:3])
49+
request.validate_response(
50+
(nodes[1], nodes[8], nodes[4]),
51+
(node_data[1], node_data[8], node_data[4]),
52+
)
5053

5154

5255
def test_node_data_request_with_fully_invalid_response():
@@ -58,7 +61,7 @@ def test_node_data_request_with_fully_invalid_response():
5861
other_node_data = tuple((keccak(node), node) for node in other_nodes)
5962

6063
with pytest.raises(ValidationError):
61-
request.validate_response(other_node_data)
64+
request.validate_response(other_nodes, other_node_data)
6265

6366

6467
def test_node_data_request_with_extra_unrequested_nodes():
@@ -71,4 +74,7 @@ def test_node_data_request_with_extra_unrequested_nodes():
7174
other_node_data = tuple((keccak(node), node) for node in other_nodes)
7275

7376
with pytest.raises(ValidationError):
74-
request.validate_response(node_data + other_node_data)
77+
request.validate_response(
78+
nodes + other_nodes,
79+
node_data + other_node_data,
80+
)
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import asyncio
2+
import os
3+
import time
4+
5+
import pytest
6+
7+
from eth_utils import to_tuple
8+
9+
from eth.db.trie import make_trie_root_and_nodes
10+
from eth.rlp.headers import BlockHeader
11+
from eth.rlp.receipts import Receipt
12+
13+
from trinity.protocol.eth.peer import ETHPeer
14+
15+
from tests.trinity.core.peer_helpers import (
16+
get_directly_linked_peers,
17+
)
18+
19+
20+
@pytest.fixture
21+
async def eth_peer_and_remote(request, event_loop):
22+
peer, remote = await get_directly_linked_peers(
23+
request,
24+
event_loop,
25+
peer1_class=ETHPeer,
26+
peer2_class=ETHPeer,
27+
)
28+
return peer, remote
29+
30+
31+
@to_tuple
32+
def mk_receipts(num_receipts):
33+
for _ in range(num_receipts):
34+
yield Receipt(
35+
state_root=os.urandom(32),
36+
gas_used=21000,
37+
bloom=0,
38+
logs=[],
39+
)
40+
41+
42+
def mk_header_and_receipts(block_number, num_receipts):
43+
receipts = mk_receipts(num_receipts)
44+
root_hash, trie_root_and_data = make_trie_root_and_nodes(receipts)
45+
header = BlockHeader(
46+
difficulty=1000000,
47+
block_number=block_number,
48+
gas_limit=3141592,
49+
timestamp=int(time.time()),
50+
receipt_root=root_hash,
51+
)
52+
return header, receipts, (root_hash, trie_root_and_data)
53+
54+
55+
@to_tuple
56+
def mk_headers(*counts):
57+
for idx, num_receipts in enumerate(counts, 1):
58+
yield mk_header_and_receipts(idx, num_receipts)
59+
60+
61+
@pytest.mark.asyncio
62+
async def test_eth_peer_get_receipts_round_trip_with_full_response(eth_peer_and_remote):
63+
peer, remote = eth_peer_and_remote
64+
65+
headers_bundle = mk_headers(1, 3, 2, 5, 4)
66+
headers, receipts, trie_roots_and_data = zip(*headers_bundle)
67+
receipts_bundle = tuple(zip(receipts, trie_roots_and_data))
68+
69+
async def send_receipts():
70+
remote.sub_proto.send_receipts(receipts)
71+
await asyncio.sleep(0)
72+
73+
asyncio.ensure_future(send_receipts())
74+
response = await peer.requests.get_receipts(headers)
75+
76+
assert len(response) == len(headers)
77+
assert response == receipts_bundle
78+
79+
80+
@pytest.mark.asyncio
81+
async def test_eth_peer_get_receipts_round_trip_with_partial_response(eth_peer_and_remote):
82+
peer, remote = eth_peer_and_remote
83+
84+
headers_bundle = mk_headers(1, 3, 2, 5, 4)
85+
headers, receipts, trie_roots_and_data = zip(*headers_bundle)
86+
receipts_bundle = tuple(zip(receipts, trie_roots_and_data))
87+
88+
async def send_receipts():
89+
remote.sub_proto.send_receipts((receipts[2], receipts[1], receipts[4]))
90+
await asyncio.sleep(0)
91+
92+
asyncio.ensure_future(send_receipts())
93+
response = await peer.requests.get_receipts(headers)
94+
95+
assert len(response) == 3
96+
assert response == (receipts_bundle[2], receipts_bundle[1], receipts_bundle[4])
97+
98+
99+
@pytest.mark.asyncio
100+
async def test_eth_peer_get_receipts_round_trip_with_noise(eth_peer_and_remote):
101+
peer, remote = eth_peer_and_remote
102+
103+
headers_bundle = mk_headers(1, 3, 2, 5, 4)
104+
headers, receipts, trie_roots_and_data = zip(*headers_bundle)
105+
receipts_bundle = tuple(zip(receipts, trie_roots_and_data))
106+
107+
async def send_receipts():
108+
remote.sub_proto.send_transactions([])
109+
await asyncio.sleep(0)
110+
remote.sub_proto.send_receipts(receipts)
111+
await asyncio.sleep(0)
112+
remote.sub_proto.send_transactions([])
113+
await asyncio.sleep(0)
114+
115+
asyncio.ensure_future(send_receipts())
116+
response = await peer.requests.get_receipts(headers)
117+
118+
assert len(response) == len(headers)
119+
assert response == receipts_bundle
120+
121+
122+
@pytest.mark.asyncio
123+
async def test_eth_peer_get_receipts_round_trip_no_match_invalid_response(eth_peer_and_remote):
124+
peer, remote = eth_peer_and_remote
125+
126+
headers_bundle = mk_headers(1, 3, 2, 5, 4)
127+
headers, receipts, trie_roots_and_data = zip(*headers_bundle)
128+
receipts_bundle = tuple(zip(receipts, trie_roots_and_data))
129+
130+
wrong_headers = mk_headers(4, 3, 8)
131+
_, wrong_receipts, _ = zip(*wrong_headers)
132+
133+
async def send_receipts():
134+
remote.sub_proto.send_receipts(wrong_receipts)
135+
await asyncio.sleep(0)
136+
remote.sub_proto.send_receipts(receipts)
137+
await asyncio.sleep(0)
138+
139+
asyncio.ensure_future(send_receipts())
140+
response = await peer.requests.get_receipts(headers)
141+
142+
assert len(response) == len(headers)
143+
assert response == receipts_bundle
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import os
2+
import time
3+
4+
import pytest
5+
6+
from eth_utils import to_tuple
7+
8+
from eth.db.trie import make_trie_root_and_nodes
9+
from eth.rlp.headers import BlockHeader
10+
from eth.rlp.receipts import Receipt
11+
12+
from p2p.exceptions import ValidationError
13+
14+
from trinity.protocol.eth.requests import ReceiptsRequest
15+
16+
17+
@to_tuple
18+
def mk_receipts(num_receipts):
19+
for _ in range(num_receipts):
20+
yield Receipt(
21+
state_root=os.urandom(32),
22+
gas_used=21000,
23+
bloom=0,
24+
logs=[],
25+
)
26+
27+
28+
def mk_header_and_receipts(block_number, num_receipts):
29+
receipts = mk_receipts(num_receipts)
30+
root_hash, trie_root_and_data = make_trie_root_and_nodes(receipts)
31+
header = BlockHeader(
32+
difficulty=1000000,
33+
block_number=block_number,
34+
gas_limit=3141592,
35+
timestamp=int(time.time()),
36+
receipt_root=root_hash,
37+
)
38+
return header, receipts, (root_hash, trie_root_and_data)
39+
40+
41+
@to_tuple
42+
def mk_headers(*counts):
43+
for idx, num_receipts in enumerate(counts, 1):
44+
yield mk_header_and_receipts(idx, num_receipts)
45+
46+
47+
def test_receipts_request_empty_response_is_valid():
48+
headers_bundle = mk_headers(1, 3, 2, 5, 4)
49+
headers, _, _ = zip(*headers_bundle)
50+
request = ReceiptsRequest(headers)
51+
request.validate_response(tuple(), tuple())
52+
53+
54+
def test_receipts_request_valid_with_full_response():
55+
headers_bundle = mk_headers(1, 3, 2, 5, 4)
56+
headers, receipts, trie_roots_and_data = zip(*headers_bundle)
57+
receipts_bundle = tuple(zip(receipts, trie_roots_and_data))
58+
request = ReceiptsRequest(headers)
59+
request.validate_response(receipts, receipts_bundle)
60+
61+
62+
def test_receipts_request_valid_with_partial_response():
63+
headers_bundle = mk_headers(1, 3, 2, 5, 4)
64+
headers, receipts, trie_roots_and_data = zip(*headers_bundle)
65+
receipts_bundle = tuple(zip(receipts, trie_roots_and_data))
66+
request = ReceiptsRequest(headers)
67+
68+
request.validate_response(receipts[:3], receipts_bundle[:3])
69+
request.validate_response(receipts[2:], receipts_bundle[2:])
70+
request.validate_response(
71+
(receipts[1], receipts[3], receipts[4]),
72+
(receipts_bundle[1], receipts_bundle[3], receipts_bundle[4]),
73+
)
74+
75+
76+
def test_receipts_request_with_fully_invalid_response():
77+
headers_bundle = mk_headers(1, 3, 2, 5, 4)
78+
headers, _, _ = zip(*headers_bundle)
79+
80+
wrong_headers = mk_headers(4, 3, 8)
81+
_, wrong_receipts, wrong_trie_roots_and_data = zip(*wrong_headers)
82+
receipts_bundle = tuple(zip(wrong_receipts, wrong_trie_roots_and_data))
83+
84+
request = ReceiptsRequest(headers)
85+
86+
with pytest.raises(ValidationError):
87+
request.validate_response(wrong_receipts, receipts_bundle)
88+
89+
90+
def test_receipts_request_with_extra_unrequested_receipts():
91+
headers_bundle = mk_headers(1, 3, 2, 5, 4)
92+
headers, receipts, trie_roots_and_data = zip(*headers_bundle)
93+
receipts_bundle = tuple(zip(receipts, trie_roots_and_data))
94+
95+
wrong_headers = mk_headers(4, 3, 8)
96+
_, wrong_receipts, wrong_trie_roots_and_data = zip(*wrong_headers)
97+
extra_receipts_bundle = tuple(zip(wrong_receipts, wrong_trie_roots_and_data))
98+
99+
request = ReceiptsRequest(headers)
100+
101+
with pytest.raises(ValidationError):
102+
request.validate_response(
103+
receipts + wrong_receipts,
104+
receipts_bundle + extra_receipts_bundle,
105+
)

trinity/p2p/handlers.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import (
2+
Any,
23
AsyncGenerator,
34
List,
45
Tuple,
@@ -80,7 +81,7 @@ async def handle_get_node_data(self, peer: ETHPeer, node_hashes: List[Hash32]) -
8081
peer.sub_proto.send_node_data(tuple(nodes))
8182

8283
async def lookup_headers(self,
83-
request: BaseHeaderRequest) -> Tuple[BlockHeader, ...]:
84+
request: BaseHeaderRequest[Any]) -> Tuple[BlockHeader, ...]:
8485
"""
8586
Lookup :max_headers: headers starting at :block_number_or_hash:, skipping :skip: items
8687
between each, in reverse order if :reverse: is True.
@@ -101,7 +102,7 @@ async def lookup_headers(self,
101102
return headers
102103

103104
async def _get_block_numbers_for_request(self,
104-
request: BaseHeaderRequest
105+
request: BaseHeaderRequest[Any],
105106
) -> Tuple[BlockNumber, ...]:
106107
"""
107108
Generate the block numbers for a given `HeaderRequest`.

0 commit comments

Comments
 (0)