diff --git a/hathor/transaction/headers/nano_header.py b/hathor/transaction/headers/nano_header.py index 589e7488e..55bf608b0 100644 --- a/hathor/transaction/headers/nano_header.py +++ b/hathor/transaction/headers/nano_header.py @@ -251,7 +251,7 @@ def get_contract_id(self) -> ContractId: return ContractId(VertexId(self.tx.hash)) return ContractId(VertexId(self.nc_id)) - def get_blueprint_id(self, block: Block | None = None) -> BlueprintId: + def get_blueprint_id(self, block: Block | None = None, *, accept_failed_execution: bool = False) -> BlueprintId: """Return the blueprint id.""" from hathor.nanocontracts.exception import NanoContractDoesNotExist from hathor.nanocontracts.types import BlueprintId, ContractId, VertexId as NCVertexId @@ -294,7 +294,8 @@ def get_blueprint_id(self, block: Block | None = None) -> BlueprintId: # otherwise, it failed or skipped execution from hathor.transaction.nc_execution_state import NCExecutionState assert nc_creation_meta.nc_execution in (NCExecutionState.FAILURE, NCExecutionState.SKIPPED) - raise NanoContractDoesNotExist + if not accept_failed_execution: + raise NanoContractDoesNotExist(f'contract creation is not executed: {self.nc_id.hex()}') blueprint_id = BlueprintId(NCVertexId(nc_creation.get_nano_header().nc_id)) return blueprint_id diff --git a/hathor/transaction/transaction.py b/hathor/transaction/transaction.py index b233b4891..64e14c6f2 100644 --- a/hathor/transaction/transaction.py +++ b/hathor/transaction/transaction.py @@ -304,7 +304,7 @@ def to_json(self, decode_script: bool = False, include_metadata: bool = False) - nano_header = self.get_nano_header() json['nc_id'] = nano_header.get_contract_id().hex() json['nc_seqnum'] = nano_header.nc_seqnum - json['nc_blueprint_id'] = nano_header.get_blueprint_id().hex() + json['nc_blueprint_id'] = nano_header.get_blueprint_id(accept_failed_execution=True).hex() json['nc_method'] = nano_header.nc_method json['nc_args'] = nano_header.nc_args_bytes.hex() json['nc_address'] = get_address_b58_from_bytes(nano_header.nc_address) diff --git a/tests/nanocontracts/test_voided_contract_serialization.py b/tests/nanocontracts/test_voided_contract_serialization.py new file mode 100644 index 000000000..5d5974af4 --- /dev/null +++ b/tests/nanocontracts/test_voided_contract_serialization.py @@ -0,0 +1,83 @@ +from hathor.nanocontracts import NC_EXECUTION_FAIL_ID, Blueprint, Context, public +from hathor.nanocontracts.exception import NCFail +from hathor.transaction import Block, Transaction +from hathor.transaction.nc_execution_state import NCExecutionState +from tests.dag_builder.builder import TestDAGBuilder +from tests.nanocontracts.blueprints.unittest import BlueprintTestCase + + +class FailingInitializeBlueprint(Blueprint): + @public + def initialize(self, ctx: Context) -> None: + raise NCFail('boom') + + @public + def nop(self, ctx: Context) -> None: + pass + + +class VoidedContractSerializationTest(BlueprintTestCase): + def setUp(self) -> None: + super().setUp() + self.fail_blueprint_id = self._register_blueprint_class(FailingInitializeBlueprint) + + def test_to_json_extended_for_voided_contract_call(self) -> None: + dag_builder = TestDAGBuilder.from_manager(self.manager) + artifacts = dag_builder.build_from_str(f''' + blockchain genesis b[1..12] + b10 < dummy + + nc_fail.nc_id = "{self.fail_blueprint_id.hex()}" + nc_fail.nc_method = initialize() + + call.nc_id = nc_fail + call.nc_method = nop() + call.nc_address = wallet1 + call.nc_seqnum = 0 + + nc_fail < call + nc_fail <-- b11 + b11 < call + call <-- b12 + ''') + # stop right after adding 'b11', and thus before 'call' and 'b12' + artifacts.propagate_with(self.manager, up_to='b11') + + nc_fail, call_tx = artifacts.get_typed_vertices(['nc_fail', 'call'], Transaction) + b12 = artifacts.get_typed_vertex('b12', Block) + + assert nc_fail.get_metadata().nc_execution == NCExecutionState.FAILURE + assert nc_fail.get_metadata().first_block == artifacts.get_typed_vertex('b11', Block).hash + + # sanity check call_tx and b12 should not have been validated yet + assert call_tx.storage is None + assert call_tx.get_metadata().validation.is_initial() + assert b12.storage is None + assert b12.get_metadata().validation.is_initial() + + # manually add call_tx as if it was received from the push_tx endpoint + # XXX: in the future if this has to be refactored check `hathor/transaction/resources/push_tx.py` and mimick it + call_tx.storage = self.manager.tx_storage + self.manager.push_tx(call_tx, allow_non_standard_script=True) + call_meta = call_tx.get_metadata() + assert call_meta.validation.is_valid() + assert call_meta.first_block is None + assert call_meta.voided_by is None + + # now manually add b12 as if it was received from the network + assert self.manager.vertex_handler.on_new_block(b12, deps=[]) + + call_meta = call_tx.get_metadata() + assert call_meta.first_block == b12.hash + assert call_meta.voided_by is not None + assert NC_EXECUTION_FAIL_ID in call_meta.voided_by + + b12_meta = b12.get_metadata() + assert b12_meta.validation.is_valid() + assert b12_meta.voided_by is None + + # extras, this should not fail: + stored_call = self.manager.tx_storage.get_transaction(call_tx.hash) + data = stored_call.to_json_extended() + assert data['nc_id'] == nc_fail.hash_hex + assert data['nc_blueprint_id'] == self.fail_blueprint_id.hex()