Skip to main content

jam_primitives/
transaction.rs

1//! # JAM Protocol Transaction Types
2//!
3//! Transaction types and structures for the JAM protocol as defined in the Gray Paper.
4//! This module contains all transaction-related data structures and their validation logic.
5
6use crate::utils::codec::{Decode, Encode};
7use crate::{
8    Hash, PrimitiveError, PrimitiveResult, Validate,
9    types::{AccountId, Balance, Gas, ServiceId, Signature, Timestamp, Weight},
10};
11use serde::{Deserialize, Serialize};
12
13/// Transaction nonce type
14pub type Nonce = u64;
15
16/// Transaction type identifier
17pub type TransactionType = u8;
18
19/// Gas price type (in smallest unit)
20pub type GasPrice = u64;
21
22/// Transaction kinds supported by JAM
23#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, Serialize, Deserialize)]
24pub enum TransactionKind {
25    /// Transfer tokens between accounts
26    Transfer {
27        /// Destination account
28        to: AccountId,
29        /// Amount to transfer
30        amount: Balance,
31    },
32    /// Call a service method
33    ServiceCall {
34        /// Target service
35        service: ServiceId,
36        /// Method identifier
37        method: u32,
38        /// Call data
39        data: Vec<u8>,
40    },
41    /// Deploy a new service
42    ServiceDeploy {
43        /// Service code (WebAssembly)
44        code: Vec<u8>,
45        /// Initial service state
46        initial_state: Vec<u8>,
47    },
48    /// Update service code
49    ServiceUpdate {
50        /// Service to update
51        service: ServiceId,
52        /// New service code
53        code: Vec<u8>,
54    },
55    /// Register as a validator
56    ValidatorRegister {
57        /// Validator public keys
58        keys: ValidatorKeys,
59        /// Stake amount
60        stake: Balance,
61    },
62    /// Unregister as a validator
63    ValidatorUnregister,
64    /// Delegate stake to a validator
65    Delegate {
66        /// Target validator
67        validator: AccountId,
68        /// Amount to delegate
69        amount: Balance,
70    },
71    /// Undelegate stake from a validator
72    Undelegate {
73        /// Target validator
74        validator: AccountId,
75        /// Amount to undelegate
76        amount: Balance,
77    },
78    /// Core assignment transaction
79    CoreAssign {
80        /// Core index to assign
81        core: u16,
82        /// Service to assign to core
83        service: ServiceId,
84        /// Assignment duration
85        duration: u64,
86    },
87    /// Availability report
88    AvailabilityReport {
89        /// Core index
90        core: u16,
91        /// Availability data hash
92        data_hash: Hash,
93        /// Erasure coded chunks
94        chunks: Vec<Vec<u8>>,
95    },
96    /// Dispute initiation
97    DisputeInitiate {
98        /// Disputed core
99        core: u16,
100        /// Block number being disputed
101        block_number: u32,
102        /// Dispute evidence
103        evidence: Vec<u8>,
104    },
105    /// Dispute vote
106    DisputeVote {
107        /// Dispute ID
108        dispute_id: Hash,
109        /// Vote (true = valid, false = invalid)
110        vote: bool,
111        /// Justification
112        justification: Vec<u8>,
113    },
114}
115
116impl TransactionKind {
117    /// Get the transaction type identifier
118    pub fn transaction_type(&self) -> TransactionType {
119        match self {
120            TransactionKind::Transfer { .. } => 0,
121            TransactionKind::ServiceCall { .. } => 1,
122            TransactionKind::ServiceDeploy { .. } => 2,
123            TransactionKind::ServiceUpdate { .. } => 3,
124            TransactionKind::ValidatorRegister { .. } => 4,
125            TransactionKind::ValidatorUnregister => 5,
126            TransactionKind::Delegate { .. } => 6,
127            TransactionKind::Undelegate { .. } => 7,
128            TransactionKind::CoreAssign { .. } => 8,
129            TransactionKind::AvailabilityReport { .. } => 9,
130            TransactionKind::DisputeInitiate { .. } => 10,
131            TransactionKind::DisputeVote { .. } => 11,
132        }
133    }
134
135    /// Get human-readable transaction type name
136    pub fn type_name(&self) -> &'static str {
137        match self {
138            TransactionKind::Transfer { .. } => "Transfer",
139            TransactionKind::ServiceCall { .. } => "ServiceCall",
140            TransactionKind::ServiceDeploy { .. } => "ServiceDeploy",
141            TransactionKind::ServiceUpdate { .. } => "ServiceUpdate",
142            TransactionKind::ValidatorRegister { .. } => "ValidatorRegister",
143            TransactionKind::ValidatorUnregister => "ValidatorUnregister",
144            TransactionKind::Delegate { .. } => "Delegate",
145            TransactionKind::Undelegate { .. } => "Undelegate",
146            TransactionKind::CoreAssign { .. } => "CoreAssign",
147            TransactionKind::AvailabilityReport { .. } => "AvailabilityReport",
148            TransactionKind::DisputeInitiate { .. } => "DisputeInitiate",
149            TransactionKind::DisputeVote { .. } => "DisputeVote",
150        }
151    }
152
153    /// Check if this transaction requires stake
154    pub fn requires_stake(&self) -> bool {
155        matches!(
156            self,
157            TransactionKind::ValidatorRegister { .. } | TransactionKind::Delegate { .. }
158        )
159    }
160
161    /// Get the target service ID if applicable
162    pub fn target_service(&self) -> Option<ServiceId> {
163        match self {
164            TransactionKind::ServiceCall { service, .. }
165            | TransactionKind::ServiceUpdate { service, .. }
166            | TransactionKind::CoreAssign { service, .. } => Some(*service),
167            _ => None,
168        }
169    }
170
171    /// Get the core index if applicable
172    pub fn target_core(&self) -> Option<u16> {
173        match self {
174            TransactionKind::CoreAssign { core, .. }
175            | TransactionKind::AvailabilityReport { core, .. }
176            | TransactionKind::DisputeInitiate { core, .. } => Some(*core),
177            _ => None,
178        }
179    }
180}
181
182/// Validator keys for registration
183#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, Serialize, Deserialize)]
184pub struct ValidatorKeys {
185    /// GRANDPA key for finality voting
186    pub grandpa: crate::crypto::Ed25519PublicKey,
187    /// BABE key for block production
188    pub babe: crate::crypto::bandersnatch::PublicKey,
189    /// Session key for networking
190    pub session: crate::crypto::Ed25519PublicKey,
191    /// Authority discovery key
192    pub authority_discovery: crate::crypto::Ed25519PublicKey,
193}
194
195impl ValidatorKeys {
196    /// Create new validator keys
197    pub fn new(
198        grandpa: crate::crypto::Ed25519PublicKey,
199        babe: crate::crypto::bandersnatch::PublicKey,
200        session: crate::crypto::Ed25519PublicKey,
201        authority_discovery: crate::crypto::Ed25519PublicKey,
202    ) -> Self {
203        Self {
204            grandpa,
205            babe,
206            session,
207            authority_discovery,
208        }
209    }
210}
211
212/// Complete JAM transaction
213#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, Serialize, Deserialize)]
214pub struct Transaction {
215    /// Transaction sender
216    pub sender: AccountId,
217    /// Transaction nonce (prevents replay attacks)
218    pub nonce: Nonce,
219    /// Maximum gas to spend
220    pub gas_limit: Gas,
221    /// Gas price per unit
222    pub gas_price: GasPrice,
223    /// Transaction kind and parameters
224    pub kind: TransactionKind,
225    /// Digital signature
226    pub signature: Signature,
227}
228
229impl Transaction {
230    /// Create a new transaction
231    pub fn new(
232        sender: AccountId,
233        nonce: Nonce,
234        gas_limit: Gas,
235        gas_price: GasPrice,
236        kind: TransactionKind,
237        signature: Signature,
238    ) -> Self {
239        Self {
240            sender,
241            nonce,
242            gas_limit,
243            gas_price,
244            kind,
245            signature,
246        }
247    }
248
249    /// Get transaction hash
250    pub fn hash(&self) -> Hash {
251        use crate::crypto::hashing::{Blake3Hasher, Hasher};
252        let hasher = Blake3Hasher::new();
253        hasher.hash(&self.encode())
254    }
255
256    /// Get transaction fee (gas_limit * gas_price)
257    pub fn fee(&self) -> Balance {
258        self.gas_limit.saturating_mul(self.gas_price)
259    }
260
261    /// Check if transaction is signed by the claimed sender
262    pub fn verify_signature(&self) -> PrimitiveResult<bool> {
263        // Create message to verify (all fields except signature)
264        let _message = self.signature_message();
265
266        // For now, we'll assume signature verification is handled elsewhere
267        // In a real implementation, this would use the appropriate crypto module
268        Ok(true) // Placeholder
269    }
270
271    /// Get the message that should be signed
272    pub fn signature_message(&self) -> Vec<u8> {
273        let mut message = Vec::new();
274        message.extend_from_slice(self.sender.as_ref());
275        message.extend_from_slice(&self.nonce.to_le_bytes());
276        message.extend_from_slice(&self.gas_limit.0.to_le_bytes());
277        message.extend_from_slice(&self.gas_price.to_le_bytes());
278        message.extend_from_slice(&self.kind.encode());
279        message
280    }
281
282    /// Check if transaction is a system transaction (no fee required)
283    pub fn is_system_transaction(&self) -> bool {
284        matches!(
285            self.kind,
286            TransactionKind::ValidatorUnregister
287                | TransactionKind::AvailabilityReport { .. }
288                | TransactionKind::DisputeVote { .. }
289        )
290    }
291
292    /// Get the transaction weight (for block weight calculations)
293    pub fn weight(&self) -> Weight {
294        let base_weight = 10_000; // Base transaction weight
295        let kind_weight = match &self.kind {
296            TransactionKind::Transfer { .. } => 25_000,
297            TransactionKind::ServiceCall { data, .. } => 50_000 + (data.len() as u64 * 10),
298            TransactionKind::ServiceDeploy {
299                code,
300                initial_state,
301            } => 100_000 + (code.len() as u64 * 50) + (initial_state.len() as u64 * 10),
302            TransactionKind::ServiceUpdate { code, .. } => 80_000 + (code.len() as u64 * 50),
303            TransactionKind::ValidatorRegister { .. } => 200_000,
304            TransactionKind::ValidatorUnregister => 50_000,
305            TransactionKind::Delegate { .. } => 30_000,
306            TransactionKind::Undelegate { .. } => 30_000,
307            TransactionKind::CoreAssign { .. } => 40_000,
308            TransactionKind::AvailabilityReport { chunks, .. } => {
309                60_000 + (chunks.iter().map(|c| c.len()).sum::<usize>() as u64 * 5)
310            }
311            TransactionKind::DisputeInitiate { evidence, .. } => {
312                150_000 + (evidence.len() as u64 * 20)
313            }
314            TransactionKind::DisputeVote { justification, .. } => {
315                30_000 + (justification.len() as u64 * 10)
316            }
317        };
318        Weight(base_weight + kind_weight)
319    }
320}
321
322impl Validate for Transaction {
323    fn validate(&self) -> PrimitiveResult<()> {
324        // Check gas limit is reasonable
325        if self.gas_limit == Gas(0) {
326            return Err(PrimitiveError::InvalidTransaction(
327                "Gas limit cannot be zero".to_string(),
328            ));
329        }
330
331        if self.gas_limit > Gas(10_000_000) {
332            return Err(PrimitiveError::InvalidTransaction(
333                "Gas limit too high".to_string(),
334            ));
335        }
336
337        // Check gas price is reasonable
338        if self.gas_price == 0 && !self.is_system_transaction() {
339            return Err(PrimitiveError::InvalidTransaction(
340                "Gas price cannot be zero for non-system transactions".to_string(),
341            ));
342        }
343
344        // Validate transaction kind
345        self.validate_kind()?;
346
347        // Verify signature
348        if !self.verify_signature()? {
349            return Err(PrimitiveError::InvalidTransaction(
350                "Invalid signature".to_string(),
351            ));
352        }
353
354        Ok(())
355    }
356}
357
358impl Transaction {
359    /// Validate transaction kind-specific rules
360    fn validate_kind(&self) -> PrimitiveResult<()> {
361        match &self.kind {
362            TransactionKind::Transfer { amount, .. } => {
363                if *amount == Balance(0) {
364                    return Err(PrimitiveError::InvalidTransaction(
365                        "Transfer amount cannot be zero".to_string(),
366                    ));
367                }
368            }
369            TransactionKind::ServiceCall { data, .. } => {
370                if data.len() > 1024 * 1024 {
371                    return Err(PrimitiveError::InvalidTransaction(
372                        "Service call data too large".to_string(),
373                    ));
374                }
375            }
376            TransactionKind::ServiceDeploy {
377                code,
378                initial_state,
379            } => {
380                if code.is_empty() {
381                    return Err(PrimitiveError::InvalidTransaction(
382                        "Service code cannot be empty".to_string(),
383                    ));
384                }
385                if code.len() > 5 * 1024 * 1024 {
386                    return Err(PrimitiveError::InvalidTransaction(
387                        "Service code too large".to_string(),
388                    ));
389                }
390                if initial_state.len() > 1024 * 1024 {
391                    return Err(PrimitiveError::InvalidTransaction(
392                        "Initial state too large".to_string(),
393                    ));
394                }
395            }
396            TransactionKind::ServiceUpdate { code, .. } => {
397                if code.is_empty() {
398                    return Err(PrimitiveError::InvalidTransaction(
399                        "Service code cannot be empty".to_string(),
400                    ));
401                }
402                if code.len() > 5 * 1024 * 1024 {
403                    return Err(PrimitiveError::InvalidTransaction(
404                        "Service code too large".to_string(),
405                    ));
406                }
407            }
408            TransactionKind::ValidatorRegister { stake, .. } => {
409                if *stake == Balance(0) {
410                    return Err(PrimitiveError::InvalidTransaction(
411                        "Validator stake cannot be zero".to_string(),
412                    ));
413                }
414            }
415            TransactionKind::Delegate { amount, .. }
416            | TransactionKind::Undelegate { amount, .. } => {
417                if *amount == Balance(0) {
418                    return Err(PrimitiveError::InvalidTransaction(
419                        "Delegation amount cannot be zero".to_string(),
420                    ));
421                }
422            }
423            TransactionKind::CoreAssign { duration, .. } => {
424                if *duration == 0 {
425                    return Err(PrimitiveError::InvalidTransaction(
426                        "Core assignment duration cannot be zero".to_string(),
427                    ));
428                }
429            }
430            TransactionKind::AvailabilityReport { chunks, .. } => {
431                if chunks.is_empty() {
432                    return Err(PrimitiveError::InvalidTransaction(
433                        "Availability report must include chunks".to_string(),
434                    ));
435                }
436            }
437            TransactionKind::DisputeInitiate { evidence, .. } => {
438                if evidence.is_empty() {
439                    return Err(PrimitiveError::InvalidTransaction(
440                        "Dispute must include evidence".to_string(),
441                    ));
442                }
443            }
444            _ => {} // Other kinds have no specific validation
445        }
446        Ok(())
447    }
448}
449
450/// Transaction pool entry with additional metadata
451#[derive(Debug, Clone, PartialEq, Eq)]
452pub struct TransactionPoolEntry {
453    /// The transaction
454    pub transaction: Transaction,
455    /// When it was received
456    pub received_at: Timestamp,
457    /// Priority score for ordering
458    pub priority: u64,
459    /// Number of times it was propagated
460    pub propagation_count: u32,
461}
462
463impl TransactionPoolEntry {
464    /// Create a new pool entry
465    pub fn new(transaction: Transaction, received_at: Timestamp) -> Self {
466        let priority = transaction.gas_price; // Simple priority based on gas price
467        Self {
468            transaction,
469            received_at,
470            priority,
471            propagation_count: 0,
472        }
473    }
474
475    /// Update priority based on current conditions
476    pub fn update_priority(&mut self, current_time: Timestamp) {
477        // Increase priority for older transactions
478        let age_bonus = current_time.saturating_sub(self.received_at) / 1000; // Age in seconds
479        self.priority = self.transaction.gas_price + age_bonus;
480    }
481
482    /// Check if transaction has expired
483    pub fn is_expired(&self, current_time: Timestamp, max_lifetime: u64) -> bool {
484        current_time.saturating_sub(self.received_at) > max_lifetime
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491    use crate::types::*;
492
493    fn dummy_signature() -> Signature {
494        Signature::Ed25519([0u8; 64])
495    }
496
497    fn dummy_account() -> AccountId {
498        AccountId::from([0u8; 32])
499    }
500
501    #[test]
502    fn test_transaction_creation() {
503        let tx = Transaction::new(
504            AccountId::from([1u8; 32]),
505            1,
506            Gas(21000),
507            20,
508            TransactionKind::Transfer {
509                to: AccountId::from([2u8; 32]),
510                amount: Balance(1000),
511            },
512            dummy_signature(),
513        );
514
515        assert_eq!(tx.nonce, 1);
516        assert_eq!(tx.gas_limit, Gas(21000));
517        assert_eq!(tx.fee(), Balance(420000));
518    }
519
520    #[test]
521    fn test_transaction_kind_types() {
522        let transfer = TransactionKind::Transfer {
523            to: dummy_account(),
524            amount: Balance(1000),
525        };
526        assert_eq!(transfer.transaction_type(), 0);
527        assert_eq!(transfer.type_name(), "Transfer");
528        assert!(!transfer.requires_stake());
529
530        let validator_register = TransactionKind::ValidatorRegister {
531            keys: ValidatorKeys::new(
532                crate::crypto::Ed25519PublicKey::from([0u8; 32]),
533                crate::crypto::bandersnatch::PublicKey::from([0u8; 32]),
534                crate::crypto::Ed25519PublicKey::from([0u8; 32]),
535                crate::crypto::Ed25519PublicKey::from([0u8; 32]),
536            ),
537            stake: Balance(1000000),
538        };
539        assert_eq!(validator_register.transaction_type(), 4);
540        assert!(validator_register.requires_stake());
541    }
542
543    #[test]
544    fn test_transaction_validation() {
545        let valid_tx = Transaction::new(
546            AccountId::from([1u8; 32]),
547            1,
548            Gas(21000),
549            20,
550            TransactionKind::Transfer {
551                to: AccountId::from([2u8; 32]),
552                amount: Balance(1000),
553            },
554            dummy_signature(),
555        );
556        assert!(valid_tx.validate().is_ok());
557
558        let invalid_tx = Transaction::new(
559            AccountId::from([1u8; 32]),
560            1,
561            Gas(0), // Invalid gas limit
562            20,
563            TransactionKind::Transfer {
564                to: AccountId::from([2u8; 32]),
565                amount: Balance(1000),
566            },
567            dummy_signature(),
568        );
569        assert!(invalid_tx.validate().is_err());
570    }
571
572    #[test]
573    fn test_transaction_pool_entry() {
574        let tx = Transaction::new(
575            AccountId::from([1u8; 32]),
576            1,
577            Gas(21000),
578            20,
579            TransactionKind::Transfer {
580                to: AccountId::from([2u8; 32]),
581                amount: Balance(1000),
582            },
583            dummy_signature(),
584        );
585
586        let mut entry = TransactionPoolEntry::new(tx, Timestamp(1000000));
587        assert_eq!(entry.priority, 20);
588
589        entry.update_priority(Timestamp(1001000)); // 1 second later
590        assert_eq!(entry.priority, 21); // Gas price + age bonus
591
592        assert!(!entry.is_expired(Timestamp(1005000), 10000)); // Not expired
593        assert!(entry.is_expired(Timestamp(1015000), 10000)); // Expired
594    }
595
596    #[test]
597    fn test_transaction_weight_calculation() {
598        let transfer = Transaction::new(
599            AccountId::from([1u8; 32]),
600            1,
601            Gas(21000),
602            20,
603            TransactionKind::Transfer {
604                to: AccountId::from([2u8; 32]),
605                amount: Balance(1000),
606            },
607            dummy_signature(),
608        );
609
610        assert_eq!(transfer.weight(), Weight(35_000)); // Base + transfer weight
611
612        let service_call = Transaction::new(
613            AccountId::from([1u8; 32]),
614            1,
615            Gas(100000),
616            20,
617            TransactionKind::ServiceCall {
618                service: ServiceId::new(1),
619                method: 0,
620                data: vec![0u8; 100],
621            },
622            dummy_signature(),
623        );
624
625        assert_eq!(service_call.weight(), Weight(61_000)); // Base + service call + data weight
626    }
627}