fix: parse the current node wire format

Recent nodes changed the wire format in three ways that broke block
ingestion (serializers raised on every block, so the DB never populated):

- cryptarchia/info wraps its fields under "cryptarchia_info" and reports
  mode as an object ({"Started": "Online"}). Normalize before validating.
- Byte fields (inscriptions, Ed25519 signatures, Groth16 pi_a/pi_b/pi_c)
  arrive as hex strings instead of int arrays. Accept both encodings via
  BytesFromHexOrIntArray, since the encoding has drifted between releases.
- mantle_tx now carries its canonical hash and dropped the gas price
  fields. Prefer the node-provided hash (a locally computed JSON hash does
  not match the chain's real tx hash) and default gas prices to 0.

Tested with a real block fixture captured from a node.
This commit is contained in:
Felipe Novaes F Rocha 2026-06-12 14:59:40 -03:00
parent 93d8ec76f4
commit 6945f45f4a
4 changed files with 74 additions and 4 deletions

View File

@ -21,6 +21,21 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
def normalize_info_payload(data: dict) -> dict:
"""Normalize cryptarchia/info responses.
Current nodes wrap the fields under "cryptarchia_info" and report mode as
a (possibly nested) object like {"Started": "Online"}; flat payloads with a
plain string mode pass through unchanged.
"""
info = dict(data.get("cryptarchia_info", data))
mode = data.get("mode", info.get("mode"))
while isinstance(mode, dict):
mode = next(iter(mode.values()), "Unknown") if mode else "Unknown"
info["mode"] = mode if isinstance(mode, str) else str(mode)
return info
class HttpNodeApi(NodeApi):
# Paths can't have a leading slash since they are relative to the base URL
ENDPOINT_INFO = "cryptarchia/info"
@ -79,7 +94,7 @@ class HttpNodeApi(NodeApi):
url = urljoin(self.base_url, self.ENDPOINT_INFO)
response = requests.get(url, auth=self.authentication, timeout=60)
response.raise_for_status()
return InfoSerializer.model_validate(response.json())
return InfoSerializer.model_validate(normalize_info_payload(response.json()))
async def get_block_by_hash(self, block_hash: str) -> Optional[BlockSerializer]:
url = urljoin(self.base_url, self.ENDPOINT_BLOCK_BY_HASH + block_hash)

View File

@ -28,6 +28,17 @@ def bytes_from_int(data: int) -> bytes:
return data.to_bytes((data.bit_length() + 7) // 8) # TODO: Ensure endianness is correct.
def bytes_from_hex_or_intarray(data: str | list[int]) -> bytes:
"""Node versions drifted between int arrays and hex strings for byte fields.
Older nodes (<= 0.1.2) serialize bytes as int arrays; newer nodes serialize
them as hex strings. Accept both so the explorer works across node versions.
"""
if isinstance(data, str):
return bytes_from_hex(data)
return bytes_from_intarray(data)
def bytes_into_hex(data: bytes) -> str:
return data.hex()
@ -35,3 +46,6 @@ def bytes_into_hex(data: bytes) -> str:
BytesFromIntArray = Annotated[bytes, BeforeValidator(bytes_from_intarray), PlainSerializer(bytes_into_hex)]
BytesFromHex = Annotated[bytes, BeforeValidator(bytes_from_hex), PlainSerializer(bytes_into_hex)]
BytesFromInt = Annotated[bytes, BeforeValidator(bytes_from_int), PlainSerializer(bytes_into_hex)]
BytesFromHexOrIntArray = Annotated[
bytes, BeforeValidator(bytes_from_hex_or_intarray), PlainSerializer(bytes_into_hex)
]

View File

@ -1,17 +1,21 @@
from random import randint
from typing import List, Self
from typing import List, Optional, Self
from pydantic import Field
from core.models import NbeSerializer
from node.api.serializers.fields import BytesFromHex
from node.api.serializers.operation import LedgerOpSerializer, MantleOpSerializerField
from utils.protocols import FromRandom
class TransactionSerializer(NbeSerializer, FromRandom):
ops: List[MantleOpSerializerField]
execution_gas_price: int = Field(description="Integer in u64 format.")
storage_gas_price: int = Field(description="Integer in u64 format.")
# Newer nodes include the canonical tx hash in mantle_tx; older ones don't.
hash: Optional[BytesFromHex] = Field(default=None, description="Canonical tx hash (newer nodes only).")
# Gas prices were dropped from mantle_tx on newer nodes; default to 0 there.
execution_gas_price: int = Field(default=0, description="Integer in u64 format.")
storage_gas_price: int = Field(default=0, description="Integer in u64 format.")
@classmethod
def from_random(cls) -> Self:

37
tests/fixtures/block_new_format.json vendored Normal file
View File

@ -0,0 +1,37 @@
{
"header": {
"id": "d266fec335ecf2bfb713db1c5bf389e21fd80d208a9ef01989da6aef3deae7b4",
"parent_block": "c4afbf0d3a99fbcb624295a6626ad2786ec181e02522467c55489eb72fe81ce1",
"slot": 13240353,
"block_root": "ac4d5d879a87da6b62a1e1c97840a73372cfef8fd8b7b454d5fbef66e21b1365",
"proof_of_leadership": {
"proof": "bf5f85bf2f664ac55bc0116e75d5ba44d36935935ea53df24cc23bdff10b16a87ec8a60cfd8414b29a50e3467f091c00ae7b2ad4edeedfa6c610de50b4051d249c62e8a0b99e99005ba4b1342c524569150bc8cd58a9d31bda86f5a23d7d858e1094b87b9db5866f998e4aea806b9c3c346b945cb10ba2e4b51c22931bbe901a",
"entropy_contribution": "1a5b15d2ef474acf2f1ef7e46e40967f9bd6ccd5bf1877a85350e6294ddad90a",
"leader_key": "a38c8d8d2e560cd21cf9cf856bb7e93fff41fa6a3ab0991945c3761ee7a20d50",
"voucher_cm": "c4ecf502b774020f2a70474720b4de25653fe8ad56853085725dc6c56b00a405"
}
},
"transactions": [
{
"mantle_tx": {
"hash": "9e00ebfc1d641adaeecaf0e20c09ffed78a84fd2f36b790b94cf64f758260348",
"ops": [
{
"opcode": 17,
"payload": {
"channel_id": "0101010101010101010101010101010101010101010101010101010101010101",
"inscription": "44160000000000006508e79b06e19340759182b8c17ee4f490ad8ffea5bf89ad06e6f10e843932af84e707c7cfa10873b5bd312345a6419de751ed433b6cf2fff0f6a1cefb965d422e0910bc9e010000ca2b0f5396f61a85e56f499c16dfe7b9f56ca86e25c5059cc5e685f59b7b1be5e67e9aaae67b3cb667a56e16454d661365e207f9c35453b792ca98abb16702bb0100000000cf8fdba1d3ac5dbd6b28315cfd7eb206c1a8cab5206a1d48ebb3cd26d4d36f68030000002f4c455a2f436c6f636b50726f6772616d4163636f756e742f303030303030312f4c455a2f436c6f636b50726f6772616d4163636f756e742f303030303031302f4c455a2f436c6f636b50726f6772616d4163636f756e742f3030303030353000000000020000002e0910bc9e0100000000000000",
"parent": "77007291cc00a4ad150d79afe4068008ec662dc3f7e1b70358a427cb03ba6de7",
"signer": "7d1726d002f952db5be1a2f534f8b4bafe429c8e56f6a42c8c6f9faa3aa59b06"
}
}
]
},
"ops_proofs": [
{
"Ed25519Sig": "307e8225cc3ccd3ea4a2a0de7509aa90a85453cb9d4fb2f56e1c36f83cd00ebf33805807f45fb6ddb332a1caa1c096ea8a95bb2135a500014174a71a256fa50a"
}
]
}
]
}