2026-02-13 01:03:47 +04:00
|
|
|
import hashlib
|
|
|
|
|
import json
|
2025-10-30 11:48:34 +01:00
|
|
|
from typing import List, Self
|
|
|
|
|
|
|
|
|
|
from pydantic import Field
|
|
|
|
|
|
|
|
|
|
from core.models import NbeSerializer
|
|
|
|
|
from models.transactions.transaction import Transaction
|
2026-02-05 12:41:42 +04:00
|
|
|
from node.api.serializers.fields import BytesFromIntArray
|
2025-10-30 11:48:34 +01:00
|
|
|
from node.api.serializers.proof import (
|
|
|
|
|
OperationProofSerializer,
|
|
|
|
|
OperationProofSerializerField,
|
|
|
|
|
)
|
|
|
|
|
from node.api.serializers.transaction import TransactionSerializer
|
|
|
|
|
from utils.protocols import FromRandom
|
|
|
|
|
from utils.random import random_bytes
|
|
|
|
|
|
|
|
|
|
|
2026-02-05 12:41:42 +04:00
|
|
|
class Groth16ProofSerializer(NbeSerializer, FromRandom):
|
|
|
|
|
pi_a: BytesFromIntArray
|
|
|
|
|
pi_b: BytesFromIntArray
|
|
|
|
|
pi_c: BytesFromIntArray
|
|
|
|
|
|
|
|
|
|
def to_bytes(self) -> bytes:
|
|
|
|
|
return self.pi_a + self.pi_b + self.pi_c
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_random(cls) -> Self:
|
|
|
|
|
return cls.model_validate(
|
|
|
|
|
{
|
|
|
|
|
"pi_a": list(random_bytes(32)),
|
|
|
|
|
"pi_b": list(random_bytes(64)),
|
|
|
|
|
"pi_c": list(random_bytes(32)),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2025-10-30 11:48:34 +01:00
|
|
|
class SignedTransactionSerializer(NbeSerializer, FromRandom):
|
|
|
|
|
transaction: TransactionSerializer = Field(alias="mantle_tx", description="Transaction.")
|
|
|
|
|
operations_proofs: List[OperationProofSerializerField] = Field(
|
|
|
|
|
alias="ops_proofs", description="List of OperationProof. Order should match `Self::transaction::operations`."
|
|
|
|
|
)
|
2026-02-05 12:41:42 +04:00
|
|
|
ledger_transaction_proof: Groth16ProofSerializer = Field(alias="ledger_tx_proof", description="Groth16 proof.")
|
2025-10-30 11:48:34 +01:00
|
|
|
|
2026-02-13 01:03:47 +04:00
|
|
|
def _compute_hash(self) -> bytes:
|
|
|
|
|
data = self.transaction.model_dump(mode="json")
|
|
|
|
|
canonical = json.dumps(data, sort_keys=True, separators=(",", ":"))
|
|
|
|
|
return hashlib.sha256(canonical.encode()).digest()
|
|
|
|
|
|
2025-10-30 11:48:34 +01:00
|
|
|
def into_transaction(self) -> Transaction:
|
|
|
|
|
operations_contents = self.transaction.operations_contents
|
|
|
|
|
if len(operations_contents) != len(self.operations_proofs):
|
|
|
|
|
raise ValueError(
|
|
|
|
|
f"Number of operations ({len(operations_contents)}) does not match number of operation proofs ({len(self.operations_proofs)})."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
operations = [
|
|
|
|
|
{
|
|
|
|
|
"content": content.into_operation_content(),
|
|
|
|
|
"proof": proof.into_operation_proof(),
|
|
|
|
|
}
|
|
|
|
|
for content, proof in zip(operations_contents, self.operations_proofs)
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
ledger_transaction = self.transaction.ledger_transaction
|
|
|
|
|
outputs = [output.into_note() for output in ledger_transaction.outputs]
|
|
|
|
|
|
|
|
|
|
return Transaction.model_validate(
|
|
|
|
|
{
|
2026-02-13 01:03:47 +04:00
|
|
|
"hash": self._compute_hash(),
|
2025-10-30 11:48:34 +01:00
|
|
|
"operations": operations,
|
|
|
|
|
"inputs": ledger_transaction.inputs,
|
|
|
|
|
"outputs": outputs,
|
2026-02-05 12:41:42 +04:00
|
|
|
"proof": self.ledger_transaction_proof.to_bytes(),
|
2025-10-30 11:48:34 +01:00
|
|
|
"execution_gas_price": self.transaction.execution_gas_price,
|
|
|
|
|
"storage_gas_price": self.transaction.storage_gas_price,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_random(cls) -> Self:
|
|
|
|
|
transaction = TransactionSerializer.from_random()
|
|
|
|
|
n = len(transaction.operations_contents)
|
|
|
|
|
operations_proofs = [OperationProofSerializer.from_random() for _ in range(n)]
|
|
|
|
|
return cls.model_validate(
|
2026-02-05 12:41:42 +04:00
|
|
|
{"mantle_tx": transaction, "ops_proofs": operations_proofs, "ledger_tx_proof": Groth16ProofSerializer.from_random()}
|
2025-10-30 11:48:34 +01:00
|
|
|
)
|