feat(nano): implement consensus [part 13]#1290
Conversation
e502727 to
010c4b9
Compare
8ab5488 to
84a4de9
Compare
Codecov ReportAttention: Patch coverage is
❌ Your project status has failed because the head coverage (78.00%) is below the target coverage (82.00%). You can increase the head coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## master #1290 +/- ##
==========================================
+ Coverage 75.66% 78.00% +2.34%
==========================================
Files 426 426
Lines 31455 31904 +449
Branches 4873 4950 +77
==========================================
+ Hits 23800 24887 +1087
+ Misses 6839 6066 -773
- Partials 816 951 +135 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
0880346 to
758f0c5
Compare
0b6f616 to
d5fb072
Compare
fdfe8f2 to
18d0a50
Compare
d5fb072 to
d9685f0
Compare
18d0a50 to
4790263
Compare
d9685f0 to
d150a7a
Compare
ffcb995 to
d394b7b
Compare
|
| Branch | feat/nano/consensus |
| Testbed | ubuntu-22.04 |
Click to view all benchmark results
| Benchmark | Latency | Benchmark Result minutes (m) (Result Δ%) | Lower Boundary minutes (m) (Limit %) | Upper Boundary minutes (m) (Limit %) |
|---|---|---|---|---|
| sync-v2 (up to 20000 blocks) | 📈 view plot 🚷 view threshold | 1.64 m(+0.37%)Baseline: 1.63 m | 1.47 m (89.66%) | 1.79 m (91.25%) |
d394b7b to
6b09bf8
Compare
| # Get fields. | ||
| fields: dict[str, NCValueSuccessResponse | NCValueErrorResponse] = {} | ||
| param_fields: list[str] = params.fields | ||
| for field in param_fields: | ||
| key_field = self.get_key_for_field(field) | ||
| if key_field is None: | ||
| fields[field] = NCValueErrorResponse(errmsg='invalid format') | ||
| continue | ||
|
|
||
| try: | ||
| field_type = blueprint_class.__annotations__[field] | ||
| except KeyError: | ||
| fields[field] = NCValueErrorResponse(errmsg='not a blueprint field') | ||
| continue | ||
|
|
||
| try: | ||
| field_nc_type = make_nc_type_for_type(field_type) | ||
| value = nc_storage.get_obj(key_field.encode(), field_nc_type) | ||
| except KeyError: | ||
| fields[field] = NCValueErrorResponse(errmsg='field not found') | ||
| continue | ||
|
|
||
| if type(value) is bytes: | ||
| value = value.hex() | ||
| fields[field] = NCValueSuccessResponse(value=value) |
There was a problem hiding this comment.
This API will have to change soon to properly support "deep keys" without custom string syntax.
| if not (self.soft_voided_tx_ids & voided_by): | ||
| return voided_by |
There was a problem hiding this comment.
Is this "shortcut" correct? For example:
# assuming these:
# self. soft_voided_txs_ids = {"sv1"}
# self._settings.NC_EXECUTION_FAIL_ID = "ncf"
self._filter_out_soft_voided_entries({"ncf", "foo"}) == {"ncf", "foo"} # will "shortcut" because it doesn't have "sv1"
self._filter_out_soft_voided_entries({"ncf", "foo", "sv1"}) == {"foo"} # will skip "ncf" (not add it to `ret`)It seems harmless because these other "failure ids" are removed later, but it does seem to me that the intended behavior of this method is not consistent: it removes failure ids in some cases but not others.
There was a problem hiding this comment.
Both _filter_out_soft_voided_entries and _filter_out_nc_fail_entries are used only once, in filter_out_voided_by_entries_from_parents, and do basically the same thing. I think they could be unified.
| self.execute_nano_contracts(tx) | ||
|
|
||
| def execute_nano_contracts(self, tx: Transaction) -> None: | ||
| """This method is called when the transaction is added to the mempool. | ||
|
|
||
| The method is currently only executed when the transaction is confirmed by a block. | ||
| Hence, we do nothing here. | ||
| """ | ||
| pass |
There was a problem hiding this comment.
So this isn't needed and this code can be removed, right?
| assert tx.storage is not None | ||
| tx2 = tx.storage.get_transaction(h) | ||
| tx2_meta = tx2.get_metadata() | ||
| tx2_voided_by: set[VertexId] = tx2_meta.voided_by or set() |
There was a problem hiding this comment.
FIX: I think we must assert tx2_meta.voided_by just like in line 280, instead of doing or set().
There was a problem hiding this comment.
Line 226 on the consensus.py file also does or set() in a similar situation. So which one is correct?
There was a problem hiding this comment.
Since this relates to code that already existed in the consensus before nano, I'm demoting this to a postponed thread.
| for tx_affected in context.txs_affected: | ||
| if not tx_affected.is_nano_contract(): | ||
| # Not a nano tx? Skip! | ||
| continue | ||
| if tx_affected.get_metadata().first_block: | ||
| # Not in mempool? Skip! | ||
| continue | ||
| assert isinstance(tx_affected, Transaction) | ||
| nano_header = tx_affected.get_nano_header() | ||
| try: | ||
| nano_header.get_blueprint_id() | ||
| except NanoContractDoesNotExist: | ||
| from hathor.transaction.validation_state import ValidationState | ||
| tx_affected.set_validation(ValidationState.INVALID) |
There was a problem hiding this comment.
FIX: I think it's bug-prone to use nano_header.get_blueprint_id() and check for NanoContractDoesNotExist. It's hard to reason about all possibilities. It would be better if this check was more explicit.
Maybe we should just add a comment explaining the purpose of this, which is checking whether the nc_id of this tx stopped existing after a reorg. Then we can improve the code later, as this will have to be refactored anyway to remove invalid txs from the storage.
There was a problem hiding this comment.
Added a comment for it in https://github.com/HathorNetwork/nano-hathor-core/pull/266 and cherry-picked it here on commit 39c9299. I'm leaving this thread open though, so we can revisit it during the post-merge pass. So it's a postponed thread.
| assert isinstance(tx_affected, Transaction) | ||
| nano_header = tx_affected.get_nano_header() | ||
| try: | ||
| nano_header.get_blueprint_id() |
There was a problem hiding this comment.
FIX: on a related note, I think there's at least one bug in NanoHeader.get_blueprint_id(). Its first lines are:
if self.is_creating_a_new_contract():
blueprint_id = BlueprintId(NCVertexId(self.nc_id))
return blueprint_idBut what if the initialize method of this contract calls self.syscall.set_blueprint()? It would mean the blueprint_id of this contract is not self.nc_id anymore after the method runs, and therefore the return above would be incorrect.
It doesn't look like this affects this call in the consensus because the tx is guaranteed to be in the mempool, therefore initialize hasn't run yet. But we must check other places that call get_blueprint_id() and check whether they could be affected. I think we should review all usages and all internal logic of this method.
There was a problem hiding this comment.
This bug is unrelated to the consensus and this PR, so I'm demoting it to a postponed thread.
| assert tx.storage is not None | ||
| tx2 = tx.storage.get_transaction(h) | ||
| tx2_meta = tx2.get_metadata() | ||
| tx2_voided_by: set[VertexId] = tx2_meta.voided_by or set() |
There was a problem hiding this comment.
Line 226 on the consensus.py file also does or set() in a similar situation. So which one is correct?
| if not (self.soft_voided_tx_ids & voided_by): | ||
| return voided_by |
| if not (self.soft_voided_tx_ids & voided_by): | ||
| return voided_by |
There was a problem hiding this comment.
Both _filter_out_soft_voided_entries and _filter_out_nc_fail_entries are used only once, in filter_out_voided_by_entries_from_parents, and do basically the same thing. I think they could be unified.
| if cur_meta.nc_block_root_id is not None: | ||
| # Reset nc_block_root_id to force re-execution. | ||
| cur_meta.nc_block_root_id = None |
There was a problem hiding this comment.
FIX: Shouldn't this be an assert cur_meta.nc_block_root_id is not None? Except for the own block that is being handled.
There was a problem hiding this comment.
Postponing thread because I want Marcelo's confirmation.
| assert tx_conflict_meta.first_block is None | ||
| assert tx_conflict_meta.voided_by |
There was a problem hiding this comment.
FIX: Add a comment explaining why these asserts must hold. What if the conflicting tx is also a nano, it could have a first block, no?
There was a problem hiding this comment.
Postponing thread because I want Marcelo's confirmation.
| case NCExecutionState.SKIPPED: | ||
| assert tx_meta.voided_by | ||
| assert self._settings.NC_EXECUTION_FAIL_ID not in tx_meta.voided_by |
There was a problem hiding this comment.
Also assert that at least one (or the only?) reason it's voided_by is a tx that should have NC_EXECUTION_FAIL_ID
| voided_by2.discard(parent.hash) | ||
| voided_by.update(self.context.consensus.filter_out_soft_voided_entries(parent, voided_by2)) | ||
| voided_by.update(self.context.consensus.filter_out_voided_by_entries_from_parents(parent, voided_by2)) | ||
| voided_by.discard(self._settings.NC_EXECUTION_FAIL_ID) |
There was a problem hiding this comment.
Maybe this discard is related to #1290 (comment)
6b09bf8 to
13588c1
Compare
Co-authored-by: Marcelo Salhab Brogliato <msbrogli@gmail.com> Co-authored-by: Jan Segre <jan@hathor.network>
39c9299 to
0519490
Compare
Motivation
Continue the nano merges, updating the consensus.
Acceptance Criteria
Checklist
master, confirm this code is production-ready and can be included in future releases as soon as it gets merged