Skip to main content

txgate_chain/
chain.rs

1//! Chain trait for blockchain transaction parsers.
2//!
3//! This module defines the [`Chain`] trait that all blockchain parsers must implement.
4//! The trait provides a unified interface for parsing raw transaction bytes into
5//! a common [`ParsedTx`] structure that can be evaluated by the policy engine.
6//!
7//! # Design Philosophy
8//!
9//! The `Chain` trait is designed to:
10//! - Provide a minimal, focused interface for transaction parsing
11//! - Be thread-safe (`Send + Sync`) for concurrent parsing in async runtimes
12//! - Support multiple transaction versions through the `supports_version` method
13//! - Enable runtime chain selection through trait objects (`Box<dyn Chain>`)
14//!
15//! # Version Handling Strategy
16//!
17//! Different blockchains have different versioning schemes:
18//!
19//! ## Ethereum
20//! - Legacy transactions (type 0): No version byte, RLP-encoded
21//! - EIP-2930 (type 1): Access list transactions
22//! - EIP-1559 (type 2): Dynamic fee transactions
23//! - EIP-4844 (type 3): Blob transactions
24//!
25//! The version byte is the first byte of the raw transaction data for typed
26//! transactions. Legacy transactions start with an RLP list marker (0xc0-0xff).
27//!
28//! ## Bitcoin
29//! - Version field in transaction header (typically 1 or 2)
30//! - `SegWit` transactions have a marker/flag after version
31//!
32//! ## Solana
33//! - Message version in the first byte (0 for legacy, 128+ for versioned)
34//!
35//! Parsers should use `supports_version` to indicate which versions they handle
36//! and return `ParseError::UnknownTxType` for unsupported versions.
37//!
38//! # Example Implementation
39//!
40//! ```ignore
41//! use txgate_chain::Chain;
42//! use txgate_core::{ParsedTx, error::ParseError};
43//! use txgate_crypto::CurveType;
44//!
45//! struct EthereumParser;
46//!
47//! impl Chain for EthereumParser {
48//!     fn id(&self) -> &'static str {
49//!         "ethereum"
50//!     }
51//!
52//!     fn parse(&self, raw: &[u8]) -> Result<ParsedTx, ParseError> {
53//!         // Parse Ethereum transaction bytes...
54//!         todo!()
55//!     }
56//!
57//!     fn curve(&self) -> CurveType {
58//!         CurveType::Secp256k1
59//!     }
60//!
61//!     fn supports_version(&self, version: u8) -> bool {
62//!         // Support legacy (detected by RLP marker), EIP-2930, EIP-1559, EIP-4844
63//!         matches!(version, 0 | 1 | 2 | 3)
64//!     }
65//! }
66//! ```
67//!
68//! # Security Considerations
69//!
70//! - Parsers must validate all input data thoroughly
71//! - The `hash` field in `ParsedTx` must be the exact bytes that will be signed
72//! - Parsers should not trust any data from the raw transaction without validation
73//! - Integer overflow and underflow must be handled carefully
74
75use txgate_core::{error::ParseError, ParsedTx};
76use txgate_crypto::CurveType;
77
78/// Trait for blockchain transaction parsers.
79///
80/// Each supported blockchain implements this trait to parse raw transaction
81/// bytes into a unified [`ParsedTx`] structure that can be evaluated by the
82/// policy engine.
83///
84/// # Implementor Requirements
85///
86/// Implementations must:
87/// - Parse raw transaction bytes according to the chain's encoding format
88/// - Extract recipient address, amount, and token information
89/// - Compute the transaction hash that will be signed
90/// - Detect token operations (ERC-20, SPL, TRC-20, etc.)
91/// - Set appropriate [`TxType`](txgate_core::TxType) for policy evaluation
92/// - Return appropriate errors for malformed or unsupported transactions
93///
94/// # Thread Safety
95///
96/// Implementations must be `Send + Sync` to allow concurrent parsing
97/// in the server's async runtime. This is typically achieved by:
98/// - Not storing mutable state in the parser
99/// - Using interior mutability with proper synchronization if state is needed
100///
101/// # Example: Using with Trait Objects
102///
103/// ```
104/// use txgate_chain::Chain;
105/// use txgate_core::{ParsedTx, error::ParseError};
106/// use txgate_crypto::CurveType;
107///
108/// // Create a registry of chain parsers
109/// struct ChainRegistry {
110///     chains: Vec<Box<dyn Chain>>,
111/// }
112///
113/// impl ChainRegistry {
114///     fn find(&self, id: &str) -> Option<&dyn Chain> {
115///         self.chains.iter().find(|c| c.id() == id).map(|c| c.as_ref())
116///     }
117/// }
118/// ```
119pub trait Chain: Send + Sync {
120    /// Returns the chain identifier (e.g., "ethereum", "bitcoin", "solana").
121    ///
122    /// This identifier is used for:
123    /// - Chain lookup in the registry
124    /// - Logging and metrics
125    /// - Configuration matching
126    /// - Correlation with key storage
127    ///
128    /// # Naming Convention
129    ///
130    /// Chain identifiers should be:
131    /// - Lowercase
132    /// - Alphanumeric with hyphens for multi-word names
133    /// - Consistent with industry conventions
134    ///
135    /// Examples: `"ethereum"`, `"bitcoin"`, `"solana"`, `"polygon"`, `"arbitrum-one"`
136    ///
137    /// # Example
138    ///
139    /// ```
140    /// use txgate_chain::Chain;
141    /// # use txgate_core::{ParsedTx, error::ParseError};
142    /// # use txgate_crypto::CurveType;
143    ///
144    /// struct EthereumParser;
145    ///
146    /// impl Chain for EthereumParser {
147    ///     fn id(&self) -> &'static str {
148    ///         "ethereum"
149    ///     }
150    /// #   fn parse(&self, _raw: &[u8]) -> Result<ParsedTx, ParseError> {
151    /// #       Err(ParseError::UnknownTxType)
152    /// #   }
153    /// #   fn curve(&self) -> CurveType {
154    /// #       CurveType::Secp256k1
155    /// #   }
156    /// }
157    ///
158    /// let parser = EthereumParser;
159    /// assert_eq!(parser.id(), "ethereum");
160    /// ```
161    fn id(&self) -> &'static str;
162
163    /// Parse raw transaction bytes into a [`ParsedTx`].
164    ///
165    /// This is the core method of the trait. It transforms chain-specific
166    /// transaction bytes into a normalized format for policy evaluation.
167    ///
168    /// # Arguments
169    ///
170    /// * `raw` - The raw transaction bytes in the chain's native format
171    ///
172    /// # Returns
173    ///
174    /// * `Ok(ParsedTx)` - Successfully parsed transaction with all fields populated
175    /// * `Err(ParseError)` - Parsing failed
176    ///
177    /// # Errors
178    ///
179    /// This method returns a [`ParseError`] in the following cases:
180    /// - [`ParseError::UnknownTxType`] - Transaction type byte is not recognized
181    /// - [`ParseError::InvalidRlp`] - RLP decoding failed (for Ethereum)
182    /// - [`ParseError::MalformedTransaction`] - Transaction structure is invalid
183    /// - [`ParseError::MalformedCalldata`] - Contract call data is invalid
184    /// - [`ParseError::InvalidAddress`] - Address format is invalid
185    ///
186    /// # Transaction Hash
187    ///
188    /// The returned `ParsedTx.hash` must be the exact hash that will be signed.
189    /// This is critical for security:
190    /// - For Ethereum legacy: Keccak256 of RLP-encoded transaction without signature
191    /// - For Ethereum EIP-155: Includes `chain_id` in the signing hash
192    /// - For Ethereum typed: Domain-separated hash with type prefix
193    /// - For Bitcoin: Double SHA256 of serialized transaction
194    /// - For Solana: SHA256 of serialized message
195    ///
196    /// # Example
197    ///
198    /// ```
199    /// use txgate_chain::Chain;
200    /// use txgate_core::{ParsedTx, TxType, error::ParseError};
201    /// # use txgate_crypto::CurveType;
202    ///
203    /// struct SimpleParser;
204    ///
205    /// impl Chain for SimpleParser {
206    ///     fn id(&self) -> &'static str { "test" }
207    ///
208    ///     fn parse(&self, raw: &[u8]) -> Result<ParsedTx, ParseError> {
209    ///         if raw.is_empty() {
210    ///             return Err(ParseError::MalformedTransaction {
211    ///                 context: "empty transaction data".to_string(),
212    ///             });
213    ///         }
214    ///         // Parse the transaction...
215    ///         Ok(ParsedTx::default())
216    ///     }
217    /// #   fn curve(&self) -> CurveType { CurveType::Secp256k1 }
218    /// }
219    /// ```
220    fn parse(&self, raw: &[u8]) -> Result<ParsedTx, ParseError>;
221
222    /// Returns the elliptic curve used by this chain.
223    ///
224    /// This is used to select the appropriate signer for the chain.
225    /// Most EVM-compatible chains use secp256k1, while Solana uses Ed25519.
226    ///
227    /// # Curve Selection
228    ///
229    /// - [`CurveType::Secp256k1`] - Ethereum, Bitcoin, Tron, Ripple, most EVM chains
230    /// - [`CurveType::Ed25519`] - Solana, NEAR, Cosmos (some chains)
231    ///
232    /// # Example
233    ///
234    /// ```
235    /// use txgate_chain::Chain;
236    /// use txgate_crypto::CurveType;
237    /// # use txgate_core::{ParsedTx, error::ParseError};
238    ///
239    /// struct SolanaParser;
240    ///
241    /// impl Chain for SolanaParser {
242    ///     fn id(&self) -> &'static str { "solana" }
243    /// #   fn parse(&self, _raw: &[u8]) -> Result<ParsedTx, ParseError> {
244    /// #       Err(ParseError::UnknownTxType)
245    /// #   }
246    ///     fn curve(&self) -> CurveType {
247    ///         CurveType::Ed25519
248    ///     }
249    /// }
250    ///
251    /// let parser = SolanaParser;
252    /// assert_eq!(parser.curve(), CurveType::Ed25519);
253    /// ```
254    fn curve(&self) -> CurveType;
255
256    /// Check if this parser supports a specific transaction version/type.
257    ///
258    /// This method allows parsers to declare which transaction versions they
259    /// can handle. The registry can use this to select the appropriate parser
260    /// for a given transaction.
261    ///
262    /// # Arguments
263    ///
264    /// * `version` - Chain-specific version identifier
265    ///
266    /// # Returns
267    ///
268    /// * `true` if this parser can handle the version
269    /// * `false` otherwise
270    ///
271    /// # Default Implementation
272    ///
273    /// The default implementation returns `true` for all versions, which is
274    /// suitable for parsers that handle all known transaction types.
275    ///
276    /// # Version Semantics
277    ///
278    /// The meaning of `version` is chain-specific:
279    /// - Ethereum: Transaction type (0=legacy, 1=EIP-2930, 2=EIP-1559, 3=EIP-4844)
280    /// - Bitcoin: Transaction version field
281    /// - Solana: Message version (0=legacy, 128+=versioned)
282    ///
283    /// # Example
284    ///
285    /// ```
286    /// use txgate_chain::Chain;
287    /// # use txgate_core::{ParsedTx, error::ParseError};
288    /// # use txgate_crypto::CurveType;
289    ///
290    /// struct LegacyEthereumParser;
291    ///
292    /// impl Chain for LegacyEthereumParser {
293    ///     fn id(&self) -> &'static str { "ethereum-legacy" }
294    /// #   fn parse(&self, _raw: &[u8]) -> Result<ParsedTx, ParseError> {
295    /// #       Err(ParseError::UnknownTxType)
296    /// #   }
297    /// #   fn curve(&self) -> CurveType { CurveType::Secp256k1 }
298    ///
299    ///     fn supports_version(&self, version: u8) -> bool {
300    ///         // Only support legacy transactions (type 0)
301    ///         version == 0
302    ///     }
303    /// }
304    ///
305    /// let parser = LegacyEthereumParser;
306    /// assert!(parser.supports_version(0));  // Legacy
307    /// assert!(!parser.supports_version(2)); // EIP-1559 not supported
308    /// ```
309    fn supports_version(&self, version: u8) -> bool {
310        // Default: support all versions
311        let _ = version;
312        true
313    }
314}
315
316// ============================================================================
317// Mock Implementation for Testing
318// ============================================================================
319
320/// Error type for `MockChain` configuration.
321///
322/// This enum mirrors the common `ParseError` variants but is `Clone`-able
323/// for use in mock configurations.
324#[cfg(any(test, feature = "mock"))]
325#[derive(Debug, Clone, Copy, PartialEq, Eq)]
326pub enum MockParseError {
327    /// Unknown transaction type.
328    UnknownTxType,
329    /// Malformed transaction.
330    MalformedTransaction,
331    /// Malformed calldata.
332    MalformedCalldata,
333    /// Invalid address.
334    InvalidAddress,
335}
336
337#[cfg(any(test, feature = "mock"))]
338impl MockParseError {
339    /// Convert to a real `ParseError`.
340    #[must_use]
341    pub fn to_parse_error(self, context: &str) -> ParseError {
342        match self {
343            Self::UnknownTxType => ParseError::UnknownTxType,
344            Self::MalformedTransaction => ParseError::MalformedTransaction {
345                context: context.to_string(),
346            },
347            Self::MalformedCalldata => ParseError::MalformedCalldata,
348            Self::InvalidAddress => ParseError::InvalidAddress {
349                address: context.to_string(),
350            },
351        }
352    }
353}
354
355/// A mock chain implementation for testing.
356///
357/// This struct allows tests to configure the behavior of a chain parser
358/// without implementing actual parsing logic.
359///
360/// # Example
361///
362/// ```
363/// use txgate_chain::{Chain, MockChain, MockParseError};
364/// use txgate_core::{ParsedTx, TxType, error::ParseError};
365/// use txgate_crypto::CurveType;
366///
367/// // Create a mock that returns a successful parse result
368/// let mock = MockChain {
369///     id: "test-chain",
370///     curve: CurveType::Secp256k1,
371///     parse_result: Some(ParsedTx {
372///         chain: "test-chain".to_string(),
373///         tx_type: TxType::Transfer,
374///         ..Default::default()
375///     }),
376///     parse_error: None,
377/// };
378///
379/// let result = mock.parse(&[0x01, 0x02, 0x03]);
380/// assert!(result.is_ok());
381///
382/// // Create a mock that returns an error
383/// let error_mock = MockChain {
384///     id: "failing-chain",
385///     curve: CurveType::Ed25519,
386///     parse_result: None,
387///     parse_error: Some(MockParseError::UnknownTxType),
388/// };
389///
390/// let result = error_mock.parse(&[0x01]);
391/// assert!(matches!(result, Err(ParseError::UnknownTxType)));
392/// ```
393#[cfg(any(test, feature = "mock"))]
394#[derive(Debug, Clone)]
395pub struct MockChain {
396    /// The chain identifier to return from `id()`.
397    pub id: &'static str,
398
399    /// The curve type to return from `curve()`.
400    pub curve: CurveType,
401
402    /// The parse result to return (if `parse_error` is `None`).
403    pub parse_result: Option<ParsedTx>,
404
405    /// The parse error to return (takes precedence over `parse_result`).
406    pub parse_error: Option<MockParseError>,
407}
408
409#[cfg(any(test, feature = "mock"))]
410impl Chain for MockChain {
411    fn id(&self) -> &'static str {
412        self.id
413    }
414
415    fn parse(&self, _raw: &[u8]) -> Result<ParsedTx, ParseError> {
416        if let Some(error) = self.parse_error {
417            return Err(error.to_parse_error("mock error"));
418        }
419        self.parse_result
420            .clone()
421            .ok_or_else(|| ParseError::MalformedTransaction {
422                context: "mock not configured with parse_result".to_string(),
423            })
424    }
425
426    fn curve(&self) -> CurveType {
427        self.curve
428    }
429}
430
431#[cfg(any(test, feature = "mock"))]
432impl Default for MockChain {
433    fn default() -> Self {
434        Self {
435            id: "mock",
436            curve: CurveType::Secp256k1,
437            parse_result: Some(ParsedTx::default()),
438            parse_error: None,
439        }
440    }
441}
442
443// ============================================================================
444// Tests
445// ============================================================================
446
447#[cfg(test)]
448mod tests {
449    #![allow(
450        clippy::expect_used,
451        clippy::unwrap_used,
452        clippy::panic,
453        clippy::indexing_slicing,
454        clippy::similar_names,
455        clippy::redundant_clone,
456        clippy::manual_string_new,
457        clippy::needless_raw_string_hashes,
458        clippy::needless_collect,
459        clippy::unreadable_literal
460    )]
461
462    use super::*;
463    use txgate_core::TxType;
464
465    // ------------------------------------------------------------------------
466    // MockChain Tests
467    // ------------------------------------------------------------------------
468
469    #[test]
470    fn test_mock_chain_id() {
471        let mock = MockChain {
472            id: "test-chain",
473            ..Default::default()
474        };
475
476        assert_eq!(mock.id(), "test-chain");
477    }
478
479    #[test]
480    fn test_mock_chain_curve() {
481        let mock_secp = MockChain {
482            curve: CurveType::Secp256k1,
483            ..Default::default()
484        };
485        assert_eq!(mock_secp.curve(), CurveType::Secp256k1);
486
487        let mock_ed = MockChain {
488            curve: CurveType::Ed25519,
489            ..Default::default()
490        };
491        assert_eq!(mock_ed.curve(), CurveType::Ed25519);
492    }
493
494    #[test]
495    fn test_mock_chain_parse_success() {
496        let expected_tx = ParsedTx {
497            hash: [0xab; 32],
498            recipient: Some("0x1234".to_string()),
499            chain: "ethereum".to_string(),
500            tx_type: TxType::Transfer,
501            ..Default::default()
502        };
503
504        let mock = MockChain {
505            id: "ethereum",
506            curve: CurveType::Secp256k1,
507            parse_result: Some(expected_tx.clone()),
508            parse_error: None,
509        };
510
511        let result = mock.parse(&[0x01, 0x02, 0x03]);
512        assert!(result.is_ok());
513
514        let parsed = result.unwrap();
515        assert_eq!(parsed.hash, expected_tx.hash);
516        assert_eq!(parsed.recipient, expected_tx.recipient);
517        assert_eq!(parsed.chain, expected_tx.chain);
518    }
519
520    #[test]
521    fn test_mock_chain_parse_error() {
522        let mock = MockChain {
523            id: "failing",
524            curve: CurveType::Secp256k1,
525            parse_result: None,
526            parse_error: Some(super::MockParseError::UnknownTxType),
527        };
528
529        let result = mock.parse(&[0x01]);
530        assert!(matches!(result, Err(ParseError::UnknownTxType)));
531    }
532
533    #[test]
534    fn test_mock_chain_error_takes_precedence() {
535        // If both parse_error and parse_result are set, error takes precedence
536        let mock = MockChain {
537            id: "test",
538            curve: CurveType::Secp256k1,
539            parse_result: Some(ParsedTx::default()),
540            parse_error: Some(super::MockParseError::MalformedCalldata),
541        };
542
543        let result = mock.parse(&[0x01]);
544        assert!(matches!(result, Err(ParseError::MalformedCalldata)));
545    }
546
547    #[test]
548    fn test_mock_chain_no_result_configured() {
549        let mock = MockChain {
550            id: "unconfigured",
551            curve: CurveType::Secp256k1,
552            parse_result: None,
553            parse_error: None,
554        };
555
556        let result = mock.parse(&[0x01]);
557        assert!(matches!(
558            result,
559            Err(ParseError::MalformedTransaction { .. })
560        ));
561    }
562
563    #[test]
564    fn test_mock_chain_default() {
565        let mock = MockChain::default();
566
567        assert_eq!(mock.id(), "mock");
568        assert_eq!(mock.curve(), CurveType::Secp256k1);
569        assert!(mock.parse(&[]).is_ok());
570    }
571
572    // ------------------------------------------------------------------------
573    // supports_version Tests
574    // ------------------------------------------------------------------------
575
576    #[test]
577    fn test_default_supports_all_versions() {
578        let mock = MockChain::default();
579
580        // Default implementation should support all versions
581        assert!(mock.supports_version(0));
582        assert!(mock.supports_version(1));
583        assert!(mock.supports_version(2));
584        assert!(mock.supports_version(3));
585        assert!(mock.supports_version(255));
586    }
587
588    // ------------------------------------------------------------------------
589    // Trait Object Tests
590    // ------------------------------------------------------------------------
591
592    #[test]
593    fn test_chain_as_trait_object() {
594        let mock = MockChain::default();
595        let chain: Box<dyn Chain> = Box::new(mock);
596
597        assert_eq!(chain.id(), "mock");
598        assert_eq!(chain.curve(), CurveType::Secp256k1);
599        assert!(chain.parse(&[]).is_ok());
600    }
601
602    #[test]
603    fn test_chain_trait_object_vec() {
604        let mock1 = MockChain {
605            id: "chain1",
606            ..Default::default()
607        };
608        let mock2 = MockChain {
609            id: "chain2",
610            curve: CurveType::Ed25519,
611            ..Default::default()
612        };
613
614        let chains: Vec<Box<dyn Chain>> = vec![Box::new(mock1), Box::new(mock2)];
615
616        assert_eq!(chains.len(), 2);
617        assert_eq!(chains[0].id(), "chain1");
618        assert_eq!(chains[1].id(), "chain2");
619        assert_eq!(chains[0].curve(), CurveType::Secp256k1);
620        assert_eq!(chains[1].curve(), CurveType::Ed25519);
621    }
622
623    #[test]
624    fn test_find_chain_by_id() {
625        let chains: Vec<Box<dyn Chain>> = vec![
626            Box::new(MockChain {
627                id: "ethereum",
628                curve: CurveType::Secp256k1,
629                ..Default::default()
630            }),
631            Box::new(MockChain {
632                id: "solana",
633                curve: CurveType::Ed25519,
634                ..Default::default()
635            }),
636        ];
637
638        let found = chains.iter().find(|c| c.id() == "ethereum");
639        assert!(found.is_some());
640        assert_eq!(found.unwrap().curve(), CurveType::Secp256k1);
641
642        let not_found = chains.iter().find(|c| c.id() == "bitcoin");
643        assert!(not_found.is_none());
644    }
645
646    // ------------------------------------------------------------------------
647    // Send + Sync Tests
648    // ------------------------------------------------------------------------
649
650    #[test]
651    fn test_chain_is_send_sync() {
652        fn assert_send_sync<T: Send + Sync>() {}
653        assert_send_sync::<MockChain>();
654    }
655
656    #[test]
657    fn test_chain_trait_object_is_send_sync() {
658        fn assert_send_sync<T: Send + Sync + ?Sized>() {}
659        assert_send_sync::<dyn Chain>();
660    }
661
662    #[test]
663    fn test_boxed_chain_is_send_sync() {
664        fn assert_send_sync<T: Send + Sync>() {}
665        assert_send_sync::<Box<dyn Chain>>();
666    }
667
668    // ------------------------------------------------------------------------
669    // Custom Implementation Test
670    // ------------------------------------------------------------------------
671
672    /// A custom chain implementation for testing custom behavior.
673    struct CustomVersionedChain {
674        supported_versions: Vec<u8>,
675    }
676
677    impl Chain for CustomVersionedChain {
678        fn id(&self) -> &'static str {
679            "custom-versioned"
680        }
681
682        fn parse(&self, raw: &[u8]) -> Result<ParsedTx, ParseError> {
683            if raw.is_empty() {
684                return Err(ParseError::malformed_transaction("empty data"));
685            }
686
687            let version = raw[0];
688            if !self.supports_version(version) {
689                return Err(ParseError::UnknownTxType);
690            }
691
692            Ok(ParsedTx {
693                chain: self.id().to_string(),
694                ..Default::default()
695            })
696        }
697
698        fn curve(&self) -> CurveType {
699            CurveType::Secp256k1
700        }
701
702        fn supports_version(&self, version: u8) -> bool {
703            self.supported_versions.contains(&version)
704        }
705    }
706
707    #[test]
708    fn test_custom_versioned_chain() {
709        let chain = CustomVersionedChain {
710            supported_versions: vec![0, 2],
711        };
712
713        assert!(chain.supports_version(0));
714        assert!(!chain.supports_version(1));
715        assert!(chain.supports_version(2));
716        assert!(!chain.supports_version(3));
717
718        // Version 0 should parse successfully
719        let result = chain.parse(&[0x00, 0x01, 0x02]);
720        assert!(result.is_ok());
721
722        // Version 1 should fail
723        let result = chain.parse(&[0x01, 0x01, 0x02]);
724        assert!(matches!(result, Err(ParseError::UnknownTxType)));
725
726        // Empty data should fail
727        let result = chain.parse(&[]);
728        assert!(matches!(
729            result,
730            Err(ParseError::MalformedTransaction { .. })
731        ));
732    }
733}