Skip to content

Commit 5ef07d9

Browse files
authored
Merge pull request #1552 from jannikluhn/bcc-handshake
Implement bcc protocol class and handshake
2 parents b10f1ed + c718bed commit 5ef07d9

File tree

5 files changed

+499
-0
lines changed

5 files changed

+499
-0
lines changed
Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
import pytest
2+
3+
import asyncio
4+
5+
from cancel_token import CancelToken
6+
7+
from eth.beacon.types.attestation_records import AttestationRecord
8+
from eth.beacon.types.attestation_signed_data import AttestationSignedData
9+
from eth.db.atomic import AtomicDB
10+
from eth.beacon.db.chain import BeaconChainDB
11+
from eth.beacon.types.blocks import BaseBeaconBlock
12+
13+
from eth.constants import (
14+
ZERO_HASH32,
15+
)
16+
17+
from p2p import ecies
18+
from p2p.exceptions import HandshakeFailure
19+
from p2p.peer import MsgBuffer
20+
21+
from trinity.protocol.bcc.context import BeaconContext
22+
from trinity.protocol.bcc.peer import (
23+
BCCPeerFactory,
24+
)
25+
from trinity.protocol.bcc.proto import BCCProtocol
26+
from trinity.protocol.bcc.commands import (
27+
Status,
28+
GetBeaconBlocks,
29+
BeaconBlocks,
30+
AttestationRecords,
31+
)
32+
33+
from p2p.tools.paragon.helpers import (
34+
get_directly_linked_peers_without_handshake as _get_directly_linked_peers_without_handshake,
35+
get_directly_linked_peers as _get_directly_linked_peers,
36+
)
37+
38+
39+
def get_fresh_chain_db():
40+
db = AtomicDB()
41+
genesis_block = BaseBeaconBlock(
42+
slot=0,
43+
randao_reveal=ZERO_HASH32,
44+
candidate_pow_receipt_root=ZERO_HASH32,
45+
ancestor_hashes=[ZERO_HASH32] * 32,
46+
state_root=ZERO_HASH32, # note: not the actual genesis state root
47+
attestations=[],
48+
specials=[],
49+
proposer_signature=None,
50+
)
51+
52+
chain_db = BeaconChainDB(db)
53+
chain_db.persist_block(genesis_block)
54+
return chain_db
55+
56+
57+
async def _setup_alice_and_bob_factories(alice_chain_db=None, bob_chain_db=None):
58+
cancel_token = CancelToken('trinity.get_directly_linked_peers_without_handshake')
59+
60+
#
61+
# Alice
62+
#
63+
if alice_chain_db is None:
64+
alice_chain_db = get_fresh_chain_db()
65+
66+
alice_context = BeaconContext(
67+
chain_db=alice_chain_db,
68+
network_id=1,
69+
)
70+
71+
alice_factory = BCCPeerFactory(
72+
privkey=ecies.generate_privkey(),
73+
context=alice_context,
74+
token=cancel_token,
75+
)
76+
77+
#
78+
# Bob
79+
#
80+
if bob_chain_db is None:
81+
bob_chain_db = get_fresh_chain_db()
82+
83+
bob_context = BeaconContext(
84+
chain_db=bob_chain_db,
85+
network_id=1,
86+
)
87+
88+
bob_factory = BCCPeerFactory(
89+
privkey=ecies.generate_privkey(),
90+
context=bob_context,
91+
token=cancel_token,
92+
)
93+
94+
return alice_factory, bob_factory
95+
96+
97+
async def get_directly_linked_peers_without_handshake(alice_chain_db=None, bob_chain_db=None):
98+
alice_factory, bob_factory = await _setup_alice_and_bob_factories(alice_chain_db, bob_chain_db)
99+
100+
return await _get_directly_linked_peers_without_handshake(
101+
alice_factory=alice_factory,
102+
bob_factory=bob_factory,
103+
)
104+
105+
106+
async def get_directly_linked_peers(request, event_loop, alice_chain_db=None, bob_chain_db=None):
107+
alice_factory, bob_factory = await _setup_alice_and_bob_factories(
108+
alice_chain_db,
109+
bob_chain_db,
110+
)
111+
112+
return await _get_directly_linked_peers(
113+
request,
114+
event_loop,
115+
alice_factory=alice_factory,
116+
bob_factory=bob_factory,
117+
)
118+
119+
120+
@pytest.mark.asyncio
121+
async def test_directly_linked_peers_without_handshake():
122+
alice, bob = await get_directly_linked_peers_without_handshake()
123+
assert alice.sub_proto is None
124+
assert bob.sub_proto is None
125+
126+
127+
@pytest.mark.asyncio
128+
async def test_directly_linked_peers(request, event_loop):
129+
alice, bob = await get_directly_linked_peers(request, event_loop)
130+
assert isinstance(alice.sub_proto, BCCProtocol)
131+
assert isinstance(bob.sub_proto, BCCProtocol)
132+
133+
assert alice.head_hash == bob.context.chain_db.get_canonical_head().hash
134+
assert bob.head_hash == alice.context.chain_db.get_canonical_head().hash
135+
136+
137+
@pytest.mark.asyncio
138+
async def test_unidirectional_handshake(request, event_loop):
139+
alice, bob = await get_directly_linked_peers_without_handshake()
140+
alice_chain_db = alice.context.chain_db
141+
alice_genesis_hash = alice_chain_db.get_canonical_block_by_slot(0).hash
142+
alice_head_hash = alice_chain_db.get_canonical_head().hash
143+
144+
await asyncio.gather(alice.do_p2p_handshake(), bob.do_p2p_handshake())
145+
146+
await alice.send_sub_proto_handshake()
147+
cmd, msg = await bob.read_msg()
148+
149+
assert isinstance(cmd, Status)
150+
151+
assert msg["protocol_version"] == BCCProtocol.version
152+
assert msg["network_id"] == alice.context.network_id
153+
assert msg["genesis_hash"] == alice_head_hash
154+
assert msg["best_hash"] == alice_genesis_hash
155+
156+
await bob.process_sub_proto_handshake(cmd, msg)
157+
158+
assert bob.head_hash == alice_head_hash
159+
assert alice.head_hash is None
160+
161+
# stop cleanly
162+
asyncio.ensure_future(alice.run())
163+
asyncio.ensure_future(bob.run())
164+
await asyncio.gather(
165+
alice.cancel(),
166+
bob.cancel(),
167+
)
168+
169+
170+
@pytest.mark.asyncio
171+
async def test_handshake_wrong_network_id(request, event_loop):
172+
alice, bob = await get_directly_linked_peers_without_handshake()
173+
alice.context.network_id += 1
174+
await asyncio.gather(alice.do_p2p_handshake(), bob.do_p2p_handshake())
175+
176+
await alice.send_sub_proto_handshake()
177+
cmd, msg = await bob.read_msg()
178+
179+
with pytest.raises(HandshakeFailure):
180+
await bob.process_sub_proto_handshake(cmd, msg)
181+
182+
# stop cleanly
183+
asyncio.ensure_future(alice.run())
184+
asyncio.ensure_future(bob.run())
185+
await asyncio.gather(
186+
alice.cancel(),
187+
bob.cancel(),
188+
)
189+
190+
191+
@pytest.mark.asyncio
192+
async def test_send_no_blocks(request, event_loop):
193+
alice, bob = await get_directly_linked_peers(request, event_loop)
194+
msg_buffer = MsgBuffer()
195+
bob.add_subscriber(msg_buffer)
196+
197+
alice.sub_proto.send_blocks(())
198+
199+
message = await msg_buffer.msg_queue.get()
200+
assert isinstance(message.command, BeaconBlocks)
201+
assert message.payload == ()
202+
203+
204+
@pytest.mark.asyncio
205+
async def test_send_single_block(request, event_loop):
206+
alice, bob = await get_directly_linked_peers(request, event_loop)
207+
msg_buffer = MsgBuffer()
208+
bob.add_subscriber(msg_buffer)
209+
210+
block = BaseBeaconBlock(
211+
slot=1,
212+
randao_reveal=ZERO_HASH32,
213+
candidate_pow_receipt_root=ZERO_HASH32,
214+
ancestor_hashes=[ZERO_HASH32] * 32,
215+
state_root=ZERO_HASH32,
216+
attestations=[],
217+
specials=[],
218+
proposer_signature=None,
219+
)
220+
alice.sub_proto.send_blocks((block,))
221+
222+
message = await msg_buffer.msg_queue.get()
223+
assert isinstance(message.command, BeaconBlocks)
224+
assert message.payload == (block,)
225+
226+
227+
@pytest.mark.asyncio
228+
async def test_send_multiple_blocks(request, event_loop):
229+
alice, bob = await get_directly_linked_peers(request, event_loop)
230+
msg_buffer = MsgBuffer()
231+
bob.add_subscriber(msg_buffer)
232+
233+
blocks = tuple(
234+
BaseBeaconBlock(
235+
slot=slot,
236+
randao_reveal=ZERO_HASH32,
237+
candidate_pow_receipt_root=ZERO_HASH32,
238+
ancestor_hashes=[ZERO_HASH32] * 32,
239+
state_root=ZERO_HASH32,
240+
attestations=[],
241+
specials=[],
242+
proposer_signature=None,
243+
)
244+
for slot in range(3)
245+
)
246+
alice.sub_proto.send_blocks(blocks)
247+
248+
message = await msg_buffer.msg_queue.get()
249+
assert isinstance(message.command, BeaconBlocks)
250+
assert message.payload == blocks
251+
252+
253+
@pytest.mark.asyncio
254+
async def test_send_get_blocks_by_slot(request, event_loop):
255+
alice, bob = await get_directly_linked_peers(request, event_loop)
256+
msg_buffer = MsgBuffer()
257+
bob.add_subscriber(msg_buffer)
258+
259+
alice.sub_proto.send_get_blocks(123, 10)
260+
261+
message = await msg_buffer.msg_queue.get()
262+
assert isinstance(message.command, GetBeaconBlocks)
263+
assert message.payload == {
264+
"block_slot_or_hash": 123,
265+
"max_blocks": 10,
266+
}
267+
268+
269+
@pytest.mark.asyncio
270+
async def test_send_get_blocks_by_hash(request, event_loop):
271+
alice, bob = await get_directly_linked_peers(request, event_loop)
272+
msg_buffer = MsgBuffer()
273+
bob.add_subscriber(msg_buffer)
274+
275+
alice.sub_proto.send_get_blocks(b"\x33" * 32, 15)
276+
277+
message = await msg_buffer.msg_queue.get()
278+
assert isinstance(message.command, GetBeaconBlocks)
279+
assert message.payload == {
280+
"block_slot_or_hash": b"\x33" * 32,
281+
"max_blocks": 15,
282+
}
283+
284+
285+
@pytest.mark.asyncio
286+
async def test_send_no_attestations(request, event_loop):
287+
alice, bob = await get_directly_linked_peers(request, event_loop)
288+
msg_buffer = MsgBuffer()
289+
bob.add_subscriber(msg_buffer)
290+
291+
alice.sub_proto.send_attestation_records(())
292+
293+
message = await msg_buffer.msg_queue.get()
294+
assert isinstance(message.command, AttestationRecords)
295+
assert message.payload == ()
296+
297+
298+
@pytest.mark.asyncio
299+
async def test_send_single_attestation(request, event_loop):
300+
alice, bob = await get_directly_linked_peers(request, event_loop)
301+
msg_buffer = MsgBuffer()
302+
bob.add_subscriber(msg_buffer)
303+
304+
attestation_record = AttestationRecord(
305+
data=AttestationSignedData(
306+
slot=0,
307+
shard=1,
308+
block_hash=ZERO_HASH32,
309+
cycle_boundary_hash=ZERO_HASH32,
310+
shard_block_hash=ZERO_HASH32,
311+
last_crosslink_hash=ZERO_HASH32,
312+
justified_slot=0,
313+
justified_block_hash=ZERO_HASH32,
314+
),
315+
attester_bitfield=b"\x00\x00\x00",
316+
poc_bitfield=b"\x00\x00\x00",
317+
)
318+
319+
alice.sub_proto.send_attestation_records((attestation_record,))
320+
321+
message = await msg_buffer.msg_queue.get()
322+
assert isinstance(message.command, AttestationRecords)
323+
assert message.payload == (attestation_record,)
324+
325+
326+
@pytest.mark.asyncio
327+
async def test_send_multiple_attestations(request, event_loop):
328+
alice, bob = await get_directly_linked_peers(request, event_loop)
329+
msg_buffer = MsgBuffer()
330+
bob.add_subscriber(msg_buffer)
331+
332+
attestation_records = tuple(
333+
AttestationRecord(
334+
data=AttestationSignedData(
335+
slot=0,
336+
shard=shard,
337+
block_hash=ZERO_HASH32,
338+
cycle_boundary_hash=ZERO_HASH32,
339+
shard_block_hash=ZERO_HASH32,
340+
last_crosslink_hash=ZERO_HASH32,
341+
justified_slot=0,
342+
justified_block_hash=ZERO_HASH32,
343+
),
344+
attester_bitfield=b"\x00\x00\x00",
345+
poc_bitfield=b"\x00\x00\x00",
346+
) for shard in range(10)
347+
)
348+
349+
alice.sub_proto.send_attestation_records(attestation_records)
350+
351+
message = await msg_buffer.msg_queue.get()
352+
assert isinstance(message.command, AttestationRecords)
353+
assert message.payload == attestation_records

trinity/protocol/bcc/commands.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class Status(Command):
1414
structure = [
1515
('protocol_version', sedes.big_endian_int),
1616
('network_id', sedes.big_endian_int),
17+
('genesis_hash', sedes.binary),
1718
('best_hash', sedes.binary),
1819
]
1920

trinity/protocol/bcc/context.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from p2p.peer import BasePeerContext
2+
3+
from eth.beacon.db.chain import BaseBeaconChainDB
4+
5+
6+
class BeaconContext(BasePeerContext):
7+
8+
def __init__(self, chain_db: BaseBeaconChainDB, network_id: int) -> None:
9+
self.chain_db = chain_db
10+
self.network_id = network_id

0 commit comments

Comments
 (0)