tenzro_types/transaction.rs
1//! Transaction types for Tenzro Network
2//!
3//! This module defines the transaction structure and related types used
4//! to represent state transitions on the Tenzro Network blockchain.
5
6use crate::asset::AssetId;
7use crate::primitives::{Address, Hash, Nonce, Signature, Timestamp, ChainId};
8use crate::settlement::{ReleaseConditions, ServiceProof};
9use serde::{Deserialize, Serialize};
10use sha2::{Digest, Sha256};
11
12/// Maximum transaction data size in bytes (128 KB)
13pub const MAX_TX_DATA_SIZE: usize = 131_072;
14
15/// A transaction on Tenzro Network
16///
17/// Transactions represent actions taken by accounts, including transfers,
18/// smart contract calls, agent operations, and model inference requests.
19///
20/// # Note
21/// The `Default` implementation creates a transaction with zero values
22/// and is intended for testing purposes only. Production transactions
23/// should be created with `Transaction::new()` and properly signed.
24///
25/// # Post-quantum migration
26///
27/// The `pq_public_key` field carries the ML-DSA-65 verifying key bytes
28/// (FIPS 204, exactly 1952 bytes) and is **mandatory**. Tenzro Network does
29/// not support classical-only transactions — the field has no `Option`
30/// wrapper and no `serde(default)`. Decoders reject any payload that omits
31/// or mis-sizes this field. There is no legacy fallback: a classical-only
32/// transaction cannot be constructed in this codebase and cannot be parsed
33/// from any external source.
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct Transaction {
36 /// The chain ID this transaction is valid for
37 pub chain_id: ChainId,
38 /// The sender's address
39 pub from: Address,
40 /// The recipient's address (or contract address)
41 pub to: Address,
42 /// The nonce for replay protection
43 pub nonce: Nonce,
44 /// The transaction type and payload
45 pub tx_type: TransactionType,
46 /// Maximum gas to spend
47 pub gas_limit: u64,
48 /// Gas price in the smallest unit of TNZO
49 pub gas_price: u64,
50 /// Transaction timestamp
51 pub timestamp: Timestamp,
52 /// Optional memo or metadata
53 pub memo: Option<String>,
54 /// ML-DSA-65 verifying key bytes (FIPS 204, exactly 1952 bytes).
55 /// Bound into the `hash()` preimage so a hybrid signer commits to the PQ
56 /// key before signing and any key-substitution attempt invalidates the
57 /// transaction.
58 #[serde(deserialize_with = "crate::validation::bounded_pq_public_key_bytes")]
59 pub pq_public_key: Vec<u8>,
60}
61
62impl Transaction {
63 /// Creates a new transaction. The `pq_public_key` is the ML-DSA-65
64 /// verifying key bytes (FIPS 204, exactly 1952 bytes) and is mandatory —
65 /// classical-only transactions are not constructible.
66 pub fn new(
67 chain_id: ChainId,
68 from: Address,
69 to: Address,
70 nonce: Nonce,
71 tx_type: TransactionType,
72 gas_limit: u64,
73 gas_price: u64,
74 pq_public_key: Vec<u8>,
75 ) -> Self {
76 Self {
77 chain_id,
78 from,
79 to,
80 nonce,
81 tx_type,
82 gas_limit,
83 gas_price,
84 timestamp: Timestamp::now(),
85 memo: None,
86 pq_public_key,
87 }
88 }
89
90 /// Adds a memo to the transaction
91 pub fn with_memo(mut self, memo: String) -> Self {
92 self.memo = Some(memo);
93 self
94 }
95
96 /// Computes the SHA-256 hash of the transaction.
97 ///
98 /// The preimage layout is the canonical signing surface for the network.
99 /// The mandatory ML-DSA-65 verifying key (`pq_public_key`) is included
100 /// with an explicit `u32` little-endian length prefix so the preimage
101 /// is unambiguous and a key-substitution attempt invalidates the hash.
102 pub fn hash(&self) -> Hash {
103 let mut hasher = Sha256::new();
104 hasher.update(self.chain_id.0.to_le_bytes());
105 hasher.update(self.from.as_bytes());
106 hasher.update(self.to.as_bytes());
107 hasher.update(self.nonce.0.to_le_bytes());
108 hasher.update(self.gas_limit.to_le_bytes());
109 hasher.update(self.gas_price.to_le_bytes());
110 hasher.update(self.timestamp.0.to_le_bytes());
111 // Include tx_type via JSON serialization for deterministic encoding
112 if let Ok(tx_type_json) = serde_json::to_vec(&self.tx_type) {
113 hasher.update(&tx_type_json);
114 }
115 if let Some(ref memo) = self.memo {
116 hasher.update(memo.as_bytes());
117 }
118 // PQ-binding: explicit length prefix is canonical even though the
119 // field is fixed-size — keeps the preimage shape stable if FIPS 204
120 // ever moves to a variant with a different key length.
121 hasher.update((self.pq_public_key.len() as u32).to_le_bytes());
122 hasher.update(&self.pq_public_key);
123 let result = hasher.finalize();
124 Hash::new(result.into())
125 }
126}
127
128/// The type and payload of a transaction.
129///
130/// Different transaction types enable different operations on Tenzro Network.
131///
132/// Uses serde's default externally-tagged enum representation
133/// (`{"Transfer": {...}}` in JSON, `u32` discriminant + payload in bincode).
134/// Adjacently-tagged form (`#[serde(tag = "type", content = "data")]`) is
135/// incompatible with bincode 1.x — see `tenzro_network::message::MessagePayload`.
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
137pub enum TransactionType {
138 /// Transfer TNZO tokens
139 Transfer {
140 /// Amount to transfer in the smallest unit
141 amount: u128,
142 },
143 /// Deploy a smart contract
144 ContractDeploy {
145 /// Contract bytecode
146 code: Vec<u8>,
147 /// Constructor arguments
148 args: Vec<u8>,
149 },
150 /// Call a smart contract
151 ContractCall {
152 /// Function signature
153 function: String,
154 /// Function arguments
155 args: Vec<u8>,
156 },
157 /// Register an AI agent
158 AgentRegister {
159 /// Agent configuration
160 config: Vec<u8>,
161 },
162 /// Execute an agent task
163 AgentExecute {
164 /// Task specification
165 task: Vec<u8>,
166 },
167 /// Request model inference
168 ModelInference {
169 /// Model identifier
170 model_id: String,
171 /// Inference input
172 input: Vec<u8>,
173 },
174 /// Register as a TEE provider
175 TeeProviderRegister {
176 /// Attestation data
177 attestation: Vec<u8>,
178 /// Provider info
179 info: Vec<u8>,
180 },
181 /// Stake tokens for provider operations
182 ProviderStake {
183 /// Amount to stake
184 amount: u128,
185 /// Provider type
186 provider_type: String,
187 },
188 /// Unstake tokens
189 ProviderUnstake {
190 /// Amount to unstake
191 amount: u128,
192 },
193 /// Submit a governance proposal
194 GovernancePropose {
195 /// Proposal data
196 proposal: Vec<u8>,
197 },
198 /// Vote on a governance proposal
199 GovernanceVote {
200 /// Proposal ID
201 proposal_id: String,
202 /// Vote value
203 vote: bool,
204 },
205 /// Initiate a bridge transfer
206 BridgeTransfer {
207 /// Target chain
208 target_chain: String,
209 /// Target address
210 target_address: String,
211 /// Amount to bridge
212 amount: u128,
213 },
214 /// Create a new escrow account, locking funds from the payer
215 ///
216 /// The escrow_id is derived deterministically by the VM as
217 /// `SHA-256("tenzro/escrow/id" || payer || nonce_le)` and the funds are
218 /// transferred to a vault address derived as
219 /// `Address(SHA-256("tenzro/escrow/vault" || escrow_id)[..20])`.
220 ///
221 /// Authorization: `tx.from` must equal the payer (enforced by signature verification).
222 CreateEscrow {
223 /// Recipient of funds upon successful release
224 payee: Address,
225 /// Amount to lock in escrow
226 amount: u128,
227 /// Asset being escrowed
228 asset_id: AssetId,
229 /// Unix timestamp (millis) at which the escrow expires
230 expires_at: u64,
231 /// Conditions for releasing funds to the payee
232 release_conditions: ReleaseConditions,
233 },
234 /// Release escrowed funds to the payee with a proof of service
235 ///
236 /// Authorization: `tx.from` must equal the original payer.
237 ReleaseEscrow {
238 /// 32-byte deterministic escrow identifier
239 escrow_id: [u8; 32],
240 /// Proof satisfying the escrow's release conditions
241 proof: ServiceProof,
242 },
243 /// Refund escrowed funds to the payer
244 ///
245 /// Authorization: `tx.from` must equal the original payer AND the escrow must
246 /// either be expired or use Timeout/Custom release conditions.
247 RefundEscrow {
248 /// 32-byte deterministic escrow identifier
249 escrow_id: [u8; 32],
250 },
251 /// Pause an agent (Agent-Swarm Spec 1, tier 1).
252 ///
253 /// Reversible state. Agent stops accepting new tasks; existing
254 /// obligations are honored under the pause-bypass allow-list. Stake
255 /// untouched. Inbound payments still permitted; outbound payments
256 /// blocked.
257 ///
258 /// Authorization: `tx.from` MUST equal the agent's controller DID
259 /// (or hold a `DelegationScope.allowed_operations` entry of
260 /// `pause_agent`). Network-initiated pauses are not allowed — escalate
261 /// to Quarantine instead.
262 PauseAgent {
263 /// DID of the agent to pause (`did:tenzro:machine:...`)
264 agent_did: String,
265 /// DID of the authorising controller. Must match the identity
266 /// bound to `tx.from`; echoed on the wire so the receipt is
267 /// self-describing and the VM does not need an identity lookup.
268 controller_did: String,
269 /// Canonical reason code (see kill-switch spec §"Reason codes")
270 reason_code: u16,
271 /// Optional human reason text, capped at 256 bytes
272 reason_text: Option<String>,
273 /// Optional pause expiry; `None` means indefinite (capped by
274 /// governance `pause_max_duration`).
275 until: Option<Timestamp>,
276 },
277 /// Quarantine an agent (Agent-Swarm Spec 1, tier 2).
278 ///
279 /// Reversible only after evidence review. Inbound + outbound payments
280 /// blocked. Stake frozen — cannot withdraw, cannot earn rewards.
281 /// Existing tasks halt.
282 ///
283 /// Authorization: controller (as PauseAgent), OR slashing-committee
284 /// quorum via `StakingSlashingCallback`, OR governance proposal.
285 QuarantineAgent {
286 /// DID of the agent to quarantine
287 agent_did: String,
288 /// DID of the authorising controller. Must match the identity
289 /// bound to `tx.from`.
290 controller_did: String,
291 /// Canonical reason code
292 reason_code: u16,
293 /// Optional human reason text, capped at 256 bytes
294 reason_text: Option<String>,
295 /// Optional commitment hash to off-chain evidence (32-byte SHA-256)
296 evidence_hash: Option<[u8; 32]>,
297 },
298 /// Terminate an agent (Agent-Swarm Spec 1, tier 3).
299 ///
300 /// **Irreversible.** Identity revoked via the underlying
301 /// `revoke_did` primitive. Stake/bond slashed by `slash_bps` capped
302 /// at the governance `slash_bps_cap`. With `cascade=true`, all
303 /// descendants under the agent's `children:` index are terminated
304 /// recursively (depth-bounded by `cascade_max_depth`, default 32).
305 ///
306 /// Authorization: controller, OR governance proposal with timelock,
307 /// OR cascade=true descended from a parent's TerminateAgent.
308 TerminateAgent {
309 /// DID of the agent to terminate
310 agent_did: String,
311 /// DID of the authorising controller. Must match the identity
312 /// bound to `tx.from`.
313 controller_did: String,
314 /// Canonical reason code
315 reason_code: u16,
316 /// Basis points of stake/bond to slash (0..=10000), capped per
317 /// governance `slash_bps_cap`.
318 slash_bps: u16,
319 /// If true, recursively terminate descendants under `children:`.
320 cascade: bool,
321 },
322 /// Post a fresh AgentBond for `agent_did` (Agent-Swarm Spec 9).
323 ///
324 /// Locks `amount` TNZO from `tx.from` (the controller wallet) into
325 /// the bond vault derived as
326 /// `Address(SHA-256("tenzro/agent-bond/vault" || agent_did))`. The
327 /// bond enters `BondLifecycle::Active` and promotes the agent to the
328 /// Delegated admission lane while ≥ `bond_min_for_promotion`.
329 ///
330 /// Authorization: `tx.from` is the controller; the VM enforces that
331 /// the agent_did either has no prior bond or its prior bond is in a
332 /// terminal state (`Returned` / `Slashed`).
333 PostAgentBond {
334 /// DID of the agent being bonded (`did:tenzro:machine:...`)
335 agent_did: String,
336 /// DID of the controller posting the bond. Echoed on the wire so
337 /// the receipt is self-describing without an identity lookup.
338 controller_did: String,
339 /// Amount of TNZO to lock in the bond vault
340 amount: u128,
341 },
342 /// Top up an existing Active AgentBond by `amount`.
343 ///
344 /// Authorization: `tx.from` MUST equal the original poster (the
345 /// bond's `controller` field).
346 IncreaseAgentBond {
347 /// DID of the agent whose bond is being increased
348 agent_did: String,
349 /// Additional TNZO to lock
350 amount: u128,
351 },
352 /// Initiate the cooldown timer on an Active AgentBond. Funds are
353 /// **not** released by the VM — finalisation happens off-VM via the
354 /// node-side `BondManager` once `cooldown_ms` has elapsed.
355 ///
356 /// Authorization: `tx.from` MUST equal the bond's controller.
357 WithdrawAgentBond {
358 /// DID of the agent whose bond is being withdrawn
359 agent_did: String,
360 },
361 /// Pay out an `Approved` insurance claim from the insurance pool
362 /// vault to the claimant. The off-chain `BondManager` has already
363 /// validated the claim and reserved funds; this transaction performs
364 /// the on-chain transfer and persists a `paid_claim:<claim_id>`
365 /// marker so the same claim cannot be paid twice.
366 ///
367 /// Authorization: governance committee (or `tx.from` matching the
368 /// configured insurance-pool admin DID).
369 PayInsuranceClaim {
370 /// 32-byte deterministic claim identifier (lowercase hex)
371 claim_id_hex: String,
372 /// Recipient of the payout
373 claimant: Address,
374 /// Amount to pay from the pool vault, in TNZO base units
375 amount: u128,
376 },
377 /// Register the signing wallet as a Candidate validator (Dynamic Validator
378 /// Set, 2026-SOTA permissionless join).
379 ///
380 /// On execution the VM emits a `ValidatorRegister` typed log carrying
381 /// `from || stake_le || consensus_pubkey || pq_pubkey || withdrawal_address ||
382 /// metadata_uri`. The node-side `ValidatorRegistry` consumes the log and
383 /// inserts a `Candidate` entry that becomes `PendingActive` at the next
384 /// epoch boundary if `self_stake >= min_self_stake` and the activation
385 /// churn budget admits it; the `EpochManager` then promotes it to `Active`
386 /// `ACTIVATION_EFFECTIVE_DELAY_BLOCKS` after the boundary block.
387 ///
388 /// Authorization: `tx.from` is the validator's stake-owning wallet. The
389 /// classical Ed25519 signature in `SignedTransaction::signature` proves
390 /// control of `consensus_pubkey`, the ML-DSA-65 leg proves control of
391 /// `pq_pubkey`, and the BLS leg proves control of `bls_pubkey`.
392 RegisterValidator {
393 /// 32-byte Ed25519 BFT signing key.
394 consensus_pubkey: Vec<u8>,
395 /// 1952-byte ML-DSA-65 verifying key (FIPS 204). Mandatory hybrid PQ.
396 pq_pubkey: Vec<u8>,
397 /// 48-byte BLS12-381 G1-compressed verifying key (`min_pk` scheme).
398 /// Mandatory third leg, used by HotStuff-2 to aggregate per-vote
399 /// signatures into a single QC-level aggregate.
400 bls_pubkey: Vec<u8>,
401 /// Address rewards / unbonded principal settle to.
402 withdrawal_address: Address,
403 /// Self-stake committed to the candidate. Must be ≥ the registry's
404 /// `min_self_stake` (default 10,000 TNZO).
405 self_stake: u128,
406 /// Optional ≤256-byte off-chain pointer (moniker / website / contact).
407 metadata_uri: String,
408 },
409 /// Voluntarily exit the active set (Dynamic Validator Set).
410 ///
411 /// The VM emits a `ValidatorExit` log; the registry transitions the entry
412 /// to `PendingExit` and the next epoch boundary stages it for removal —
413 /// effective `ACTIVATION_EFFECTIVE_DELAY_BLOCKS` after that boundary
414 /// block. Re-registration is blocked for `reentry_cooldown_epochs` (default
415 /// 4) following voluntary exit.
416 ///
417 /// Authorization: `tx.from` MUST equal the validator's registry address.
418 ExitValidator,
419 /// Update validator metadata (moniker / TEE attestation commitment).
420 ///
421 /// At least one of `metadata_uri` or `tee_attestation_hash` should be
422 /// `Some`; the registry treats `None` as "no change". `tee_attestation_hash`
423 /// is the 32-byte SHA-256 commitment to a fresh attestation document — the
424 /// active-set boundary applies the TEE multiplier from this commitment.
425 ///
426 /// Authorization: `tx.from` MUST equal the validator's registry address.
427 UpdateValidatorMetadata {
428 /// New off-chain pointer; `None` = no change. ≤256 bytes when present.
429 metadata_uri: Option<String>,
430 /// New 32-byte SHA-256 commitment to the attestation document.
431 tee_attestation_hash: Option<[u8; 32]>,
432 },
433}
434
435/// A signed transaction ready for submission
436///
437/// Contains the transaction and the **composite** signature (classical +
438/// post-quantum) proving authorization. Both legs are mandatory — there is
439/// no classical-only path. Verifiers reject a `SignedTransaction` whose
440/// `pq_signature` is absent or wrong-sized.
441///
442/// # Wire layout
443///
444/// - `signature` — classical Ed25519 / Secp256k1 leg (existing field, raw bytes
445/// in `signature.bytes` and the classical pubkey in `signature.public_key`).
446/// - `pq_signature` — ML-DSA-65 signature (FIPS 204, exactly 3309 bytes).
447/// - The ML-DSA-65 verifying key lives in `transaction.pq_public_key` so that
448/// the hash preimage commits to the PQ identity.
449#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
450pub struct SignedTransaction {
451 /// The underlying transaction
452 pub transaction: Transaction,
453 /// The classical signature authorizing the transaction
454 pub signature: Signature,
455 /// ML-DSA-65 signature over `transaction.hash()` (FIPS 204, exactly 3309
456 /// bytes). Mandatory; mis-sized payloads are rejected at deserialization.
457 #[serde(deserialize_with = "crate::validation::bounded_pq_signature_bytes")]
458 pub pq_signature: Vec<u8>,
459 /// Cached transaction hash
460 #[serde(skip)]
461 hash: Option<Hash>,
462}
463
464impl SignedTransaction {
465 /// Creates a new signed transaction. Both the classical signature and the
466 /// ML-DSA-65 signature are mandatory.
467 pub fn new(transaction: Transaction, signature: Signature, pq_signature: Vec<u8>) -> Self {
468 Self {
469 transaction,
470 signature,
471 pq_signature,
472 hash: None,
473 }
474 }
475
476 /// Returns the hash of the transaction
477 pub fn hash(&mut self) -> Hash {
478 if let Some(hash) = self.hash {
479 hash
480 } else {
481 let hash = self.transaction.hash();
482 self.hash = Some(hash);
483 hash
484 }
485 }
486
487 /// Validates the signed transaction
488 ///
489 /// Performs basic validation checks:
490 /// - Classical signature bytes are non-empty
491 /// - Classical public key is non-empty
492 /// - PQ signature is exactly ML_DSA_65_SIG_LEN bytes (3309)
493 /// - PQ public key (carried in `transaction.pq_public_key`) is exactly
494 /// ML_DSA_65_VK_LEN bytes (1952)
495 /// - Gas limit is non-zero
496 /// - Addresses are non-zero
497 /// - Transaction data size is within limits
498 ///
499 /// Note: This does NOT verify the cryptographic signature.
500 /// Signature verification requires the crypto crate.
501 pub fn validate(&self) -> Result<(), &'static str> {
502 // Check classical signature is non-empty
503 if self.signature.bytes.is_empty() {
504 return Err("Transaction signature is empty");
505 }
506
507 // Check classical public key is non-empty
508 if self.signature.public_key.is_empty() {
509 return Err("Transaction public key is empty");
510 }
511
512 // PQ leg is mandatory — defense-in-depth in case in-memory construction
513 // bypassed the deserializer-level length checks.
514 if self.pq_signature.len() != 3309 {
515 return Err("PQ signature has wrong length (expected ML-DSA-65, 3309 bytes)");
516 }
517 if self.transaction.pq_public_key.len() != 1952 {
518 return Err("PQ public key has wrong length (expected ML-DSA-65 vk, 1952 bytes)");
519 }
520
521 // Check gas limit is non-zero
522 if self.transaction.gas_limit == 0 {
523 return Err("Gas limit cannot be zero");
524 }
525
526 // Check sender is not zero address
527 if self.transaction.from == Address::zero() {
528 return Err("Sender address cannot be zero");
529 }
530
531 // Validate transaction data size based on type
532 match &self.transaction.tx_type {
533 TransactionType::ContractDeploy { code, args } => {
534 if code.len() > MAX_TX_DATA_SIZE {
535 return Err("Contract code exceeds maximum size");
536 }
537 if args.len() > MAX_TX_DATA_SIZE {
538 return Err("Contract args exceed maximum size");
539 }
540 }
541 TransactionType::ContractCall { args, .. } => {
542 if args.len() > MAX_TX_DATA_SIZE {
543 return Err("Contract call args exceed maximum size");
544 }
545 }
546 TransactionType::AgentRegister { config } => {
547 if config.len() > MAX_TX_DATA_SIZE {
548 return Err("Agent config exceeds maximum size");
549 }
550 }
551 TransactionType::AgentExecute { task } => {
552 if task.len() > MAX_TX_DATA_SIZE {
553 return Err("Agent task exceeds maximum size");
554 }
555 }
556 TransactionType::ModelInference { input, .. } => {
557 if input.len() > MAX_TX_DATA_SIZE {
558 return Err("Inference input exceeds maximum size");
559 }
560 }
561 TransactionType::TeeProviderRegister { attestation, info } => {
562 if attestation.len() > MAX_TX_DATA_SIZE {
563 return Err("TEE attestation exceeds maximum size");
564 }
565 if info.len() > MAX_TX_DATA_SIZE {
566 return Err("Provider info exceeds maximum size");
567 }
568 }
569 TransactionType::GovernancePropose { proposal } => {
570 if proposal.len() > MAX_TX_DATA_SIZE {
571 return Err("Proposal data exceeds maximum size");
572 }
573 }
574 TransactionType::ReleaseEscrow { proof, .. } => {
575 if proof.proof_data.len() > MAX_TX_DATA_SIZE {
576 return Err("Escrow proof data exceeds maximum size");
577 }
578 // Bound number of signatures to prevent DoS at verification time
579 if proof.signatures.len() > 16 {
580 return Err("Too many signatures in escrow release proof");
581 }
582 }
583 TransactionType::PauseAgent { agent_did, reason_text, .. }
584 | TransactionType::QuarantineAgent { agent_did, reason_text, .. } => {
585 if agent_did.len() > 256 {
586 return Err("agent_did exceeds maximum length");
587 }
588 if let Some(t) = reason_text
589 && t.len() > 256
590 {
591 return Err("reason_text exceeds 256 bytes");
592 }
593 }
594 TransactionType::TerminateAgent { agent_did, slash_bps, .. } => {
595 if agent_did.len() > 256 {
596 return Err("agent_did exceeds maximum length");
597 }
598 if *slash_bps > 10_000 {
599 return Err("slash_bps exceeds 100%");
600 }
601 }
602 TransactionType::RegisterValidator {
603 consensus_pubkey,
604 pq_pubkey,
605 bls_pubkey,
606 metadata_uri,
607 ..
608 } => {
609 if consensus_pubkey.len() != 32 {
610 return Err("consensus_pubkey must be 32 bytes (Ed25519)");
611 }
612 if pq_pubkey.len() != 1952 {
613 return Err("pq_pubkey must be 1952 bytes (ML-DSA-65 vk)");
614 }
615 if bls_pubkey.len() != 48 {
616 return Err("bls_pubkey must be 48 bytes (BLS12-381 G1-compressed, min_pk)");
617 }
618 if metadata_uri.len() > 256 {
619 return Err("metadata_uri exceeds 256 bytes");
620 }
621 }
622 TransactionType::UpdateValidatorMetadata { metadata_uri, .. } => {
623 if let Some(uri) = metadata_uri
624 && uri.len() > 256
625 {
626 return Err("metadata_uri exceeds 256 bytes");
627 }
628 }
629 // Other transaction types have bounded data (strings, primitives)
630 _ => {}
631 }
632
633 // Recipient can be zero for contract creation
634
635 Ok(())
636 }
637
638 /// Returns the sender address
639 pub fn sender(&self) -> Address {
640 self.transaction.from
641 }
642
643 /// Returns the recipient address
644 pub fn recipient(&self) -> Address {
645 self.transaction.to
646 }
647
648 /// Returns the nonce
649 pub fn nonce(&self) -> Nonce {
650 self.transaction.nonce
651 }
652
653 /// Checks if the transaction has both classical and PQ signatures
654 /// populated (non-empty classical legs and exact-size PQ legs).
655 /// This does NOT verify cryptographic validity.
656 pub fn is_signed(&self) -> bool {
657 !self.signature.bytes.is_empty()
658 && !self.signature.public_key.is_empty()
659 && self.pq_signature.len() == 3309
660 && self.transaction.pq_public_key.len() == 1952
661 }
662}