From 2045b8df6f9874387b0d6eb9ceaceb81e012171b Mon Sep 17 00:00:00 2001 From: David Rusu Date: Wed, 6 May 2026 18:31:16 -0400 Subject: [PATCH] update schemas to v0.1.3rc5 --- .../transactions/operations/contents.py | 8 +-- src/node/api/http.py | 13 ++-- src/node/api/serializers/operation.py | 66 +++++++++++++++++-- src/node/api/serializers/proof.py | 16 +++-- .../api/serializers/proof_of_leadership.py | 10 ++- .../api/serializers/signed_transaction.py | 51 ++++++++++++-- 6 files changed, 127 insertions(+), 37 deletions(-) diff --git a/src/models/transactions/operations/contents.py b/src/models/transactions/operations/contents.py index 6888deb..0aa4932 100644 --- a/src/models/transactions/operations/contents.py +++ b/src/models/transactions/operations/contents.py @@ -1,5 +1,5 @@ from enum import Enum -from typing import List, Literal, Optional +from typing import Any, List, Literal, Optional from core.models import NbeSchema from core.types import HexBytes @@ -59,7 +59,7 @@ class SDPDeclareServiceType(Enum): class SDPDeclare(NbeContent): type: Literal["SDPDeclare"] = "SDPDeclare" service_type: SDPDeclareServiceType - locators: List[bytes] + locators: List[str] provider_id: HexBytes zk_id: HexBytes locked_note_id: HexBytes @@ -74,8 +74,8 @@ class SDPWithdraw(NbeContent): class SDPActive(NbeContent): type: Literal["SDPActive"] = "SDPActive" declaration_id: HexBytes - nonce: HexBytes - metadata: Optional[bytes] + nonce: int + metadata: Optional[Any] = None class LeaderClaim(NbeContent): diff --git a/src/node/api/http.py b/src/node/api/http.py index 6f469d8..e5f8944 100644 --- a/src/node/api/http.py +++ b/src/node/api/http.py @@ -25,7 +25,7 @@ class HttpNodeApi(NodeApi): # Paths can't have a leading slash since they are relative to the base URL ENDPOINT_INFO = "cryptarchia/info" ENDPOINT_BLOCKS_STREAM = "cryptarchia/events/blocks/stream" - ENDPOINT_BLOCK_BY_HASH = "storage/block" + ENDPOINT_BLOCK_BY_HASH = "cryptarchia/blocks/" # block hash appended as path segment def __init__(self, settings: "NBESettings"): self.host: str = settings.node_api_host @@ -82,8 +82,8 @@ class HttpNodeApi(NodeApi): return InfoSerializer.model_validate(response.json()) async def get_block_by_hash(self, block_hash: str) -> Optional[BlockSerializer]: - url = urljoin(self.base_url, self.ENDPOINT_BLOCK_BY_HASH) - response = await self._client.post(url, json=block_hash) + url = urljoin(self.base_url, self.ENDPOINT_BLOCK_BY_HASH + block_hash) + response = await self._client.get(url) if response.status_code == 404: return None response.raise_for_status() @@ -91,12 +91,7 @@ class HttpNodeApi(NodeApi): if json_data is None: logger.warning(f"Block {block_hash} returned null from API") return None - block = BlockSerializer.model_validate(json_data) - # The storage endpoint doesn't include the block hash in the response, - # so we set it from the request body - if not block.header.hash: - block.header.hash = bytes.fromhex(block_hash) - return block + return BlockSerializer.model_validate(json_data) async def get_blocks_stream(self) -> AsyncIterator[BlockSerializer]: url = urljoin(self.base_url, self.ENDPOINT_BLOCKS_STREAM) diff --git a/src/node/api/serializers/operation.py b/src/node/api/serializers/operation.py index e03ee81..e5c518b 100644 --- a/src/node/api/serializers/operation.py +++ b/src/node/api/serializers/operation.py @@ -1,17 +1,20 @@ from random import randint -from typing import Annotated, Any, List, Self, Union +from typing import Annotated, Any, List, Optional, Self, Union from pydantic import BeforeValidator, Field from core.models import NbeSerializer +from models.transactions.operations.contents import SDPDeclareServiceType from node.api.serializers.fields import BytesFromHex, BytesFromIntArray from node.api.serializers.note import NoteSerializer from utils.protocols import FromRandom from utils.random import random_bytes -# Mantle op opcodes (new node release). +# Mantle op opcodes. OPCODE_LEDGER = 0 OPCODE_CHANNEL_INSCRIBE = 17 +OPCODE_SDP_DECLARE = 32 +OPCODE_SDP_ACTIVE = 34 class LedgerOpSerializer(NbeSerializer, FromRandom): @@ -52,14 +55,68 @@ class ChannelInscribeOpSerializer(NbeSerializer, FromRandom): ) +class SDPDeclareOpSerializer(NbeSerializer, FromRandom): + """SDP declare op (opcode 32): registers a service provider.""" + + service_type: SDPDeclareServiceType + locators: List[str] = Field(description="Multiaddr strings, e.g. /ip4/.../udp/.../quic-v1.") + provider_id: BytesFromHex = Field(description="Provider ID in hex format.") + zk_id: BytesFromHex = Field(description="ZK ID in hex format.") + locked_note_id: BytesFromHex = Field(description="Locked note ID in hex format.") + + @classmethod + def from_random(cls) -> Self: + return cls.model_validate( + { + "service_type": "BN", + "locators": ["/ip4/127.0.0.1/udp/3400/quic-v1"], + "provider_id": random_bytes(32).hex(), + "zk_id": random_bytes(32).hex(), + "locked_note_id": random_bytes(32).hex(), + } + ) + + +class SDPActiveOpSerializer(NbeSerializer, FromRandom): + """SDP active op (opcode 34): proves a declared provider is online.""" + + declaration_id: BytesFromHex = Field(description="Declaration ID in hex format.") + nonce: int = Field(description="Activity nonce in u64 format.") + metadata: Optional[Any] = Field( + default=None, + description="Service-specific metadata (e.g. Blend session/proofs). Stored verbatim.", + ) + + @classmethod + def from_random(cls) -> Self: + return cls.model_validate( + { + "declaration_id": random_bytes(32).hex(), + "nonce": randint(0, 1_000), + "metadata": None, + } + ) + + OPCODE_TO_SERIALIZER: dict[int, type] = { OPCODE_LEDGER: LedgerOpSerializer, OPCODE_CHANNEL_INSCRIBE: ChannelInscribeOpSerializer, + OPCODE_SDP_DECLARE: SDPDeclareOpSerializer, + OPCODE_SDP_ACTIVE: SDPActiveOpSerializer, } -def _parse_mantle_op(data: Any) -> Union[LedgerOpSerializer, ChannelInscribeOpSerializer]: - if isinstance(data, (LedgerOpSerializer, ChannelInscribeOpSerializer)): +MantleOpSerializerVariants = Union[ + LedgerOpSerializer, + ChannelInscribeOpSerializer, + SDPDeclareOpSerializer, + SDPActiveOpSerializer, +] +_MANTLE_OP_SERIALIZER_CLASSES = tuple(OPCODE_TO_SERIALIZER.values()) + + +def _parse_mantle_op(data: Any) -> MantleOpSerializerVariants: + if isinstance(data, _MANTLE_OP_SERIALIZER_CLASSES): return data if isinstance(data, dict) and "opcode" in data: opcode = data["opcode"] @@ -72,5 +129,4 @@ def _parse_mantle_op(data: Any) -> Union[LedgerOpSerializer, ChannelInscribeOpSe raise ValueError(f"Cannot parse mantle op from {type(data).__name__}.") -MantleOpSerializerVariants = Union[LedgerOpSerializer, ChannelInscribeOpSerializer] MantleOpSerializerField = Annotated[MantleOpSerializerVariants, BeforeValidator(_parse_mantle_op)] diff --git a/src/node/api/serializers/proof.py b/src/node/api/serializers/proof.py index 7566b0c..3e59928 100644 --- a/src/node/api/serializers/proof.py +++ b/src/node/api/serializers/proof.py @@ -10,7 +10,7 @@ from models.transactions.operations.proofs import ( ZkAndEd25519Signature, ZkSignature, ) -from node.api.serializers.fields import BytesFromIntArray +from node.api.serializers.fields import BytesFromHex, BytesFromIntArray from utils.protocols import EnforceSubclassFromRandom from utils.random import random_bytes @@ -65,13 +65,13 @@ class ZkSignatureSerializer(OperationProofSerializer, NbeSerializer): class ZkAndEd25519SignaturesSerializer(OperationProofSerializer, NbeSerializer): - zk_signature: BytesFromIntArray = Field(alias="zk_sig") - ed25519_signature: BytesFromIntArray = Field(alias="ed25519_sig") + 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, + "zk_signature": self.zk_signature.to_bytes(), "ed25519_signature": self.ed25519_signature, } ) @@ -80,8 +80,12 @@ class ZkAndEd25519SignaturesSerializer(OperationProofSerializer, NbeSerializer): def from_random(cls, *args, **kwargs) -> Self: return ZkAndEd25519SignaturesSerializer.model_validate( { - "zk_sig": list(random_bytes(32)), - "ed25519_sig": list(random_bytes(64)), + "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(), } ) diff --git a/src/node/api/serializers/proof_of_leadership.py b/src/node/api/serializers/proof_of_leadership.py index 4c0cccb..2817410 100644 --- a/src/node/api/serializers/proof_of_leadership.py +++ b/src/node/api/serializers/proof_of_leadership.py @@ -9,7 +9,7 @@ from models.header.proof_of_leadership import ( Groth16ProofOfLeadership, ProofOfLeadership, ) -from node.api.serializers.fields import BytesFromHex, BytesFromIntArray +from node.api.serializers.fields import BytesFromHex from utils.protocols import EnforceSubclassFromRandom from utils.random import random_bytes @@ -22,10 +22,8 @@ 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.", - ) + leader_key: BytesFromHex = Field(description="Hash in hex format.") + proof: BytesFromHex = Field(description="Groth16 proof bytes (128B) in hex format.") voucher_cm: BytesFromHex = Field(description="Hash.") def into_proof_of_leadership(self) -> ProofOfLeadership: @@ -44,7 +42,7 @@ class Groth16LeaderProofSerializer(ProofOfLeadershipSerializer, NbeSerializer): { "entropy_contribution": random_bytes(32).hex(), "leader_key": random_bytes(32).hex(), - "proof": list(random_bytes(128)), + "proof": random_bytes(128).hex(), "voucher_cm": random_bytes(32).hex(), } ) diff --git a/src/node/api/serializers/signed_transaction.py b/src/node/api/serializers/signed_transaction.py index debb8c1..89ad9d2 100644 --- a/src/node/api/serializers/signed_transaction.py +++ b/src/node/api/serializers/signed_transaction.py @@ -9,16 +9,33 @@ from models.transactions.transaction import Transaction from node.api.serializers.operation import ( ChannelInscribeOpSerializer, LedgerOpSerializer, + SDPActiveOpSerializer, + SDPDeclareOpSerializer, ) from node.api.serializers.proof import ( Ed25519SignatureSerializer, OperationProofSerializerField, + ZkAndEd25519SignaturesSerializer, ZkSignatureSerializer, ) from node.api.serializers.transaction import TransactionSerializer from utils.protocols import FromRandom +def _proof_to_internal(proof) -> dict: + if isinstance(proof, ZkSignatureSerializer): + return {"type": "Zk", "signature": proof.to_bytes()} + if isinstance(proof, Ed25519SignatureSerializer): + return {"type": "Ed25519", "signature": proof.root} + if isinstance(proof, ZkAndEd25519SignaturesSerializer): + return { + "type": "ZkAndEd25519", + "zk_signature": proof.zk_signature.to_bytes(), + "ed25519_signature": proof.ed25519_signature, + } + raise ValueError(f"Unsupported proof type: {type(proof).__name__}") + + class SignedTransactionSerializer(NbeSerializer, FromRandom): transaction: TransactionSerializer = Field(alias="mantle_tx", description="Transaction.") operations_proofs: List[OperationProofSerializerField] = Field( @@ -53,10 +70,7 @@ class SignedTransactionSerializer(NbeSerializer, FromRandom): "inputs": list(op.inputs), "outputs": [o.into_note() for o in op.outputs], }, - "proof": { - "type": "Zk", - "signature": proof.to_bytes(), - }, + "proof": _proof_to_internal(proof), } ) elif isinstance(op, ChannelInscribeOpSerializer): @@ -73,10 +87,33 @@ class SignedTransactionSerializer(NbeSerializer, FromRandom): "parent": op.parent, "signer": op.signer, }, - "proof": { - "type": "Ed25519", - "signature": proof.root, + "proof": _proof_to_internal(proof), + } + ) + elif isinstance(op, SDPDeclareOpSerializer): + operations.append( + { + "content": { + "type": "SDPDeclare", + "service_type": op.service_type, + "locators": list(op.locators), + "provider_id": op.provider_id, + "zk_id": op.zk_id, + "locked_note_id": op.locked_note_id, }, + "proof": _proof_to_internal(proof), + } + ) + elif isinstance(op, SDPActiveOpSerializer): + operations.append( + { + "content": { + "type": "SDPActive", + "declaration_id": op.declaration_id, + "nonce": op.nonce, + "metadata": op.metadata, + }, + "proof": _proof_to_internal(proof), } ) else: