Skip to content

Commit 31ffede

Browse files
committed
Some refactoring along with drafted implementation
1 parent 48800f4 commit 31ffede

File tree

7 files changed

+89
-17
lines changed

7 files changed

+89
-17
lines changed

node/blockchain/migrations/0004_pendingblock.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ class Migration(migrations.Migration):
1818
('_id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
1919
('number', models.PositiveBigIntegerField()),
2020
('hash', models.CharField(max_length=128)),
21+
('signer', models.CharField(max_length=64)),
2122
('body', models.BinaryField()),
2223
],
2324
options={
24-
'ordering': ('number', 'hash'),
25+
'ordering': ('number', 'signer'),
2526
'unique_together': {('number', 'hash')},
2627
},
2728
),

node/blockchain/models/block_confirmation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ def update_or_create_from_block_confirmation(self, block_confirmation: PydanticB
3030

3131
class BlockConfirmation(CustomModel):
3232

33-
_id = models.UUIDField(primary_key=True, default=uuid.uuid4) # noqa: A003
33+
_id = models.UUIDField(primary_key=True, default=uuid.uuid4)
3434
number = models.PositiveBigIntegerField()
3535
hash = models.CharField(max_length=128) # noqa: A003
36-
signer = models.CharField(max_length=64) # noqa: A003
36+
signer = models.CharField(max_length=64)
3737
body = models.BinaryField()
3838

3939
objects = BlockConfirmationManager()

node/blockchain/models/pending_block.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,17 @@
88

99
class PendingBlock(CustomModel):
1010

11-
_id = models.UUIDField(primary_key=True, default=uuid.uuid4) # noqa: A003
11+
_id = models.UUIDField(primary_key=True, default=uuid.uuid4)
1212
number = models.PositiveBigIntegerField()
1313
hash = models.CharField(max_length=128) # noqa: A003
14+
signer = models.CharField(max_length=64)
1415
body = models.BinaryField()
1516

1617
def get_block(self) -> PydanticBlock:
1718
return PydanticBlock.parse_raw(self.body)
1819

1920
class Meta:
20-
unique_together = ('number', 'hash')
21+
unique_together = ('number', 'signer')
2122
ordering = unique_together
2223

2324
def __str__(self):

node/blockchain/serializers/block.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
from collections import OrderedDict
33

44
from rest_framework import serializers
5+
from rest_framework.exceptions import ValidationError
56

7+
from node.blockchain.facade import BlockchainFacade
68
from node.blockchain.inner_models import Block as PydanticBlock
79
from node.blockchain.models import Block as ORMBlock
810
from node.blockchain.models import PendingBlock
@@ -20,19 +22,25 @@ def to_representation(self, instance):
2022
# This "hack" is needed to reduce deserialization / serialization overhead when reading blocks
2123
return OrderedDict(body=instance.body)
2224

25+
def validate_message(self, message):
26+
is_invalid_number = ((block_number := message.get('number')) is None or
27+
block_number < BlockchainFacade.get_instance().get_next_block_number())
28+
if is_invalid_number:
29+
raise ValidationError('Invalid number')
30+
31+
return message
32+
2333
def create(self, validated_data):
2434
block = PydanticBlock.parse_obj(validated_data)
25-
26-
block_number = block.get_block_number()
27-
block_hash = block.make_hash()
28-
instance, is_created = PendingBlock.objects.get_or_create(
29-
number=block_number,
30-
hash=block_hash,
35+
instance, _ = PendingBlock.objects.update_or_create(
36+
number=block.get_block_number(),
37+
signer=block.signer,
3138
# TODO(dmu) MEDIUM: It would be more effective to save original request body instead of serializing again
32-
defaults={'body': block.json()},
39+
defaults={
40+
'hash': block.make_hash(),
41+
'body': block.json(),
42+
},
3343
)
34-
if not is_created:
35-
logger.warning('Block number %s, hash %s appeared more than once')
3644

3745
return instance
3846

node/blockchain/serializers/block_confirmation.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from node.core.serializers import ValidateUnknownFieldsMixin
66

77

8+
# TODO(dmu) MEDIUM: Consider implementing BlockConfirmationSerializer as ModelSerializer similar to
9+
# node.blockchain.serializers.block.BlockSerializer
810
class BlockConfirmationSerializer(serializers.Serializer, ValidateUnknownFieldsMixin):
911
signer = serializers.CharField()
1012
signature = serializers.CharField()

node/blockchain/tasks/process_pending_blocks.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,41 @@
1+
import logging
2+
13
from celery import shared_task
24

5+
from node.blockchain.facade import BlockchainFacade
6+
from node.blockchain.models import PendingBlock
37

4-
@shared_task
5-
def process_pending_blocks_task():
8+
logger = logging.getLogger(__name__)
9+
10+
11+
def validate_pending_block(orm_pending_block: PendingBlock):
12+
pending_block = orm_pending_block.get_block()
13+
return pending_block
14+
15+
16+
def process_next_block() -> bool:
617
# TODO(dmu) CRITICAL: Process pending blocks. To be implemented in
718
# https://thenewboston.atlassian.net/browse/BC-263
8-
pass
19+
facade = BlockchainFacade.get_instance()
20+
next_block_number = facade.get_next_block_number()
21+
22+
# There may be more than one pending block, but at most one of them can be valid
23+
orm_pending_blocks = list(PendingBlock.objects.filter(number=next_block_number))
24+
for orm_pending_block in orm_pending_blocks:
25+
try:
26+
validate_pending_block(orm_pending_block)
27+
return False
28+
except Exception:
29+
logger.warning('Error while trying to validate pending block: %s', orm_pending_block, exc_info=True)
30+
31+
return False
32+
33+
34+
@shared_task
35+
def process_pending_blocks_task():
36+
should_process_next_block = True
37+
while should_process_next_block:
38+
should_process_next_block = process_next_block()
939

1040

1141
def start_process_pending_blocks_task():

node/blockchain/tests/test_rest_api/test_block.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
from unittest.mock import patch
33

44
import pytest
5+
from django.db import connection
56

67
from node.blockchain.facade import BlockchainFacade
8+
from node.blockchain.inner_models import NodeDeclarationBlockMessage
79
from node.blockchain.models import Block, PendingBlock
810
from node.blockchain.tests.factories.block import make_block
911
from node.blockchain.tests.factories.block_message.node_declaration import make_node_declaration_block_message
@@ -109,3 +111,31 @@ def test_create_pending_block(regular_node, regular_node_key_pair, primary_valid
109111
pending_block = PendingBlock.objects.get_or_none(number=block.get_block_number(), hash=block.make_hash())
110112
assert pending_block
111113
assert pending_block.body == payload
114+
115+
116+
@pytest.mark.usefixtures('rich_blockchain')
117+
def test_try_to_create_outdated_block(regular_node, regular_node_key_pair, primary_validator_key_pair, api_client):
118+
assert not PendingBlock.objects.exists()
119+
120+
facade = BlockchainFacade.get_instance()
121+
block_message = make_node_declaration_block_message(regular_node, regular_node_key_pair, facade)
122+
block_message_dict = block_message.dict()
123+
block_message_dict['number'] = facade.get_next_block_number() - 1
124+
block_message = NodeDeclarationBlockMessage.parse_obj(block_message_dict)
125+
126+
assert facade.get_primary_validator().identifier == primary_validator_key_pair.public
127+
block = make_block(block_message, primary_validator_key_pair.private)
128+
129+
payload = block.json()
130+
with patch('node.blockchain.views.block.start_process_pending_blocks_task') as mock:
131+
response = api_client.post('/api/blocks/', payload, content_type='application/json')
132+
133+
assert response.status_code == 400
134+
assert response.json() == {'message': [{'code': 'invalid', 'message': 'Invalid number'}]}
135+
mock.assert_not_called()
136+
137+
# This is because we have queried the database and nested transactions (save points) are not supported
138+
assert connection.needs_rollback
139+
connection.set_rollback(False)
140+
141+
assert not PendingBlock.objects.exists()

0 commit comments

Comments
 (0)