mirror of
https://github.com/logos-blockchain/logos-blockchain-block-explorer-template.git
synced 2026-07-03 05:39:27 +00:00
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:
parent
93d8ec76f4
commit
6945f45f4a
@ -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)
|
||||
|
||||
@ -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)
|
||||
]
|
||||
|
||||
@ -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
37
tests/fixtures/block_new_format.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user