|
1 | 1 | import logging
|
2 | 2 | from itertools import groupby
|
3 | 3 | from operator import attrgetter
|
| 4 | +from typing import Optional |
4 | 5 |
|
5 | 6 | from celery import shared_task
|
6 | 7 | from django.db import transaction
|
7 | 8 |
|
8 | 9 | from node.blockchain.constants import BLOCK_LOCK
|
9 | 10 | from node.blockchain.facade import BlockchainFacade
|
| 11 | +from node.blockchain.inner_models import BlockConfirmation as PydanticBlockConfirmation |
| 12 | +from node.blockchain.mixins.crypto import HashableStringWrapper |
10 | 13 | from node.blockchain.models import BlockConfirmation, PendingBlock
|
| 14 | +from node.blockchain.types import Hash |
11 | 15 | from node.blockchain.utils.lock import lock
|
12 | 16 | from node.core.exceptions import ValidationError
|
13 | 17 |
|
14 | 18 | logger = logging.getLogger(__name__)
|
15 | 19 |
|
16 | 20 |
|
17 |
| -@lock(BLOCK_LOCK) |
18 |
| -def process_next_block(): |
19 |
| - facade = BlockchainFacade.get_instance() |
20 |
| - next_block_number = facade.get_next_block_number() |
| 21 | +def get_consensus_block_hash_with_confirmations( |
| 22 | + facade, next_block_number, minimum_consensus |
| 23 | +) -> Optional[tuple[Hash, list[PydanticBlockConfirmation]]]: |
21 | 24 | cv_identifiers = facade.get_confirmation_validator_identifiers()
|
22 | 25 |
|
23 | 26 | # Query only confirmations for the next block number and received from confirmation validators
|
24 | 27 | all_confirmations = BlockConfirmation.objects.filter(number=next_block_number, signer__in=cv_identifiers)
|
25 | 28 |
|
26 | 29 | # Group confirmations by hash to see which hash wins the consensus
|
27 | 30 | grouped_confirmations = groupby(all_confirmations.order_by('hash'), key=attrgetter('hash'))
|
28 |
| - minimum_consensus = facade.get_minimum_consensus() |
29 |
| - |
30 | 31 | finalizable_hashes = [(hash_, confirmations)
|
31 | 32 | for hash_, confirmations in grouped_confirmations
|
32 | 33 | if len(list(confirmations)) >= minimum_consensus]
|
33 | 34 |
|
34 | 35 | if not finalizable_hashes:
|
35 |
| - return False # No consensus, yet |
| 36 | + return None # No consensus, yet |
36 | 37 |
|
37 | 38 | if len(finalizable_hashes) >= 2:
|
38 |
| - # We should never get here |
39 |
| - raise ValueError('More than one finalizable hash found') |
| 39 | + raise ValueError('More than one finalizable hash found') # We should never get here |
40 | 40 |
|
41 | 41 | assert len(finalizable_hashes) == 1
|
42 |
| - hash_, consensus_confirmations = finalizable_hashes[0] |
| 42 | + block_hash, consensus_confirmations = finalizable_hashes[0] |
| 43 | + return block_hash, [confirmation.get_block_confirmation() for confirmation in consensus_confirmations] |
| 44 | + |
43 | 45 |
|
| 46 | +def is_valid_consensus(facade, confirmations, minimum_consensus): |
44 | 47 | # Validate confirmations, since they may have not been validated on API call because some of them were added
|
45 | 48 | # much earlier then the next block number become equal to confirmation block number
|
46 | 49 | valid_confirmations = []
|
47 |
| - for confirmation in consensus_confirmations: |
| 50 | + for confirmation in confirmations: |
48 | 51 | try:
|
49 |
| - confirmation.validate_all() |
| 52 | + confirmation.validate_all(facade) |
50 | 53 | except ValidationError:
|
51 | 54 | logger.warning('Invalid confirmation detected: %s', confirmation)
|
52 | 55 | continue
|
53 | 56 |
|
54 | 57 | valid_confirmations.append(confirmation)
|
55 | 58 |
|
56 |
| - if len(valid_confirmations) < minimum_consensus: # Check that we still have consensus after validation |
| 59 | + return len(valid_confirmations) >= minimum_consensus |
| 60 | + |
| 61 | + |
| 62 | +@lock(BLOCK_LOCK) |
| 63 | +def process_next_block(): |
| 64 | + facade = BlockchainFacade.get_instance() |
| 65 | + next_block_number = facade.get_next_block_number() |
| 66 | + minimum_consensus = facade.get_minimum_consensus() |
| 67 | + |
| 68 | + if not (result := get_consensus_block_hash_with_confirmations(facade, next_block_number, minimum_consensus)): |
| 69 | + return False |
| 70 | + |
| 71 | + block_hash, confirmations = result |
| 72 | + if not is_valid_consensus(facade, confirmations, minimum_consensus): |
57 | 73 | return False
|
58 | 74 |
|
59 |
| - pending_block = PendingBlock.objects.get_or_none(number=next_block_number, hash=hash_) |
| 75 | + pending_block = PendingBlock.objects.get_or_none(number=next_block_number, hash=block_hash) |
60 | 76 | if pending_block is None:
|
61 | 77 | # TODO(dmu) CRITICAL: https://thenewboston.atlassian.net/browse/BC-283
|
62 | 78 | raise NotImplementedError('Edge case of processing confirmed missing pending block is not implemented')
|
63 | 79 |
|
| 80 | + block_body = pending_block.body |
| 81 | + if HashableStringWrapper(block_body).make_hash() != block_hash: |
| 82 | + raise ValidationError('Pending block body hash is not valid') # we should never get here |
| 83 | + |
64 | 84 | with transaction.atomic():
|
65 |
| - facade.add_block_from_json(pending_block.body, expect_locked=True) |
| 85 | + facade.add_block_from_json(block_body, expect_locked=True) |
66 | 86 | # There may be blocks with other hashes therefore we delete all of them
|
67 | 87 | PendingBlock.objects.filter(number__lte=next_block_number).delete()
|
68 | 88 | BlockConfirmation.objects.filter(number__lte=next_block_number).delete()
|
|
0 commit comments