From 527873c8c6b238ff7def55aa566796523349a452 Mon Sep 17 00:00:00 2001 From: gusto Date: Thu, 11 Jun 2026 14:15:08 +0300 Subject: [PATCH] Handle unknown operations in explorer --- .../transactions/operations/contents.py | 16 +++++++- src/models/transactions/operations/proofs.py | 7 +++- src/node/api/serializers/operation.py | 15 +++++--- .../api/serializers/signed_transaction.py | 13 ++++++- tests/test_transaction_models.py | 37 +++++++++++++++++++ 5 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 tests/test_transaction_models.py diff --git a/src/models/transactions/operations/contents.py b/src/models/transactions/operations/contents.py index 6888deb..a933db9 100644 --- a/src/models/transactions/operations/contents.py +++ b/src/models/transactions/operations/contents.py @@ -85,6 +85,20 @@ class LeaderClaim(NbeContent): mantle_tx_hash: HexBytes +class UnknownOp(NbeContent): + type: Literal["UnknownOp"] = "UnknownOp" + opcode: int | str + raw_payload: dict | str + + OperationContent = ( - LedgerTransfer | ChannelInscribe | ChannelBlob | ChannelSetKeys | SDPDeclare | SDPWithdraw | SDPActive | LeaderClaim + LedgerTransfer + | ChannelInscribe + | ChannelBlob + | ChannelSetKeys + | SDPDeclare + | SDPWithdraw + | SDPActive + | LeaderClaim + | UnknownOp ) diff --git a/src/models/transactions/operations/proofs.py b/src/models/transactions/operations/proofs.py index a27b145..62b131d 100644 --- a/src/models/transactions/operations/proofs.py +++ b/src/models/transactions/operations/proofs.py @@ -31,4 +31,9 @@ class ZkAndEd25519Signature(NbeSignature): ed25519_signature: HexBytes -OperationProof = Ed25519Signature | ZkSignature | ZkAndEd25519Signature +class UnknownSignature(NbeSignature): + type: Literal["Unknown"] = "Unknown" + signature: HexBytes + + +OperationProof = Ed25519Signature | ZkSignature | ZkAndEd25519Signature | UnknownSignature diff --git a/src/node/api/serializers/operation.py b/src/node/api/serializers/operation.py index e03ee81..9571b8d 100644 --- a/src/node/api/serializers/operation.py +++ b/src/node/api/serializers/operation.py @@ -51,6 +51,11 @@ class ChannelInscribeOpSerializer(NbeSerializer, FromRandom): } ) +class UnknownOpSerializer(NbeSerializer): + """Fallback serializer for unrecognized opcodes.""" + opcode: int + payload: dict[str, Any] = Field(default_factory=dict) + OPCODE_TO_SERIALIZER: dict[int, type] = { OPCODE_LEDGER: LedgerOpSerializer, @@ -58,19 +63,17 @@ OPCODE_TO_SERIALIZER: dict[int, type] = { } -def _parse_mantle_op(data: Any) -> Union[LedgerOpSerializer, ChannelInscribeOpSerializer]: - if isinstance(data, (LedgerOpSerializer, ChannelInscribeOpSerializer)): +def _parse_mantle_op(data: Any) -> Any: + if isinstance(data, (LedgerOpSerializer, ChannelInscribeOpSerializer, UnknownOpSerializer)): return data if isinstance(data, dict) and "opcode" in data: opcode = data["opcode"] serializer_class = OPCODE_TO_SERIALIZER.get(opcode) if serializer_class is None: - raise ValueError( - f"Unsupported mantle op opcode {opcode}; known opcodes: {sorted(OPCODE_TO_SERIALIZER)}." - ) + return UnknownOpSerializer(opcode=opcode, payload=data.get("payload", {})) return serializer_class.model_validate(data["payload"]) raise ValueError(f"Cannot parse mantle op from {type(data).__name__}.") -MantleOpSerializerVariants = Union[LedgerOpSerializer, ChannelInscribeOpSerializer] +MantleOpSerializerVariants = Union[LedgerOpSerializer, ChannelInscribeOpSerializer, UnknownOpSerializer] MantleOpSerializerField = Annotated[MantleOpSerializerVariants, BeforeValidator(_parse_mantle_op)] diff --git a/src/node/api/serializers/signed_transaction.py b/src/node/api/serializers/signed_transaction.py index debb8c1..36b06c2 100644 --- a/src/node/api/serializers/signed_transaction.py +++ b/src/node/api/serializers/signed_transaction.py @@ -80,7 +80,18 @@ class SignedTransactionSerializer(NbeSerializer, FromRandom): } ) else: - raise ValueError(f"Unsupported mantle op type: {type(op).__name__}") + # Gracefully handle unknown ops. We assume that whatever comes from the node is correct. + operations.append({ + "content": { + "type": "UnknownOp", + "opcode": getattr(op, "opcode", "unknown"), + "raw_payload": op.model_dump() if hasattr(op, "model_dump") else str(op), + }, + "proof": { + "type": "Unknown", + "signature": proof.to_bytes() if hasattr(proof, "to_bytes") else getattr(proof, "root", b""), + }, + }) return Transaction.model_validate( { diff --git a/tests/test_transaction_models.py b/tests/test_transaction_models.py new file mode 100644 index 0000000..3183bc6 --- /dev/null +++ b/tests/test_transaction_models.py @@ -0,0 +1,37 @@ +import pytest +from models.transactions.transaction import Transaction +from core.types import HexBytes + +def test_transaction_validation_with_unknown_types(): + # This data simulates what is produced by SignedTransactionSerializer.into_transaction() + # when it encounters unknown opcodes or proofs. + transaction_data = { + "hash": HexBytes(b"\x01" * 32), + "operations": [ + { + "content": { + "type": "UnknownOp", + "opcode": 123, + "raw_payload": {"some": "data"} + }, + "proof": { + "type": "Unknown", + "signature": HexBytes(b"\x02" * 64) + } + } + ], + "execution_gas_price": 100, + "storage_gas_price": 200 + } + + tx = Transaction.model_validate(transaction_data) + + assert tx.hash == HexBytes(b"\x01" * 32) + assert tx.operations[0].content.type == "UnknownOp" + assert tx.operations[0].content.opcode == 123 + assert tx.operations[0].proof.type == "Unknown" + assert tx.operations[0].proof.signature == HexBytes(b"\x02" * 64) + +if __name__ == "__main__": + test_transaction_validation_with_unknown_types() + print("Test passed!")