Felipe Novaes F Rocha 4ec899ec6d feat: handle unknown ops and proofs without halting ingestion
The chain contains op types the explorer has no serializer for (e.g.
ChannelSetKeys, opcode 16), and a single such transaction aborted the
entire backfill. Same approach as #19, adapted to the current tx format:
unrecognized opcodes and proof variants (e.g. the NoProof unit variant)
fall back to an UnknownOp/UnknownSignature pair that preserves the raw
data verbatim. Typed support can be added incrementally; known ops keep
their strict proof-type checks.

Also adds a typed ChannelSetKeys serializer (opcode 16), shaped from a
real op captured on the chain (fixture included) — its DB model already
existed, but with List[bytes] keys, which break the JSON column's utf-8
encoding; changed to HexBytes like every other byte field.
2026-06-12 16:23:04 -03:00

137 lines
4.2 KiB
Python

from abc import ABC, abstractmethod
from typing import Annotated, Any, Optional, Self, Union
from pydantic import BeforeValidator, Field, RootModel
from core.models import NbeSerializer
from models.transactions.operations.proofs import (
Ed25519Signature,
NbeSignature,
UnknownSignature,
ZkAndEd25519Signature,
ZkSignature,
)
from node.api.serializers.fields import BytesFromHex, BytesFromHexOrIntArray
from utils.protocols import EnforceSubclassFromRandom
from utils.random import random_bytes
class OperationProofSerializer(EnforceSubclassFromRandom, ABC):
@abstractmethod
def into_operation_proof(cls) -> NbeSignature:
raise NotImplementedError
class Ed25519SignatureSerializer(OperationProofSerializer, RootModel[bytes]):
root: BytesFromHexOrIntArray
def into_operation_proof(self) -> NbeSignature:
return Ed25519Signature.model_validate(
{
"signature": self.root,
}
)
@classmethod
def from_random(cls, *args, **kwargs) -> Self:
return cls.model_validate(list(random_bytes(64)))
class ZkSignatureSerializer(OperationProofSerializer, NbeSerializer):
"""Groth16 ZK proof: pi_a (32B) + pi_b (64B) + pi_c (32B) = 128 bytes total."""
pi_a: BytesFromHexOrIntArray
pi_b: BytesFromHexOrIntArray
pi_c: BytesFromHexOrIntArray
def to_bytes(self) -> bytes:
return self.pi_a + self.pi_b + self.pi_c
def into_operation_proof(self) -> NbeSignature:
return ZkSignature.model_validate(
{
"signature": self.to_bytes(),
}
)
@classmethod
def from_random(cls, *args, **kwargs) -> Self:
return cls.model_validate(
{
"pi_a": list(random_bytes(32)),
"pi_b": list(random_bytes(64)),
"pi_c": list(random_bytes(32)),
}
)
class ZkAndEd25519SignaturesSerializer(OperationProofSerializer, NbeSerializer):
zk_signature: ZkSignatureSerializer = Field(alias="zk_sig")
ed25519_signature: BytesFromHex = Field(alias="ed25519_sig")
def into_operation_proof(self) -> NbeSignature:
return ZkAndEd25519Signature.model_validate(
{
"zk_signature": self.zk_signature.to_bytes(),
"ed25519_signature": self.ed25519_signature,
}
)
@classmethod
def from_random(cls, *args, **kwargs) -> Self:
return ZkAndEd25519SignaturesSerializer.model_validate(
{
"zk_sig": {
"pi_a": list(random_bytes(32)),
"pi_b": list(random_bytes(64)),
"pi_c": list(random_bytes(32)),
},
"ed25519_sig": random_bytes(64).hex(),
}
)
class UnknownProofSerializer(OperationProofSerializer, NbeSerializer):
"""Fallback for proof variants without a typed serializer (e.g. NoProof).
Preserves the raw value verbatim so unknown proof types never break block
ingestion.
"""
raw: Optional[Any] = None
def into_operation_proof(self) -> NbeSignature:
return UnknownSignature.model_validate({"raw": self.raw})
@classmethod
def from_random(cls, *args, **kwargs) -> Self:
return cls.model_validate({"raw": "NoProof"})
PROOF_TAG_TO_SERIALIZER = {
"Ed25519Sig": Ed25519SignatureSerializer,
"ZkSig": ZkSignatureSerializer,
"ZkAndEd25519Sigs": ZkAndEd25519SignaturesSerializer,
}
def _parse_proof(data: Any) -> OperationProofSerializer:
if isinstance(data, OperationProofSerializer):
return data
if isinstance(data, dict):
for tag, serializer_class in PROOF_TAG_TO_SERIALIZER.items():
if tag in data:
return serializer_class.model_validate(data[tag])
# Unit variants (e.g. "NoProof") arrive as plain strings; unknown tagged
# variants arrive as dicts that matched no known tag. Keep them verbatim.
return UnknownProofSerializer.model_validate({"raw": data})
OperationProofSerializerVariants = Union[
Ed25519SignatureSerializer, ZkSignatureSerializer, ZkAndEd25519SignaturesSerializer, UnknownProofSerializer
]
OperationProofSerializerField = Annotated[
OperationProofSerializerVariants,
BeforeValidator(_parse_proof),
]