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}