Skip to content

Commit 6dd453f

Browse files
authored
Merge pull request #198 from thenewboston-developers/BC-272-add-block-to-blockchain-once-it-gets-enough-confirmations
A unittest and some more refactoring
2 parents b9ff3a5 + 317f9a8 commit 6dd453f

File tree

4 files changed

+68
-19
lines changed

4 files changed

+68
-19
lines changed

node/blockchain/tasks/process_block_confirmations.py

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
from node.blockchain.constants import BLOCK_LOCK
1010
from node.blockchain.facade import BlockchainFacade
11-
from node.blockchain.inner_models import BlockConfirmation as PydanticBlockConfirmation
1211
from node.blockchain.mixins.crypto import HashableStringWrapper
1312
from node.blockchain.models import BlockConfirmation, PendingBlock
1413
from node.blockchain.types import Hash
@@ -18,19 +17,20 @@
1817
logger = logging.getLogger(__name__)
1918

2019

21-
def get_consensus_block_hash_with_confirmations(
22-
facade, next_block_number, minimum_consensus
23-
) -> Optional[tuple[Hash, list[PydanticBlockConfirmation]]]:
20+
def get_next_block_confirmations(next_block_number) -> list[BlockConfirmation]:
21+
facade = BlockchainFacade.get_instance()
2422
cv_identifiers = facade.get_confirmation_validator_identifiers()
23+
return list(BlockConfirmation.objects.filter(number=next_block_number, signer__in=cv_identifiers))
2524

26-
# Query only confirmations for the next block number and received from confirmation validators
27-
all_confirmations = BlockConfirmation.objects.filter(number=next_block_number, signer__in=cv_identifiers)
2825

29-
# Group confirmations by hash to see which hash wins the consensus
30-
grouped_confirmations = groupby(all_confirmations.order_by('hash'), key=attrgetter('hash'))
26+
def get_consensus_block_hash_with_confirmations(confirmations,
27+
minimum_consensus) -> Optional[tuple[Hash, list[BlockConfirmation]]]:
28+
key_func = attrgetter('hash')
29+
grouped_confirmations = [(Hash(hash_), list(confirmations))
30+
for hash_, confirmations in groupby(sorted(confirmations, key=key_func), key=key_func)]
3131
finalizable_hashes = [(hash_, confirmations)
3232
for hash_, confirmations in grouped_confirmations
33-
if len(list(confirmations)) >= minimum_consensus]
33+
if len(confirmations) >= minimum_consensus]
3434

3535
if not finalizable_hashes:
3636
return None # No consensus, yet
@@ -40,36 +40,43 @@ def get_consensus_block_hash_with_confirmations(
4040

4141
assert len(finalizable_hashes) == 1
4242
block_hash, consensus_confirmations = finalizable_hashes[0]
43-
return block_hash, [confirmation.get_block_confirmation() for confirmation in consensus_confirmations]
43+
assert len(set(confirmation.signer for confirmation in consensus_confirmations)) == len(consensus_confirmations)
44+
return block_hash, consensus_confirmations
4445

4546

46-
def is_valid_consensus(facade, confirmations, minimum_consensus):
47+
def is_valid_consensus(confirmations: list[BlockConfirmation], minimum_consensus):
4748
# Validate confirmations, since they may have not been validated on API call because some of them were added
4849
# much earlier then the next block number become equal to confirmation block number
49-
valid_confirmations = []
50+
assert len(set(confirmation.signer for confirmation in confirmations)) == len(confirmations)
51+
facade = BlockchainFacade.get_instance()
52+
53+
confirmations_left = minimum_consensus
5054
for confirmation in confirmations:
5155
try:
52-
confirmation.validate_all(facade)
56+
confirmation.get_block_confirmation().validate_all(facade)
5357
except ValidationError:
5458
logger.warning('Invalid confirmation detected: %s', confirmation)
5559
continue
5660

57-
valid_confirmations.append(confirmation)
61+
confirmations_left -= 1
62+
if confirmations_left <= 0:
63+
return True
5864

59-
return len(valid_confirmations) >= minimum_consensus
65+
return False
6066

6167

6268
@lock(BLOCK_LOCK)
6369
def process_next_block():
6470
facade = BlockchainFacade.get_instance()
6571
next_block_number = facade.get_next_block_number()
66-
minimum_consensus = facade.get_minimum_consensus()
72+
confirmations = get_next_block_confirmations(next_block_number)
6773

68-
if not (result := get_consensus_block_hash_with_confirmations(facade, next_block_number, minimum_consensus)):
74+
minimum_consensus = facade.get_minimum_consensus()
75+
if not (result := get_consensus_block_hash_with_confirmations(confirmations, minimum_consensus)):
6976
return False
7077

7178
block_hash, confirmations = result
72-
if not is_valid_consensus(facade, confirmations, minimum_consensus):
79+
if not is_valid_consensus(confirmations, minimum_consensus):
7380
return False
7481

7582
pending_block = PendingBlock.objects.get_or_none(number=next_block_number, hash=block_hash)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from unittest.mock import patch
2+
3+
import pytest
4+
from model_bakery import baker
5+
6+
from node.blockchain.models import Node
7+
from node.blockchain.tasks.process_block_confirmations import get_next_block_confirmations
8+
from node.blockchain.types import NodeRole
9+
10+
11+
@pytest.mark.django_db
12+
def test_get_next_block_confirmations():
13+
node0 = baker.make('blockchain.AccountState', _id='0' * 64, node={'fee': 1})
14+
node1 = baker.make('blockchain.AccountState', _id='1' * 64, node={'fee': 1})
15+
node2 = baker.make('blockchain.AccountState', _id='2' * 64, node={'fee': 1})
16+
baker.make('blockchain.AccountState', _id='3' * 64, node={'fee': 1})
17+
18+
baker.make('blockchain.BlockConfirmation', number=4, signer=node0._id)
19+
bc2 = baker.make('blockchain.BlockConfirmation', number=3, signer=node0._id)
20+
bc3 = baker.make('blockchain.BlockConfirmation', number=3, signer=node1._id)
21+
22+
return_value = Node.objects.filter(_id__in=(node0._id, node1._id, node2._id))
23+
with patch('node.blockchain.models.node.NodeQuerySet.filter_by_roles', return_value=return_value) as mock:
24+
assert set(get_next_block_confirmations(3)) == {bc2, bc3}
25+
26+
mock.assert_called_once_with((NodeRole.CONFIRMATION_VALIDATOR,))

poetry.lock

Lines changed: 16 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ colorlog = "^6.6.0"
2525
celery = "^5.2.3"
2626
sentry-sdk = "^1.5.6"
2727
drf-spectacular = "^0.21.2"
28+
model-bakery = "^1.4.0"
2829

2930
[tool.poetry.dev-dependencies]
3031
pre-commit = "^2.15.0"

0 commit comments

Comments
 (0)