Skip to main content

abtc_application/
services.rs

1//! Application services that orchestrate domain logic through ports
2//!
3//! Services implement use cases by coordinating the domain layer with adapters.
4//!
5//! This module implements the core application use cases:
6//! - Block validation with full script verification (including SegWit/BIP143)
7//! - Transaction validation for mempool acceptance
8//! - Mempool management
9//! - Mining block template generation
10
11use abtc_domain::consensus::rules;
12use abtc_domain::consensus::ConsensusParams;
13use abtc_domain::crypto::signing::TransactionSignatureChecker;
14use abtc_domain::primitives::{Block, BlockHash, Transaction};
15use abtc_domain::script::{verify_script_with_witness, ScriptFlags};
16use abtc_ports::{
17    BlockStore, BlockTemplateProvider, ChainStateStore, MempoolPort, PeerManager, UtxoEntry,
18};
19use std::sync::Arc;
20
21/// Blockchain validation and acceptance service
22///
23/// Orchestrates block validation, chain updates, UTXO set management, and consensus enforcement.
24pub struct BlockchainService {
25    block_store: Arc<dyn BlockStore>,
26    chain_state: Arc<dyn ChainStateStore>,
27    peer_manager: Arc<dyn PeerManager>,
28    /// Consensus parameters for the active network (mainnet, testnet, regtest, signet).
29    consensus_params: ConsensusParams,
30}
31
32impl BlockchainService {
33    /// Create a new blockchain service
34    pub fn new(
35        block_store: Arc<dyn BlockStore>,
36        chain_state: Arc<dyn ChainStateStore>,
37        peer_manager: Arc<dyn PeerManager>,
38    ) -> Self {
39        Self::with_params(
40            block_store,
41            chain_state,
42            peer_manager,
43            ConsensusParams::mainnet(),
44        )
45    }
46
47    /// Create a new blockchain service with explicit consensus parameters.
48    pub fn with_params(
49        block_store: Arc<dyn BlockStore>,
50        chain_state: Arc<dyn ChainStateStore>,
51        peer_manager: Arc<dyn PeerManager>,
52        consensus_params: ConsensusParams,
53    ) -> Self {
54        BlockchainService {
55            block_store,
56            chain_state,
57            peer_manager,
58            consensus_params,
59        }
60    }
61
62    /// Validate and accept a block into the blockchain
63    ///
64    /// This performs:
65    /// 1. Duplicate block check
66    /// 2. Merkle root verification
67    /// 3. Consensus rule validation (block structure, transaction validity)
68    /// 4. UTXO set update (remove spent, add new outputs)
69    /// 5. Chain tip update
70    /// 6. Broadcast to peers
71    pub async fn validate_and_accept_block(&self, block: &Block) -> Result<(), String> {
72        let block_hash = block.block_hash();
73        tracing::info!("Validating block: {}", block_hash);
74
75        // Check if block already exists
76        if self
77            .block_store
78            .has_block(&block_hash)
79            .await
80            .map_err(|e| e.to_string())?
81        {
82            return Err("Block already exists".to_string());
83        }
84
85        // Verify merkle root
86        if !block.has_valid_merkle_root() {
87            return Err("Invalid merkle root".to_string());
88        }
89
90        // Validate block against consensus rules
91        rules::check_block(block, &self.consensus_params)
92            .map_err(|e| format!("Block validation failed: {}", e))?;
93
94        // Get current chain tip height
95        let (_, current_height) = self
96            .chain_state
97            .get_best_chain_tip()
98            .await
99            .map_err(|e| e.to_string())?;
100        let new_height = current_height + 1;
101
102        // Standard script verification flags (P2SH + SegWit + strictness)
103        let script_flags = ScriptFlags::standard();
104
105        // Verify scripts for all non-coinbase transaction inputs
106        // This performs full ECDSA/SegWit signature verification against the UTXO set.
107        for tx in block.transactions.iter() {
108            if tx.is_coinbase() {
109                continue;
110            }
111
112            for (input_idx, input) in tx.inputs.iter().enumerate() {
113                // Look up the UTXO being spent
114                let utxo = self
115                    .chain_state
116                    .get_utxo(&input.previous_output.txid, input.previous_output.vout)
117                    .await
118                    .map_err(|e| e.to_string())?
119                    .ok_or_else(|| {
120                        format!(
121                            "Missing UTXO for input {}:{} in tx {}",
122                            input.previous_output.txid,
123                            input.previous_output.vout,
124                            tx.txid()
125                        )
126                    })?;
127
128                let script_pubkey = &utxo.output.script_pubkey;
129                let spent_amount = utxo.output.value;
130
131                // Choose the right signature checker:
132                // - If the output is a witness program (P2WPKH, P2WSH) or P2SH-wrapped witness,
133                //   use BIP143 sighash (new_witness_v0)
134                // - Otherwise use legacy sighash
135                let checker =
136                    if script_pubkey.is_witness_program() || Self::is_p2sh_witness(tx, input_idx) {
137                        TransactionSignatureChecker::new_witness_v0(tx, input_idx, spent_amount)
138                    } else {
139                        TransactionSignatureChecker::new(tx, input_idx, spent_amount)
140                    };
141
142                verify_script_with_witness(
143                    &input.script_sig,
144                    script_pubkey,
145                    &input.witness,
146                    script_flags,
147                    &checker,
148                )
149                .map_err(|e| {
150                    format!(
151                        "Script verification failed for input {} of tx {}: {:?}",
152                        input_idx,
153                        tx.txid(),
154                        e
155                    )
156                })?;
157            }
158        }
159
160        // Update UTXO set: process each transaction
161        let mut utxo_adds = Vec::new();
162        let mut utxo_removes = Vec::new();
163
164        for (tx_idx, tx) in block.transactions.iter().enumerate() {
165            let txid = tx.txid();
166
167            // For non-coinbase transactions, mark inputs as spent
168            if !tx.is_coinbase() {
169                for input in &tx.inputs {
170                    utxo_removes.push((input.previous_output.txid, input.previous_output.vout));
171                }
172            }
173
174            // Add new outputs to UTXO set
175            for (vout, output) in tx.outputs.iter().enumerate() {
176                let entry = UtxoEntry {
177                    output: output.clone(),
178                    height: new_height,
179                    is_coinbase: tx_idx == 0,
180                };
181                utxo_adds.push((txid, vout as u32, entry));
182            }
183        }
184
185        // Write UTXO set changes
186        self.chain_state
187            .write_utxo_set(utxo_adds, utxo_removes)
188            .await
189            .map_err(|e| e.to_string())?;
190
191        // Store the block
192        self.block_store
193            .store_block(block, new_height)
194            .await
195            .map_err(|e| e.to_string())?;
196
197        // Update chain tip
198        self.chain_state
199            .write_chain_tip(block_hash, new_height)
200            .await
201            .map_err(|e| e.to_string())?;
202
203        // Broadcast to peers
204        let peer_count = self
205            .peer_manager
206            .broadcast_block(block)
207            .await
208            .map_err(|e| e.to_string())?;
209
210        tracing::info!(
211            "Accepted block {} at height {} (broadcast to {} peers)",
212            block_hash,
213            new_height,
214            peer_count
215        );
216
217        Ok(())
218    }
219
220    /// Process a new transaction (validate and prepare for mempool)
221    ///
222    /// Performs:
223    /// 1. Basic consensus validation (structure, sizes, amounts)
224    /// 2. Double-spend check against UTXO set
225    /// 3. Input value verification (all inputs exist in UTXO set)
226    pub async fn process_new_transaction(&self, tx: &Transaction) -> Result<(), String> {
227        let txid = tx.txid();
228        tracing::debug!("Processing transaction: {}", txid);
229
230        // Validate transaction against consensus rules
231        rules::check_transaction(tx)
232            .map_err(|e| format!("Transaction validation failed: {}", e))?;
233
234        // Coinbase transactions can't be submitted to mempool
235        if tx.is_coinbase() {
236            return Err("Cannot submit coinbase transaction to mempool".to_string());
237        }
238
239        // Check that all inputs reference existing UTXOs (no double-spends)
240        for input in &tx.inputs {
241            let has_utxo = self
242                .chain_state
243                .has_utxo(&input.previous_output.txid, input.previous_output.vout)
244                .await
245                .map_err(|e| e.to_string())?;
246
247            if !has_utxo {
248                return Err(format!(
249                    "Input {}:{} references non-existent or already-spent UTXO",
250                    input.previous_output.txid, input.previous_output.vout
251                ));
252            }
253        }
254
255        // Verify total input value >= total output value
256        let mut total_input_value: i64 = 0;
257        for input in &tx.inputs {
258            let utxo = self
259                .chain_state
260                .get_utxo(&input.previous_output.txid, input.previous_output.vout)
261                .await
262                .map_err(|e| e.to_string())?
263                .ok_or_else(|| "UTXO disappeared during validation".to_string())?;
264
265            total_input_value += utxo.output.value.as_sat();
266        }
267
268        let total_output_value = tx.total_output_value().as_sat();
269        if total_input_value < total_output_value {
270            return Err(format!(
271                "Transaction outputs ({}) exceed inputs ({})",
272                total_output_value, total_input_value
273            ));
274        }
275
276        let fee = total_input_value - total_output_value;
277
278        // Verify scripts for all inputs (full signature + SegWit verification)
279        let script_flags = ScriptFlags::standard();
280
281        for (input_idx, input) in tx.inputs.iter().enumerate() {
282            let utxo = self
283                .chain_state
284                .get_utxo(&input.previous_output.txid, input.previous_output.vout)
285                .await
286                .map_err(|e| e.to_string())?
287                .ok_or_else(|| "UTXO disappeared during script verification".to_string())?;
288
289            let script_pubkey = &utxo.output.script_pubkey;
290            let spent_amount = utxo.output.value;
291
292            let checker =
293                if script_pubkey.is_witness_program() || Self::is_p2sh_witness(tx, input_idx) {
294                    TransactionSignatureChecker::new_witness_v0(tx, input_idx, spent_amount)
295                } else {
296                    TransactionSignatureChecker::new(tx, input_idx, spent_amount)
297                };
298
299            verify_script_with_witness(
300                &input.script_sig,
301                script_pubkey,
302                &input.witness,
303                script_flags,
304                &checker,
305            )
306            .map_err(|e| {
307                format!(
308                    "Script verification failed for input {} of tx {}: {:?}",
309                    input_idx, txid, e
310                )
311            })?;
312        }
313
314        tracing::debug!("Transaction {} validated (fee: {} satoshis)", txid, fee);
315
316        Ok(())
317    }
318
319    /// Detect if a transaction input is a P2SH-wrapped witness program.
320    ///
321    /// P2SH-P2WPKH and P2SH-P2WSH have a scriptSig that contains a single push
322    /// of the witness program. We detect this by checking if the scriptSig's last
323    /// data push is a valid witness program (version byte 0x00-0x10, followed by
324    /// 20 or 32 byte program).
325    fn is_p2sh_witness(tx: &Transaction, input_idx: usize) -> bool {
326        let script_sig = &tx.inputs[input_idx].script_sig;
327        let bytes = script_sig.as_bytes();
328
329        // A P2SH-wrapped witness scriptSig is a single push of a witness program.
330        // Minimal witness programs are 22 bytes (P2WPKH: 0x0014{20}) or 34 bytes (P2WSH: 0x0020{32}).
331        // The scriptSig would be: push_opcode + witness_program
332        if bytes.is_empty() {
333            return false;
334        }
335
336        // Try to interpret the scriptSig as a single push of a witness program
337        // P2SH-P2WPKH scriptSig: 0x16 0x0014{20-byte-hash} (23 bytes total)
338        // P2SH-P2WSH scriptSig:  0x22 0x0020{32-byte-hash} (35 bytes total)
339        #[allow(clippy::if_same_then_else)]
340        let inner = if bytes.len() == 23 && bytes[0] == 0x16 {
341            &bytes[1..]
342        } else if bytes.len() == 35 && bytes[0] == 0x22 {
343            &bytes[1..]
344        } else {
345            return false;
346        };
347
348        // Check if the inner data is a valid witness program
349        // Version byte: 0x00 (OP_0) or 0x51-0x60 (OP_1..OP_16)
350        // Followed by a push of 20 or 32 bytes
351        if inner.len() >= 2 {
352            let version_byte = inner[0];
353            let push_len = inner[1] as usize;
354            let is_valid_version =
355                version_byte == 0x00 || (0x51..=0x60).contains(&version_byte);
356            let is_valid_program =
357                (push_len == 20 || push_len == 32) && inner.len() == 2 + push_len;
358            return is_valid_version && is_valid_program;
359        }
360
361        false
362    }
363
364    /// Get current chain information
365    pub async fn get_chain_info(&self) -> Result<ChainInfo, String> {
366        let (best_block_hash, height) = self
367            .chain_state
368            .get_best_chain_tip()
369            .await
370            .map_err(|e| e.to_string())?;
371
372        Ok(ChainInfo {
373            best_block_hash,
374            height,
375            blocks: height + 1,
376        })
377    }
378
379    /// Get a block by hash
380    pub async fn get_block(&self, hash: &BlockHash) -> Result<Option<Block>, String> {
381        self.block_store
382            .get_block(hash)
383            .await
384            .map_err(|e| e.to_string())
385    }
386}
387
388/// Mempool transaction management service
389pub struct MempoolService {
390    mempool: Arc<dyn MempoolPort>,
391    /// Chain state (available for height-aware fee policies).
392    _chain_state: Arc<dyn ChainStateStore>,
393}
394
395impl MempoolService {
396    /// Create a new mempool service.
397    pub fn new(mempool: Arc<dyn MempoolPort>, chain_state: Arc<dyn ChainStateStore>) -> Self {
398        MempoolService {
399            mempool,
400            _chain_state: chain_state,
401        }
402    }
403
404    /// Submit a transaction to the mempool
405    pub async fn submit_transaction(&self, tx: &Transaction) -> Result<String, String> {
406        tracing::info!("Submitting transaction: {}", tx.txid());
407
408        self.mempool
409            .add_transaction(tx)
410            .await
411            .map_err(|e| e.to_string())?;
412
413        Ok(tx.txid().to_hex_reversed())
414    }
415
416    /// Get all transactions in the mempool
417    pub async fn get_mempool_contents(&self) -> Result<Vec<String>, String> {
418        let txs = self
419            .mempool
420            .get_all_transactions()
421            .await
422            .map_err(|e| e.to_string())?;
423
424        Ok(txs
425            .iter()
426            .map(|entry| entry.tx.txid().to_hex_reversed())
427            .collect())
428    }
429
430    /// Estimate fee for a transaction
431    pub async fn estimate_fee(&self, target_blocks: u32) -> Result<f64, String> {
432        self.mempool
433            .estimate_fee(target_blocks)
434            .await
435            .map_err(|e| e.to_string())
436    }
437
438    /// Get mempool statistics
439    pub async fn get_mempool_info(&self) -> Result<abtc_ports::MempoolInfo, String> {
440        self.mempool
441            .get_mempool_info()
442            .await
443            .map_err(|e| e.to_string())
444    }
445
446    /// Look up a single transaction in the mempool by txid.
447    pub async fn get_mempool_entry(
448        &self,
449        txid: &abtc_domain::primitives::Txid,
450    ) -> Option<abtc_ports::MempoolEntry> {
451        self.mempool.get_transaction(txid).await.ok().flatten()
452    }
453}
454
455/// Block mining service
456pub struct MiningService {
457    template_provider: Arc<dyn BlockTemplateProvider>,
458    blockchain: Arc<BlockchainService>,
459    /// Consensus parameters for block template generation.
460    consensus_params: ConsensusParams,
461}
462
463impl MiningService {
464    /// Create a new mining service (defaults to mainnet consensus).
465    pub fn new(
466        template_provider: Arc<dyn BlockTemplateProvider>,
467        blockchain: Arc<BlockchainService>,
468    ) -> Self {
469        Self::with_params(template_provider, blockchain, ConsensusParams::mainnet())
470    }
471
472    /// Create a new mining service with explicit consensus parameters.
473    pub fn with_params(
474        template_provider: Arc<dyn BlockTemplateProvider>,
475        blockchain: Arc<BlockchainService>,
476        consensus_params: ConsensusParams,
477    ) -> Self {
478        MiningService {
479            template_provider,
480            blockchain,
481            consensus_params,
482        }
483    }
484
485    /// Generate a block template for miners
486    pub async fn generate_block_template(
487        &self,
488        coinbase_script: &abtc_domain::script::Script,
489    ) -> Result<abtc_ports::BlockTemplate, String> {
490        self.template_provider
491            .create_block_template(coinbase_script, &self.consensus_params)
492            .await
493            .map_err(|e| e.to_string())
494    }
495
496    /// Submit a mined block
497    pub async fn submit_mined_block(&self, block: &Block) -> Result<(), String> {
498        self.blockchain.validate_and_accept_block(block).await
499    }
500}
501
502/// Information about the current chain state.
503#[derive(Clone, Debug)]
504pub struct ChainInfo {
505    /// Hash of the best (most-work) block.
506    pub best_block_hash: BlockHash,
507    /// Height of the best block (0 for genesis).
508    pub height: u32,
509    /// Total number of blocks including genesis.
510    pub blocks: u32,
511}
512
513#[cfg(test)]
514mod tests {
515    use super::*;
516
517    #[test]
518    fn test_chain_info_creation() {
519        let info = ChainInfo {
520            best_block_hash: BlockHash::zero(),
521            height: 0,
522            blocks: 1,
523        };
524        assert_eq!(info.height, 0);
525    }
526}