bitcoin_pool_identification/
lib.rs

1//! # Bitcoin Mining Pool Identification
2//! Crate to identify Bitcoin mining pools based on coinbase transaction
3//! metadata like, for example, pool set coinbase tags or coinbase output
4//! addresses.
5
6use bitcoin::{Address, Block, Network, Transaction};
7use serde::{Deserialize, Serialize};
8use std::str::FromStr;
9
10pub const DEFAULT_MAINNET_POOL_LIST: &str = include_str!("../known-mining-pools/pool-list.json");
11pub const DEFAULT_SIGNET_POOL_LIST: &str =
12    include_str!("../known-mining-pools/signet-pool-list.json");
13
14/// Models a mining pool with a name and optionally a link to the pool website.
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct Pool {
17    /// Unique id of the mining pool. Can, for example, be used as a database id.
18    pub id: u64,
19    /// Name of the mining pool.
20    pub name: String,
21    /// The addresses the mining pool uses or used in the past.
22    pub addresses: Vec<String>,
23    /// The coinbase tags the mining pool uses or used in the past.
24    pub tags: Vec<String>,
25    /// Optional link to the mining pools website.
26    pub link: String,
27}
28
29impl Pool {
30    pub fn addresses(self, network: Network) -> Vec<Address> {
31        self.addresses
32            .iter()
33            .map(|a| {
34                Address::from_str(a)
35                    .unwrap()
36                    .require_network(network)
37                    .unwrap()
38            })
39            .collect()
40    }
41}
42
43/// Parses a JSON formatted list of pools entries. Format:
44/// ```json
45/// [
46/// {
47///  "id": 7,
48///  "name": "Example Pool",
49///  "addresses": [
50///    "15kDhRAcpgsugmh6mQsTcCHdvbsuYncEEV",
51///    "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"
52///  ],
53///  "tags": [
54///    "/example/",
55///    "Example Pool"
56///  ],
57///  "link": "https://example.com"
58/// }
59/// ]
60/// ```
61///
62/// This format is used and produced by e.g. https://github.com/bitcoin-data/mining-pools
63/// and https://github.com/bitcoin-data/mining-pools/tree/generated
64pub fn parse_json(json: &str) -> Result<Vec<Pool>, serde_json::Error> {
65    serde_json::from_str(json)
66}
67
68/// Returns the default pool list for the given [Network]. These lists might
69/// not be fully up-to-date. Currently, only default lists for
70/// [Network::Bitcoin] (mainnet) and [Network:Signet] are provided. The
71/// defaults for [Network::Testnet] and [Network::Regtest] are empty lists.
72/// For all other possible future networks (Network is non_exhaustive), an
73/// empty list is returned.
74pub fn default_data(network: Network) -> Vec<Pool> {
75    match network {
76        Network::Bitcoin => parse_json(DEFAULT_MAINNET_POOL_LIST)
77            .expect("the default mainnet JSON list should be parseable"),
78        Network::Testnet => vec![], // update the comment above when changeing this
79        Network::Signet => parse_json(DEFAULT_SIGNET_POOL_LIST)
80            .expect("the default signet JSON list should be parseable"),
81        Network::Regtest => vec![], // update the comment above when changeing this
82        _ => vec![],                // update the comment above when changeing this
83    }
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87pub enum IdentificationMethod {
88    /// The [Pool] was identified via a known coinbase output address.
89    Address,
90    /// The [Pool] was identified via a known tag in the coinbase script sig.
91    Tag,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95pub struct IdentificationResult {
96    /// The pool that was identified.
97    pub pool: Pool,
98    /// The method the pool was identified with.
99    pub identification_method: IdentificationMethod,
100}
101
102/// Trait for Bitcoin mining pool identification based on metadata like coinbase
103/// tags or coinbase output addresses.
104pub trait PoolIdentification {
105    /// Checks both the coinbase output address and coinbase tags against known
106    /// values to identify a mining pool. The coinbase output address is
107    /// checked first, as it is harder to fake than the coinbase tag. Coinbase
108    /// tags are not authenticated and can easily be faked by a malicious party.
109    ///
110    /// If both methods can't identify the pool, then `None` is returned.
111    ///
112    /// /// # Examples
113    ///
114    /// ```
115    /// use bitcoin::{Transaction, Network};
116    /// use bitcoin_pool_identification::{IdentificationMethod, Pool, PoolIdentification, default_data, DEFAULT_MAINNET_POOL_LIST};
117    /// use bitcoin::hex::FromHex;
118    ///
119    /// // Bitcoin mainnet coinbase transaction of block 670828 mined by ViaBTC:
120    /// // 71093a08fe47c9d0c08921049f1a317541d78470376d7029c5e27fda2205361b
121    /// let rawtx = Vec::from_hex("010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff5f036c3c0a1c2f5669614254432f4d696e6564206279206c6565686f6f343434342f2cfabe6d6d41f647100ea398435411f0297fd9d798625e1b82c82451f7c6ccb59c0c67ec07100000000000000010d02cfe0845dca9281bb0ee077c090000ffffffff04bdb8892b000000001976a914536ffa992491508dca0354e52f32a3a7a679a53a88ac00000000000000002b6a2952534b424c4f434b3a2f21f07f3619ef6521a90de396c2617f2edc5bda4fd04aba89632f2c002f79bc0000000000000000266a24b9e11b6d2dd1c7233a019c512c5f1e105e185a6ea0a47824b5ae390cc7cec5c01714588b0000000000000000266a24aa21a9ed23418324183dba97076f21aadc97aeeb1782c6859faf8e141c601e5c856c55440120000000000000000000000000000000000000000000000000000000000000000000000000").unwrap();
122    /// let tx: Transaction = bitcoin::consensus::deserialize(&rawtx).unwrap();
123    /// let pools = default_data(Network::Bitcoin);
124    /// let pool = tx.identify_pool(Network::Bitcoin, &pools).unwrap().pool;
125    /// assert_eq!(pool.name, "ViaBTC".to_string());
126    fn identify_pool(&self, network: Network, pools: &[Pool]) -> Option<IdentificationResult> {
127        let result = self.identify_coinbase_output_address(network, pools);
128        if result.is_some() {
129            return result;
130        } else {
131            let result = self.identify_coinbase_tag(pools);
132            if result.is_some() {
133                return result;
134            }
135        }
136        None
137    }
138
139    /// Checks coinbase tags from against the UTF-8 encoded coinbase script_sig
140    /// to identify mining pools.
141    ///
142    /// These coinbase tags are not authenticated and can easily be faked by a
143    /// malicious party.
144    ///
145    /// The coinbase tag for the ViaBTC pool is, for example, `/ViaBTC/`. An
146    /// UTF-8 encoded coinbase looks, for example, like (line breaks removed):
147    /// ```text
148    /// l</ViaBTC/Mined by leehoo4444/,��mmA�G��CT�)�טb^��̵�g��,Eܩ(
149    /// ```
150    fn identify_coinbase_tag(&self, pools: &[Pool]) -> Option<IdentificationResult>;
151
152    /// Checks the coinbase output address against a list of known pool
153    /// addresses and returns a found pool. If no output address matches, then
154    /// `None` is returned.
155    fn identify_coinbase_output_address(
156        &self,
157        network: Network,
158        pools: &[Pool],
159    ) -> Option<IdentificationResult>;
160
161    /// Returns the coinbase script encoded as lossy UTF-8 String (any invalid
162    /// UTF-8 sequences with U+FFFD REPLACEMENT CHARACTER, which looks like
163    /// this: �). Line-breaks are removed as well.
164    fn coinbase_script_as_utf8(&self) -> String;
165
166    /// Returns the coinbase output addresses for all output types that can be
167    /// represented as addresses. This excludes, for example, P2PK or OP_RETURN
168    /// outputs. Addresses are ordered by value (descending).
169    fn coinbase_output_addresses(&self, network: Network) -> Vec<Address>;
170}
171
172impl PoolIdentification for Transaction {
173    /// Checks coinbase tags from against the UTF-8 encoded coinbase script_sig
174    /// to identify mining pools.
175    ///
176    /// These coinbase tags are not authenticated and can easily be faked by a
177    /// malicious party.
178    ///
179    /// The coinbase tag for the ViaBTC pool is, for example, `/ViaBTC/`. An
180    /// UTF-8 encoded coinbase looks, for example, like (line breaks removed):
181    /// ```text
182    /// l</ViaBTC/Mined by leehoo4444/,��mmA�G��CT�)�טb^��̵�g��,Eܩ(
183    /// ```
184    ///
185    /// # Panics
186    ///
187    /// The caller MUST make sure the transaction is a **coinbase transaction**
188    /// This can be done, for example, with [Transaction::is_coin_base()]. This
189    /// is asserted and will panic.
190    ///
191    /// # Examples
192    ///
193    /// ```
194    /// use bitcoin::{Transaction, Network};
195    /// use bitcoin_pool_identification::{Pool, PoolIdentification, DEFAULT_MAINNET_POOL_LIST, default_data};
196    /// use bitcoin::hex::FromHex;
197    ///
198    /// // Bitcoin mainnet coinbase transaction of block 670828 mined by ViaBTC:
199    /// // 71093a08fe47c9d0c08921049f1a317541d78470376d7029c5e27fda2205361b
200    /// let rawtx = Vec::from_hex("010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff5f036c3c0a1c2f5669614254432f4d696e6564206279206c6565686f6f343434342f2cfabe6d6d41f647100ea398435411f0297fd9d798625e1b82c82451f7c6ccb59c0c67ec07100000000000000010d02cfe0845dca9281bb0ee077c090000ffffffff04bdb8892b000000001976a914536ffa992491508dca0354e52f32a3a7a679a53a88ac00000000000000002b6a2952534b424c4f434b3a2f21f07f3619ef6521a90de396c2617f2edc5bda4fd04aba89632f2c002f79bc0000000000000000266a24b9e11b6d2dd1c7233a019c512c5f1e105e185a6ea0a47824b5ae390cc7cec5c01714588b0000000000000000266a24aa21a9ed23418324183dba97076f21aadc97aeeb1782c6859faf8e141c601e5c856c55440120000000000000000000000000000000000000000000000000000000000000000000000000").unwrap();
201    /// let tx: Transaction = bitcoin::consensus::deserialize(&rawtx).unwrap();
202    /// let pools = default_data(Network::Bitcoin);
203    /// let pool = tx.identify_coinbase_tag(&pools).unwrap().pool;
204    /// assert_eq!(pool.name, "ViaBTC");
205    /// ```
206    fn identify_coinbase_tag(&self, pools: &[Pool]) -> Option<IdentificationResult> {
207        assert!(self.is_coinbase());
208        let utf8_coinbase = self.coinbase_script_as_utf8();
209        for pool in pools.iter() {
210            for tag in pool.tags.iter() {
211                if utf8_coinbase.contains(tag) {
212                    return Some(IdentificationResult {
213                        pool: pool.clone(),
214                        identification_method: IdentificationMethod::Tag,
215                    });
216                }
217            }
218        }
219        None
220    }
221
222    /// Checks the coinbase output address against a list of known pool
223    /// addresses and returns a found pool. If no output address matches, then
224    /// `None` is returned.
225    ///
226    /// # Panics
227    ///
228    /// The caller MUST make sure the transaction is a **coinbase transaction**
229    /// This can be done, for example, with [Transaction::is_coinbase()].
230    ///
231    /// # Examples
232    ///
233    /// ```
234    /// use bitcoin::{Transaction, Network};
235    /// use bitcoin_pool_identification::{IdentificationMethod, Pool, PoolIdentification, DEFAULT_MAINNET_POOL_LIST, default_data};
236    /// use bitcoin::hex::FromHex;
237    ///
238    /// // Bitcoin mainnet coinbase transaction of block 670828 mined by ViaBTC:
239    /// // 71093a08fe47c9d0c08921049f1a317541d78470376d7029c5e27fda2205361b
240    /// let rawtx = Vec::from_hex("010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff5f036c3c0a1c2f5669614254432f4d696e6564206279206c6565686f6f343434342f2cfabe6d6d41f647100ea398435411f0297fd9d798625e1b82c82451f7c6ccb59c0c67ec07100000000000000010d02cfe0845dca9281bb0ee077c090000ffffffff04bdb8892b000000001976a914536ffa992491508dca0354e52f32a3a7a679a53a88ac00000000000000002b6a2952534b424c4f434b3a2f21f07f3619ef6521a90de396c2617f2edc5bda4fd04aba89632f2c002f79bc0000000000000000266a24b9e11b6d2dd1c7233a019c512c5f1e105e185a6ea0a47824b5ae390cc7cec5c01714588b0000000000000000266a24aa21a9ed23418324183dba97076f21aadc97aeeb1782c6859faf8e141c601e5c856c55440120000000000000000000000000000000000000000000000000000000000000000000000000").unwrap();
241    /// let tx: Transaction = bitcoin::consensus::deserialize(&rawtx).unwrap();
242    /// let pools = default_data(Network::Bitcoin);
243    /// let pool = tx.identify_coinbase_output_address(Network::Bitcoin, &pools);
244    /// assert_eq!(pool, None);
245    /// ```
246    fn identify_coinbase_output_address(
247        &self,
248        network: Network,
249        pools: &[Pool],
250    ) -> Option<IdentificationResult> {
251        for address in self.coinbase_output_addresses(network) {
252            for pool in pools {
253                for pool_address in pool.clone().addresses(network) {
254                    if pool_address == address {
255                        return Some(IdentificationResult {
256                            pool: pool.clone(),
257                            identification_method: IdentificationMethod::Address,
258                        });
259                    }
260                }
261            }
262        }
263        None
264    }
265
266    /// Returns the coinbase script encoded as lossy UTF-8 String (any invalid
267    /// UTF-8 sequences with U+FFFD REPLACEMENT CHARACTER, which looks like
268    /// this: �). Line-breaks are removed as well.
269    ///
270    /// # Panics
271    ///
272    /// The caller MUST make sure the transaction is a **coinbase transaction**
273    /// This can be done, for example, with [Transaction::is_coinbase()]. This
274    /// is asserted and will panic.
275    fn coinbase_script_as_utf8(&self) -> String {
276        assert!(self.is_coinbase());
277        let in0 = &self.input[0];
278        String::from_utf8_lossy(in0.script_sig.as_bytes())
279            .replace('\n', "")
280            .to_string()
281    }
282
283    /// Returns the coinbase output addresses for all output types that can be
284    /// represented as addresses. This excludes, for example, P2PK or OP_RETURN
285    /// outputs. Addresses are ordered by value (descending).
286    ///
287    /// # Panics
288    ///
289    /// The caller MUST make sure the transaction is a **coinbase transaction**
290    /// This can be done, for example, with [Transaction::is_coinbase()].
291    ///
292    fn coinbase_output_addresses(&self, network: Network) -> Vec<Address> {
293        assert!(self.is_coinbase());
294        let mut outputs = self.output.clone();
295        outputs.sort_by_key(|o| o.value);
296
297        let mut addresses = vec![];
298        for out in outputs {
299            if let Ok(address) = Address::from_script(&out.script_pubkey, network) {
300                addresses.push(address);
301            }
302        }
303        addresses
304    }
305}
306
307impl PoolIdentification for Block {
308    /// Checks coinbase tags from against the UTF-8 encoded coinbase script_sig
309    /// to identify mining pools.
310    ///
311    /// These coinbase tags are not authenticated and can easily be faked by a
312    /// malicious party.
313    ///
314    /// The coinbase tag for the ViaBTC pool is, for example, `/ViaBTC/`. An
315    /// UTF-8 encoded coinbase looks, for example, like (line breaks removed):
316    /// ```text
317    /// l</ViaBTC/Mined by leehoo4444/,��mmA�G��CT�)�טb^��̵�g��,Eܩ(
318    /// ```
319    fn identify_coinbase_tag(&self, pools: &[Pool]) -> Option<IdentificationResult> {
320        let coinbase = self.txdata.first().unwrap();
321        coinbase.identify_coinbase_tag(pools)
322    }
323
324    /// Checks the coinbase output addresses against a list of known pool
325    /// addresses and returns a found pool. If no output address matches, then
326    /// `None` is returned.
327    fn identify_coinbase_output_address(
328        &self,
329        network: Network,
330        pools: &[Pool],
331    ) -> Option<IdentificationResult> {
332        let coinbase = self.txdata.first().unwrap();
333        coinbase.identify_coinbase_output_address(network, pools)
334    }
335
336    /// Returns the coinbase script encoded as lossy UTF-8 String (any invalid
337    /// UTF-8 sequences with U+FFFD REPLACEMENT CHARACTER, which looks like
338    /// this: �). Line-breaks are removed as well.
339    fn coinbase_script_as_utf8(&self) -> String {
340        self.txdata.first().unwrap().coinbase_script_as_utf8()
341    }
342
343    /// Returns the coinbase output addresses for all output types that can be
344    /// represented as addresses. This excludes, for example, P2PK or OP_RETURN
345    /// outputs. Addresses are ordered by value (descending).
346    fn coinbase_output_addresses(&self, network: Network) -> Vec<Address> {
347        self.txdata
348            .first()
349            .unwrap()
350            .coinbase_output_addresses(network)
351    }
352}
353
354#[cfg(test)]
355mod tests {
356
357    use crate::{default_data, IdentificationResult};
358
359    use super::{IdentificationMethod, Pool, PoolIdentification};
360    use bitcoin::hex::FromHex;
361    use bitcoin::{Block, Network, Transaction};
362
363    #[test]
364    fn test_block_10000() {
365        // Bitcoin mainnet block at height 10000 likely mined by a solo miner:
366        // 0000000099c744455f58e6c6e98b671e1bf7f37346bfd4cf5d0274ad8ee660cb
367        let raw_block = Vec::from_hex("01000000a7c3299ed2475e1d6ea5ed18d5bfe243224add249cce99c5c67cc9fb00000000601c73862a0a7238e376f497783c8ecca2cf61a4f002ec8898024230787f399cb575d949ffff001d3a5de07f0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0804ffff001d026f03ffffffff0100f2052a010000004341042f462d3245d2f3a015f7f9505f763ee1080cab36191d07ae9e6509f71bb68818719e6fb41c019bf48ae11c45b024d476e19b6963103ce8647fc15fee513b15c7ac00000000").unwrap();
368        let block: Block = bitcoin::consensus::deserialize(&raw_block).unwrap();
369        let pools = default_data(Network::Bitcoin);
370
371        assert_eq!(block.identify_pool(Network::Bitcoin, &pools.clone()), None);
372        assert_eq!(
373            block.identify_coinbase_output_address(Network::Bitcoin, &pools),
374            None
375        );
376        assert_eq!(block.identify_coinbase_tag(&pools), None);
377    }
378
379    #[test]
380    fn test_block_btccom() {
381        // Bitcoin mainnet block at height 670718 mined by BTC.com:
382        // 0000000000000000000566438fa7dc31ec2b26e8cfd0a704822238ee8a40922c
383        // Identified by both its coinbase tag and output address.
384        let raw_block = Vec::from_hex("00e0ff3f0c85cd07e4c8b446f64d9179ddd7627d4858f9bd07df08000000000000000000b263e9b0077a5f8ea941f8498a0df7b88d6d2077e9be4ef9d5b5f5b8e77906c9c56b2a60b9210d173aa2253a0102000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4c03fe3b0a04c16b2a6065752f4254432e636f6d2ffabe6d6d5793cfdad17c5272fca204a71fb04e88a5955239c018b8e5186ce838e789f7d4020000008e9b20aa04f5d252bb00000000000000ffffffff0340be4025000000001976a91474e878616bd5e5236ecb22667627eeecbff54b9f88ac00000000000000002b6a2952534b424c4f434b3a2dcf611172e7f2605b32915ca21102a7b0139400413995a4df47ea0b002ee6900000000000000000266a24b9e11b6d3974264c2913656ea4ee829e6327179645a5e8b4dc463914680b2003569a36e200000000").unwrap();
385        let block: Block = bitcoin::consensus::deserialize(&raw_block).unwrap();
386        let pools = default_data(Network::Bitcoin);
387
388        let pool = Pool {
389            id: 43,
390            addresses: vec![
391                "1Bf9sZvBHPFGVPX71WX2njhd1NXKv5y7v5".to_string(),
392                "34qkc2iac6RsyxZVfyE2S5U5WcRsbg2dpK".to_string(),
393                "36cWgjEtxQSFgbHTJDw5AB96ZX7fJk1URd".to_string(),
394                "3EhLZarJUNSfV6TWMZY1Nh5mi3FMsdHa5U".to_string(),
395                "3NA8hsjfdgVkmmVS9moHmkZsVCoLxUkvvv".to_string(),
396                "bc1qjl8uwezzlech723lpnyuza0h2cdkvxvh54v3dn".to_string(),
397            ],
398            tags: vec![
399                "/BTC.COM/".to_string(),
400                "/BTC.com/".to_string(),
401                "btccom".to_string(),
402            ],
403            name: "BTC.com".to_string(),
404            link: "https://pool.btc.com".to_string(),
405        };
406
407        let expected_address = Some(IdentificationResult {
408            pool: pool.clone(),
409            identification_method: IdentificationMethod::Address,
410        });
411
412        let expected_tag = Some(IdentificationResult {
413            pool,
414            identification_method: IdentificationMethod::Tag,
415        });
416
417        assert_eq!(
418            block.identify_pool(Network::Bitcoin, &pools.clone()),
419            expected_address
420        );
421        assert_eq!(
422            block.identify_coinbase_output_address(Network::Bitcoin, &pools),
423            expected_address
424        );
425        assert_eq!(block.identify_coinbase_tag(&pools), expected_tag);
426    }
427
428    #[test]
429    fn test_coinbase_slushpool() {
430        // Bitcoin mainnet coinbase transaction of block 670987 mined by SlushPool:
431        // 069dc08e89524fb1f2120ecc383ec54bc3e54b9c63716ba4352147dcdd7240a6
432        // Identified by both it's coinbase output address and coinbase tag.
433        let rawtx = Vec::from_hex("010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff4b030b3d0afabe6d6d87a2773b0dfb971a762db2fd5a473882417a86aa7e1a2993feec04bfa383f93701000000000000002b6501031eb6e5300303000000000002c54ac6082f736c7573682f0000000003f09e942b000000001976a9147c154ed1dc59609e3d26abb2df2ea3d587cd8c4188ac00000000000000002c6a4c2952534b424c4f434b3ae47c0b11ada150b68f298a42147c6a1817907b6e0b435b0021057134002f87000000000000000000266a24aa21a9eda2fe9c7da3d1b9c033e1caa2064e844e1a1b46cf80c4a10c5d1cc15a34f252450120000000000000000000000000000000000000000000000000000000000000000000000000").unwrap();
434        let tx: Transaction = bitcoin::consensus::deserialize(&rawtx).unwrap();
435        let pools = default_data(Network::Bitcoin);
436
437        let pool = Pool {
438            id: 119,
439            addresses: vec![
440                "1AqTMY7kmHZxBuLUR5wJjPFUvqGs23sesr".to_string(),
441                "1CK6KHY6MHgYvmRQ4PAafKYDrg1ejbH1cE".to_string(),
442            ],
443            tags: vec!["/slush/".to_string()],
444            name: "Braiins Pool".to_string(),
445            link: "https://braiins.com/".to_string(),
446        };
447
448        let expected_address = Some(IdentificationResult {
449            pool: pool.clone(),
450            identification_method: IdentificationMethod::Address,
451        });
452
453        let expected_tag = Some(IdentificationResult {
454            pool,
455            identification_method: IdentificationMethod::Tag,
456        });
457
458        assert_eq!(tx.identify_pool(Network::Bitcoin, &pools), expected_address);
459        assert_eq!(
460            tx.identify_coinbase_output_address(Network::Bitcoin, &pools),
461            expected_address
462        );
463        assert_eq!(tx.identify_coinbase_tag(&pools), expected_tag);
464    }
465
466    #[test]
467    fn test_coinbase_viabtc() {
468        // Bitcoin mainnet coinbase transaction of block 670828 mined by ViaBTC:
469        // 71093a08fe47c9d0c08921049f1a317541d78470376d7029c5e27fda2205361b
470        // Identified by it's coinbase tag.
471        let rawtx = Vec::from_hex("010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff5f036c3c0a1c2f5669614254432f4d696e6564206279206c6565686f6f343434342f2cfabe6d6d41f647100ea398435411f0297fd9d798625e1b82c82451f7c6ccb59c0c67ec07100000000000000010d02cfe0845dca9281bb0ee077c090000ffffffff04bdb8892b000000001976a914536ffa992491508dca0354e52f32a3a7a679a53a88ac00000000000000002b6a2952534b424c4f434b3a2f21f07f3619ef6521a90de396c2617f2edc5bda4fd04aba89632f2c002f79bc0000000000000000266a24b9e11b6d2dd1c7233a019c512c5f1e105e185a6ea0a47824b5ae390cc7cec5c01714588b0000000000000000266a24aa21a9ed23418324183dba97076f21aadc97aeeb1782c6859faf8e141c601e5c856c55440120000000000000000000000000000000000000000000000000000000000000000000000000").unwrap();
472        let tx: Transaction = bitcoin::consensus::deserialize(&rawtx).unwrap();
473        let pools = default_data(Network::Bitcoin);
474
475        let pool = Pool {
476            id: 110,
477            addresses: vec!["1PuJjnF476W3zXfVYmJfGnouzFDAXakkL4".to_string()],
478            tags: vec!["/ViaBTC/".to_string(), "viabtc.com deploy".to_string()],
479            name: "ViaBTC".to_string(),
480            link: "https://viabtc.com".to_string(),
481        };
482
483        let expected_tag = Some(IdentificationResult {
484            pool: pool,
485            identification_method: IdentificationMethod::Tag,
486        });
487
488        assert_eq!(tx.identify_pool(Network::Bitcoin, &pools), expected_tag);
489        assert_eq!(
490            tx.identify_coinbase_output_address(Network::Bitcoin, &pools),
491            None
492        );
493        assert_eq!(tx.identify_coinbase_tag(&pools), expected_tag);
494    }
495
496    #[test]
497    fn test_coinbase_ghashio() {
498        // Bitcoin mainnet coinbase transaction of block 300000 mined by GHashIO:
499        // b39fa6c39b99683ac8f456721b270786c627ecb246700888315991877024b983
500        // Identified by its output address.
501        let rawtx = Vec::from_hex("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4803e09304062f503253482f0403c86d53087ceca141295a00002e522cfabe6d6d7561cf262313da1144026c8f7a43e3899c44f6145f39a36507d36679a8b7006104000000000000000000000001c8704095000000001976a91480ad90d403581fa3bf46086a91b2d9d4125db6c188ac00000000").unwrap();
502        let tx: Transaction = bitcoin::consensus::deserialize(&rawtx).unwrap();
503        let pools = default_data(Network::Bitcoin);
504
505        let pool = Pool {
506            id: 26,
507            addresses: vec!["1CjPR7Z5ZSyWk6WtXvSFgkptmpoi4UM9BC".to_string()],
508            tags: vec!["ghash.io".to_string()],
509            name: "GHash.IO".to_string(),
510            link: "https://ghash.io".to_string(),
511        };
512
513        let expected_address = Some(IdentificationResult {
514            pool,
515            identification_method: IdentificationMethod::Address,
516        });
517
518        assert_eq!(tx.identify_pool(Network::Bitcoin, &pools), expected_address);
519        assert_eq!(
520            tx.identify_coinbase_output_address(Network::Bitcoin, &pools),
521            expected_address
522        );
523        assert_eq!(tx.identify_coinbase_tag(&pools), None);
524    }
525
526    #[test]
527    fn test_coinbase_with_address_output_not_first() {
528        // Bitcoin mainnet coinbase transaction where the first output is an
529        // OP_RETURN output.
530        // 980fab41429b321b4722dcfb780d6f39f9f19065f1a96a5058689c312e0b16be
531        // Mined in Block 455860 by BitcoinRussia.
532        // 0000000000000000002f5721c2d63215a6a956a356d170339377ac24518e1df8
533        let rawtx = Vec::from_hex("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4c03b4f40604f4fbbb5808fabe6d6dc6c7031efbd1de4725926e45c2ba9443fd84234cfb4cfb606e7d873cbdbdb88001000000000000005fffff799b3703000d2f6e6f64655374726174756d2f00000000020000000000000000266a24aa21a9ed159d16a5ce680dbe165700ef4a5776fcbf4fe216dc886c895d5dd5e0bd923aa0f5c77751000000001976a9142573e708154145b6a6a4a8898a2e458e6828d10688ac00000000").unwrap();
534        let tx: Transaction = bitcoin::consensus::deserialize(&rawtx).unwrap();
535        let pools = default_data(Network::Bitcoin);
536
537        let pool = Pool {
538            id: 104,
539            addresses: vec![
540                "14R2r9FkyDmyxGB9xUVwVLdgsX9YfdVamk".to_string(),
541                "165GCEAx81wce33FWEnPCRhdjcXCrBJdKn".to_string(),
542            ],
543            tags: vec!["/Bitcoin-Russia.ru/".to_string()],
544            name: "BitcoinRussia".to_string(),
545            link: "https://bitcoin-russia.ru".to_string(),
546        };
547
548        let expected_address = Some(IdentificationResult {
549            pool,
550            identification_method: IdentificationMethod::Address,
551        });
552
553        assert_eq!(tx.identify_pool(Network::Bitcoin, &pools), expected_address);
554        assert_eq!(
555            tx.identify_coinbase_output_address(Network::Bitcoin, &pools),
556            expected_address
557        );
558        assert_eq!(tx.identify_coinbase_tag(&pools), None);
559    }
560
561    #[test]
562    fn test_coinbase_signet_miner3() {
563        // Bitcoin signet coinbase transaction of block 174359 by signet:3:
564        // 6e6bc8626a408b34ddfc1dfa9966e346a2a8ac71a76815abfed933ca53b1183b
565        // Identified by both it's coinbase tag.
566        let rawtx = Vec::from_hex("020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0f0317a9020a2f7369676e65743a332ffeffffff0200f2052a010000002251207099e4b23427fc40ba4777bbf52cfd0b7444d69a3e21ef281270723f54c0c14b0000000000000000776a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf94c4fecc7daa2490047304402205dfcbd8c81d0ed065ed0662e9b833424a8ae495afda62c101ad9ca656c15d56f02205bf4f0ea0956ff1529f7e93a3d003c9f3b5372913fc1b825afbc6ef9598a654a01000120000000000000000000000000000000000000000000000000000000000000000000000000").unwrap();
567        let tx: Transaction = bitcoin::consensus::deserialize(&rawtx).unwrap();
568        let pools = default_data(Network::Signet);
569
570        let pool = Pool {
571            id: 3,
572            addresses: vec![
573                "tb1pwzv7fv35yl7ypwj8w7al2t8apd6yf4568cs772qjwper74xqc99sk8x7tk".to_string(),
574            ],
575            tags: vec!["/signet:3/".to_string()],
576            name: "signet-3 (inquisition)".to_string(),
577            link: "https://github.com/bitcoin-inquisition/bitcoin/wiki".to_string(),
578        };
579
580        let expected_address = Some(IdentificationResult {
581            pool: pool.clone(),
582            identification_method: IdentificationMethod::Address,
583        });
584
585        let expected_tag = Some(IdentificationResult {
586            pool: pool.clone(),
587            identification_method: IdentificationMethod::Tag,
588        });
589
590        assert_eq!(tx.identify_pool(Network::Signet, &pools), expected_address);
591        assert_eq!(
592            tx.identify_coinbase_output_address(Network::Signet, &pools),
593            expected_address
594        );
595        assert_eq!(tx.identify_coinbase_tag(&pools), expected_tag);
596    }
597}