Sync block structure with latest state

This commit is contained in:
Antonio Antonino 2025-12-19 16:48:21 +01:00
parent 6fa406f83f
commit cce312ecb6
No known key found for this signature in database
GPG Key ID: 70CC1DF6BCF7E76D
7 changed files with 182 additions and 77 deletions

View File

@ -1,4 +1,4 @@
from typing import Annotated
from typing import Annotated, Union
from pydantic import BeforeValidator, PlainSerializer, ValidationError
@ -28,6 +28,20 @@ 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: Union[str, list[int]]) -> bytes:
"""Accepts either hex string or int array and converts to bytes."""
if isinstance(data, str):
return bytes.fromhex(data)
elif isinstance(data, list):
if not all(isinstance(item, int) for item in data):
raise ValueError("List items must be integers.")
return bytes(data)
else:
raise ValueError(
f"Unsupported data type for bytes deserialization. Expected string or list, got {type(data).__name__}."
)
def bytes_into_hex(data: bytes) -> str:
return data.hex()
@ -35,3 +49,4 @@ 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)]
BytesFlexible = Annotated[bytes, BeforeValidator(bytes_from_hex_or_intarray), PlainSerializer(bytes_into_hex)]

View File

@ -1,8 +1,9 @@
from enum import IntEnum
from random import randint
from typing import Self
from typing import Optional, Self
from pydantic import Field
from rusty_results import Option, Some
from pydantic import Field, computed_field
from rusty_results import Option
from core.models import NbeSerializer
from node.api.serializers.fields import BytesFromHex
@ -14,21 +15,33 @@ from utils.protocols import FromRandom
from utils.random import random_hash
class Version(IntEnum):
Bedrock = 1
class HeaderSerializer(NbeSerializer, FromRandom):
hash: BytesFromHex = Field(alias="id", description="Hash id in hex format.")
id: Optional[BytesFromHex] = Field(default=None, description="Header ID hash in hex format.")
version: Version = Field(default=Version.Bedrock, description="Block version.")
parent_block: BytesFromHex = Field(description="Hash in hex format.")
slot: int = Field(description="Integer in u64 format.")
block_root: BytesFromHex = Field(description="Hash in hex format.")
proof_of_leadership: ProofOfLeadershipSerializerField
@computed_field
@property
def hash(self) -> bytes:
"""Return the header hash (id if available, otherwise block_root)."""
return self.id if self.id is not None else self.block_root
@classmethod
def from_random(cls, *, slot: Option[int]) -> Self:
return cls.model_validate(
{
"id": random_hash().hex(),
"version": Version.Bedrock,
"parent_block": random_hash().hex(),
"slot": slot.unwrap_or_else(lambda: randint(0, 10_000)),
"block_root": random_hash().hex(),
"proof_of_leadership": ProofOfLeadershipSerializer.from_random(slot=slot),
}
)
)

View File

@ -1,9 +1,9 @@
from abc import ABC, abstractmethod
from enum import Enum
from enum import IntEnum
from random import choice, randint
from typing import Annotated, List, Optional, Self, Union
from typing import Annotated, Any, Dict, List, Optional, Self, Union
from pydantic import Field
from pydantic import Field, model_validator
from core.models import NbeSerializer
from models.transactions.operations.contents import (
@ -16,7 +16,7 @@ from models.transactions.operations.contents import (
SDPDeclare,
SDPWithdraw,
)
from node.api.serializers.fields import BytesFromHex, BytesFromInt, BytesFromIntArray
from node.api.serializers.fields import BytesFlexible, BytesFromHex, BytesFromInt, BytesFromIntArray
from utils.protocols import EnforceSubclassFromRandom
from utils.random import random_bytes
@ -28,9 +28,9 @@ class OperationContentSerializer(NbeSerializer, EnforceSubclassFromRandom, ABC):
class ChannelInscribeSerializer(OperationContentSerializer):
channel_id: BytesFromIntArray = Field(description="Bytes as a 32-integer array.")
inscription: BytesFromIntArray = Field(description="Bytes as an integer array.")
parent: BytesFromIntArray = Field(description="Bytes as a 32-integer array.")
channel_id: BytesFlexible = Field(description="Bytes as hex or 32-integer array.")
inscription: BytesFlexible = Field(description="Bytes as hex or integer array.")
parent: BytesFlexible = Field(description="Bytes as hex or 32-integer array.")
signer: BytesFromHex = Field(description="Public Key in hex format.")
def into_operation_content(self) -> ChannelInscribe:
@ -56,11 +56,11 @@ class ChannelInscribeSerializer(OperationContentSerializer):
class ChannelBlobSerializer(OperationContentSerializer):
channel: BytesFromIntArray = Field(description="Bytes as a 32-integer array.")
blob: BytesFromIntArray = Field(description="Bytes as a 32-integer array.")
channel: BytesFlexible = Field(description="Bytes as hex or 32-integer array.")
blob: BytesFlexible = Field(description="Bytes as hex or 32-integer array.")
blob_size: int
da_storage_gas_price: int
parent: BytesFromIntArray = Field(description="Bytes as a 32-integer array.")
parent: BytesFlexible = Field(description="Bytes as hex or 32-integer array.")
signer: BytesFromHex = Field(description="Public Key in hex format.")
def into_operation_content(self) -> ChannelBlob:
@ -90,7 +90,7 @@ class ChannelBlobSerializer(OperationContentSerializer):
class ChannelSetKeysSerializer(OperationContentSerializer):
channel: BytesFromIntArray = Field(description="Bytes as a 32-integer array.")
channel: BytesFlexible = Field(description="Bytes as hex or 32-integer array.")
keys: List[BytesFromHex] = Field(description="List of Public Keys in hex format.")
def into_operation_content(self) -> ChannelSetKeys:
@ -112,9 +112,9 @@ class ChannelSetKeysSerializer(OperationContentSerializer):
)
class SDPDeclareServiceType(Enum):
BN = "BN"
DA = "DA"
class SDPDeclareServiceType(IntEnum):
BN = 0
DA = 1
class SDPDeclareSerializer(OperationContentSerializer):
@ -127,7 +127,7 @@ class SDPDeclareSerializer(OperationContentSerializer):
def into_operation_content(self) -> SDPDeclare:
return SDPDeclare.model_validate(
{
"service_type": self.service_type.value,
"service_type": self.service_type.name,
"locators": self.locators,
"provider_id": self.provider_id,
"zk_id": self.zk_id,
@ -140,7 +140,7 @@ class SDPDeclareSerializer(OperationContentSerializer):
n = 1 if randint(0, 1) <= 0.5 else randint(1, 5)
return cls.model_validate(
{
"service_type": choice(list(SDPDeclareServiceType)).value,
"service_type": choice(list(SDPDeclareServiceType)),
"locators": [random_bytes(32).hex() for _ in range(n)],
"provider_id": list(random_bytes(32)),
"zk_id": random_bytes(32).hex(),
@ -174,7 +174,7 @@ class SDPWithdrawSerializer(OperationContentSerializer):
class SDPActiveSerializer(OperationContentSerializer):
declaration_id: BytesFromIntArray = Field(description="Bytes as a 32-integer array.")
nonce: BytesFromInt
metadata: Optional[BytesFromIntArray] = Field(description="Bytes as an integer array.")
metadata: Optional[BytesFromIntArray] = Field(default=None, description="Bytes as an integer array.")
def into_operation_content(self) -> SDPActive:
return SDPActive.model_validate(
@ -221,6 +221,59 @@ class LeaderClaimSerializer(OperationContentSerializer):
)
class OpCode(IntEnum):
ChannelInscribe = 0
ChannelBlob = 1
ChannelSetKeys = 2
SDPDeclare = 3
SDPWithdraw = 4
SDPActive = 5
LeaderClaim = 6
# Map opcode to serializer class
OPCODE_TO_SERIALIZER: Dict[int, type[OperationContentSerializer]] = {
OpCode.ChannelInscribe: ChannelInscribeSerializer,
OpCode.ChannelBlob: ChannelBlobSerializer,
OpCode.ChannelSetKeys: ChannelSetKeysSerializer,
OpCode.SDPDeclare: SDPDeclareSerializer,
OpCode.SDPWithdraw: SDPWithdrawSerializer,
OpCode.SDPActive: SDPActiveSerializer,
OpCode.LeaderClaim: LeaderClaimSerializer,
}
class OperationWrapper(NbeSerializer):
"""Wrapper for operations with opcode and payload structure."""
opcode: int
payload: Dict[str, Any]
_content: Optional[OperationContentSerializer] = None
@model_validator(mode="after")
def parse_payload(self) -> Self:
serializer_cls = OPCODE_TO_SERIALIZER.get(self.opcode)
if serializer_cls is None:
raise ValueError(f"Unknown opcode: {self.opcode}")
self._content = serializer_cls.model_validate(self.payload)
return self
def into_operation_content(self) -> NbeContent:
if self._content is None:
raise ValueError("Content not parsed")
return self._content.into_operation_content()
@classmethod
def from_random(cls) -> Self:
opcode = choice(list(OpCode))
serializer_cls = OPCODE_TO_SERIALIZER[opcode]
content = serializer_cls.from_random()
return cls.model_validate({
"opcode": opcode,
"payload": content.model_dump(),
})
type OperationContentSerializerVariants = Union[
ChannelInscribeSerializer,
ChannelBlobSerializer,
@ -230,4 +283,4 @@ type OperationContentSerializerVariants = Union[
SDPActiveSerializer,
LeaderClaimSerializer,
]
OperationContentSerializerField = Annotated[OperationContentSerializerVariants, Field(union_mode="left_to_right")]
OperationContentSerializerField = Annotated[OperationContentSerializerVariants, Field(union_mode="left_to_right")]

View File

@ -1,7 +1,7 @@
from abc import ABC, abstractmethod
from typing import Annotated, Self, Union
from typing import Annotated, Any, Dict, Self, Union
from pydantic import Field, RootModel
from pydantic import Field, RootModel, model_validator
from core.models import NbeSerializer
from models.transactions.operations.proofs import (
@ -10,7 +10,7 @@ from models.transactions.operations.proofs import (
ZkAndEd25519Signature,
ZkSignature,
)
from node.api.serializers.fields import BytesFromHex
from node.api.serializers.fields import BytesFromHex, BytesFromIntArray
from utils.protocols import EnforceSubclassFromRandom
from utils.random import random_bytes
@ -21,57 +21,77 @@ class OperationProofSerializer(EnforceSubclassFromRandom, ABC):
raise NotImplementedError
# TODO: Differentiate between Ed25519SignatureSerializer and ZkSignatureSerializer
class Ed25519SignatureSerializer(OperationProofSerializer, RootModel[str]):
root: BytesFromHex
class Ed25519SignatureSerializer(OperationProofSerializer, NbeSerializer):
"""Ed25519 signature as int array, wrapped in Ed25519Sig key."""
signature: BytesFromIntArray = Field(alias="Ed25519Sig")
def into_operation_proof(self) -> NbeSignature:
return Ed25519Signature.model_validate(
{
"signature": self.root,
"signature": self.signature,
}
)
@classmethod
def from_random(cls, *args, **kwargs) -> Self:
return cls.model_validate(random_bytes(64).hex())
return cls.model_validate({"Ed25519Sig": list(random_bytes(64))})
class ZkSignatureSerializer(OperationProofSerializer, RootModel[str]):
root: BytesFromHex
class ZkSignatureComponentsSerializer(NbeSerializer):
"""ZK signature proof with pi_a, pi_b, pi_c components as int arrays."""
pi_a: BytesFromIntArray = Field(description="32 bytes as int array")
pi_b: BytesFromIntArray = Field(description="64 bytes as int array")
pi_c: BytesFromIntArray = Field(description="32 bytes as int array")
class ZkSignatureSerializer(OperationProofSerializer, NbeSerializer):
"""ZK signature wrapped in ZkSig key."""
zk_sig: ZkSignatureComponentsSerializer = Field(alias="ZkSig")
def into_operation_proof(self) -> NbeSignature:
# Concatenate the components for storage
signature = self.zk_sig.pi_a + self.zk_sig.pi_b + self.zk_sig.pi_c
return ZkSignature.model_validate(
{
"signature": self.root,
"signature": signature,
}
)
@classmethod
def from_random(cls, *args, **kwargs) -> Self:
return cls.model_validate(random_bytes(32).hex())
return cls.model_validate({
"ZkSig": {
"pi_a": list(random_bytes(32)),
"pi_b": list(random_bytes(64)),
"pi_c": list(random_bytes(32)),
}
})
class ZkAndEd25519SignaturesSerializer(OperationProofSerializer, NbeSerializer):
zk_signature: BytesFromHex = Field(alias="zk_sig")
ed25519_signature: BytesFromHex = Field(alias="ed25519_sig")
"""Combined ZK and Ed25519 signatures."""
zk_signature: ZkSignatureComponentsSerializer = Field(alias="zk_sig")
ed25519_signature: BytesFromIntArray = Field(alias="ed25519_sig")
def into_operation_proof(self) -> NbeSignature:
zk_sig = self.zk_signature.pi_a + self.zk_signature.pi_b + self.zk_signature.pi_c
return ZkAndEd25519Signature.model_validate(
{
"zk_signature": self.zk_signature,
"zk_signature": zk_sig,
"ed25519_signature": self.ed25519_signature,
}
)
@classmethod
def from_random(cls, *args, **kwargs) -> Self:
return ZkAndEd25519SignaturesSerializer.model_validate(
return cls.model_validate(
{
"zk_sig": random_bytes(32).hex(),
"ed25519_sig": random_bytes(32).hex(),
"zk_sig": {
"pi_a": list(random_bytes(32)),
"pi_b": list(random_bytes(64)),
"pi_c": list(random_bytes(32)),
},
"ed25519_sig": list(random_bytes(64)),
}
)
@ -79,4 +99,4 @@ class ZkAndEd25519SignaturesSerializer(OperationProofSerializer, NbeSerializer):
OperationProofSerializerVariants = Union[
Ed25519SignatureSerializer, ZkSignatureSerializer, ZkAndEd25519SignaturesSerializer
]
OperationProofSerializerField = Annotated[OperationProofSerializerVariants, Field(union_mode="left_to_right")]
OperationProofSerializerField = Annotated[OperationProofSerializerVariants, Field(union_mode="left_to_right")]

View File

@ -1,7 +1,7 @@
from abc import ABC, abstractmethod
from typing import Annotated, Optional, Self, Union
from pydantic import Field
from pydantic import BeforeValidator, Field
from rusty_results import Option
from core.models import NbeSerializer
@ -22,13 +22,11 @@ class ProofOfLeadershipSerializer(NbeSerializer, EnforceSubclassFromRandom, ABC)
class Groth16LeaderProofSerializer(ProofOfLeadershipSerializer, NbeSerializer):
entropy_contribution: BytesFromHex = Field(description="Fr integer.")
leader_key: BytesFromHex = Field(description="Bytes in Integer Array format.")
proof: BytesFromIntArray = Field(
description="Bytes in Integer Array format.",
)
public: Optional[PublicSerializer] = Field(description="Only received if Node is running in dev mode.")
voucher_cm: BytesFromHex = Field(description="Hash.")
proof: BytesFromIntArray = Field(description="Bytes in Integer Array format (128 bytes).")
entropy_contribution: BytesFromHex = Field(description="Fr integer in hex.")
leader_key: BytesFromHex = Field(description="Ed25519PublicKey in hex.")
voucher_cm: BytesFromHex = Field(description="VoucherCm hash in hex.")
public: Optional[PublicSerializer] = Field(default=None, description="Only received if Node is running in dev mode.")
def into_proof_of_leadership(self) -> ProofOfLeadership:
public = self.public.into_public() if self.public else None
@ -46,29 +44,23 @@ class Groth16LeaderProofSerializer(ProofOfLeadershipSerializer, NbeSerializer):
def from_random(cls, *, slot: Option[int]) -> Self:
return cls.model_validate(
{
"proof": list(random_bytes(128)),
"entropy_contribution": random_bytes(32).hex(),
"leader_key": random_bytes(32).hex(),
"proof": list(random_bytes(128)),
"public": PublicSerializer.from_random(slot),
"voucher_cm": random_bytes(32).hex(),
"public": PublicSerializer.from_random(slot),
}
)
# Fake Variant that never resolves to allow union type checking to work
# TODO: Remove this when another Variant is added
from pydantic import BeforeValidator
def _always_fail(_):
raise ValueError("Never matches.")
_NeverType = Annotated[object, BeforeValidator(_always_fail)]
#
ProofOfLeadershipVariants = Union[
Groth16LeaderProofSerializer, _NeverType
] # TODO: Remove _NeverType when another Variant is added
ProofOfLeadershipSerializerField = Annotated[ProofOfLeadershipVariants, Field(union_mode="left_to_right")]
ProofOfLeadershipSerializerField = Annotated[ProofOfLeadershipVariants, Field(union_mode="left_to_right")]

View File

@ -1,14 +1,14 @@
from typing import List, Self
from pydantic import Field
from rusty_results import Option
from core.models import NbeSerializer
from models.transactions.transaction import Transaction
from node.api.serializers.fields import BytesFromHex
from node.api.serializers.fields import BytesFromIntArray
from node.api.serializers.proof import (
OperationProofSerializer,
OperationProofSerializerField,
ZkSignatureComponentsSerializer,
)
from node.api.serializers.transaction import TransactionSerializer
from utils.protocols import FromRandom
@ -20,8 +20,8 @@ class SignedTransactionSerializer(NbeSerializer, FromRandom):
operations_proofs: List[OperationProofSerializerField] = Field(
alias="ops_proofs", description="List of OperationProof. Order should match `Self::transaction::operations`."
)
ledger_transaction_proof: BytesFromHex = Field(
alias="ledger_tx_proof", description="Hash.", min_length=128, max_length=128
ledger_transaction_proof: ZkSignatureComponentsSerializer = Field(
alias="ledger_tx_proof", description="ZK proof with pi_a, pi_b, pi_c."
)
def into_transaction(self) -> Transaction:
@ -42,13 +42,20 @@ class SignedTransactionSerializer(NbeSerializer, FromRandom):
ledger_transaction = self.transaction.ledger_transaction
outputs = [output.into_note() for output in ledger_transaction.outputs]
# Combine pi_a, pi_b, pi_c into single proof bytes
proof_bytes = (
self.ledger_transaction_proof.pi_a +
self.ledger_transaction_proof.pi_b +
self.ledger_transaction_proof.pi_c
)
return Transaction.model_validate(
{
"hash": self.transaction.hash,
"operations": operations,
"inputs": ledger_transaction.inputs,
"outputs": outputs,
"proof": self.ledger_transaction_proof,
"proof": proof_bytes,
"execution_gas_price": self.transaction.execution_gas_price,
"storage_gas_price": self.transaction.storage_gas_price,
}
@ -60,5 +67,13 @@ class SignedTransactionSerializer(NbeSerializer, FromRandom):
n = len(transaction.operations_contents)
operations_proofs = [OperationProofSerializer.from_random() for _ in range(n)]
return cls.model_validate(
{"mantle_tx": transaction, "ops_proofs": operations_proofs, "ledger_tx_proof": random_bytes(128).hex()}
)
{
"mantle_tx": transaction,
"ops_proofs": operations_proofs,
"ledger_tx_proof": {
"pi_a": list(random_bytes(32)),
"pi_b": list(random_bytes(64)),
"pi_c": list(random_bytes(32)),
},
}
)

View File

@ -6,17 +6,14 @@ from pydantic import Field
from core.models import NbeSerializer
from node.api.serializers.fields import BytesFromHex
from node.api.serializers.ledger_transaction import LedgerTransactionSerializer
from node.api.serializers.operation import (
OperationContentSerializer,
OperationContentSerializerField,
)
from node.api.serializers.operation import OperationWrapper
from utils.protocols import FromRandom
from utils.random import random_bytes
class TransactionSerializer(NbeSerializer, FromRandom):
hash: BytesFromHex = Field(description="Hash id in hex format.")
operations_contents: List[OperationContentSerializerField] = Field(alias="ops")
hash: BytesFromHex = Field(description="Transaction hash in hex format.")
operations_contents: List[OperationWrapper] = Field(alias="ops")
ledger_transaction: LedgerTransactionSerializer = Field(alias="ledger_tx")
execution_gas_price: int = Field(description="Integer in u64 format.")
storage_gas_price: int = Field(description="Integer in u64 format.")
@ -24,7 +21,7 @@ class TransactionSerializer(NbeSerializer, FromRandom):
@classmethod
def from_random(cls) -> Self:
n = 0 if randint(0, 1) <= 0.5 else randint(1, 5)
operations_contents = [OperationContentSerializer.from_random() for _ in range(n)]
operations_contents = [OperationWrapper.from_random() for _ in range(n)]
return cls.model_validate(
{
"hash": random_bytes(32).hex(),
@ -33,4 +30,4 @@ class TransactionSerializer(NbeSerializer, FromRandom):
"execution_gas_price": randint(1, 10_000),
"storage_gas_price": randint(1, 10_000),
}
)
)