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}