query.rs 10.8 KB
Newer Older
1
use bitcoin::blockdata::transaction::Transaction;
2
use bitcoin::consensus::encode::deserialize;
Roman Zeyde's avatar
Roman Zeyde committed
3 4
use bitcoin_hashes::hex::ToHex;
use bitcoin_hashes::sha256d::Hash as Sha256dHash;
5 6
use crypto::digest::Digest;
use crypto::sha2::Sha256;
Roman Zeyde's avatar
Roman Zeyde committed
7
use serde_json::Value;
8
use std::collections::HashMap;
9
use std::sync::{Arc, RwLock};
10

Roman Zeyde's avatar
Roman Zeyde committed
11
use crate::app::App;
12
use crate::cache::TransactionCache;
Roman Zeyde's avatar
Roman Zeyde committed
13 14 15
use crate::errors::*;
use crate::index::{compute_script_hash, TxInRow, TxOutRow, TxRow};
use crate::mempool::Tracker;
16
use crate::metrics::{HistogramOpts, HistogramVec, Metrics};
Roman Zeyde's avatar
Roman Zeyde committed
17 18
use crate::store::{ReadStore, Row};
use crate::util::{FullHash, HashPrefix, HeaderEntry};
Roman Zeyde's avatar
Roman Zeyde committed
19

20 21 22 23 24
pub struct FundingOutput {
    pub txn_id: Sha256dHash,
    pub height: u32,
    pub output_index: usize,
    pub value: u64,
Roman Zeyde's avatar
Roman Zeyde committed
25 26
}

27 28
type OutPoint = (Sha256dHash, usize); // (txid, output_index)

29 30
struct SpendingInput {
    txn_id: Sha256dHash,
31
    height: u32,
32
    funding_output: OutPoint,
33
    value: u64,
Roman Zeyde's avatar
Roman Zeyde committed
34 35 36
}

pub struct Status {
37 38 39 40
    confirmed: (Vec<FundingOutput>, Vec<SpendingInput>),
    mempool: (Vec<FundingOutput>, Vec<SpendingInput>),
}

41
impl Status {
42 43 44 45 46 47 48 49
    fn funding(&self) -> impl Iterator<Item = &FundingOutput> {
        self.confirmed.0.iter().chain(self.mempool.0.iter())
    }

    fn spending(&self) -> impl Iterator<Item = &SpendingInput> {
        self.confirmed.1.iter().chain(self.mempool.1.iter())
    }

50 51
    pub fn history(&self) -> Vec<(i32, Sha256dHash)> {
        let mut txns_map = HashMap::<Sha256dHash, i32>::new();
52
        for f in self.funding() {
53
            txns_map.insert(f.txn_id, f.height as i32);
54
        }
55
        for s in self.spending() {
56
            txns_map.insert(s.txn_id, s.height as i32);
57 58 59
        }
        let mut txns: Vec<(i32, Sha256dHash)> =
            txns_map.into_iter().map(|item| (item.1, item.0)).collect();
60
        txns.sort_unstable();
61 62 63 64 65 66 67 68 69 70 71
        txns
    }

    pub fn hash(&self) -> Option<FullHash> {
        let txns = self.history();
        if txns.is_empty() {
            None
        } else {
            let mut hash = FullHash::default();
            let mut sha2 = Sha256::new();
            for (height, txn_id) in txns {
Roman Zeyde's avatar
Roman Zeyde committed
72
                let part = format!("{}:{}:", txn_id.to_hex(), height);
73 74 75 76 77 78 79 80
                sha2.input(part.as_bytes());
            }
            sha2.result(&mut hash);
            Some(hash)
        }
    }
}

Roman Zeyde's avatar
Roman Zeyde committed
81 82
struct TxnHeight {
    txn: Transaction,
83
    height: u32,
Roman Zeyde's avatar
Roman Zeyde committed
84 85
}

86
// TODO: the functions below can be part of ReadStore.
87
fn txrow_by_txid(store: &dyn ReadStore, txid: &Sha256dHash) -> Option<TxRow> {
88 89 90 91 92
    let key = TxRow::filter_full(&txid);
    let value = store.get(&key)?;
    Some(TxRow::from_row(&Row { key, value }))
}

93
fn txrows_by_prefix(store: &dyn ReadStore, txid_prefix: HashPrefix) -> Vec<TxRow> {
94
    store
Roman Zeyde's avatar
Roman Zeyde committed
95
        .scan(&TxRow::filter_prefix(txid_prefix))
96 97 98 99
        .iter()
        .map(|row| TxRow::from_row(row))
        .collect()
}
100

101
fn txids_by_script_hash(store: &dyn ReadStore, script_hash: &[u8]) -> Vec<HashPrefix> {
102 103 104 105 106 107
    store
        .scan(&TxOutRow::filter(script_hash))
        .iter()
        .map(|row| TxOutRow::from_row(row).txid_prefix)
        .collect()
}
108

109
fn txids_by_funding_output(
110
    store: &dyn ReadStore,
111 112 113 114 115 116 117 118
    txn_id: &Sha256dHash,
    output_index: usize,
) -> Vec<HashPrefix> {
    store
        .scan(&TxInRow::filter(&txn_id, output_index))
        .iter()
        .map(|row| TxInRow::from_row(row).txid_prefix)
        .collect()
119 120
}

121 122
pub struct Query {
    app: Arc<App>,
123
    tracker: RwLock<Tracker>,
124
    tx_cache: TransactionCache,
125
    txid_limit: usize,
126
    duration: HistogramVec,
127 128
}

129
impl Query {
130 131 132 133 134 135
    pub fn new(
        app: Arc<App>,
        metrics: &Metrics,
        tx_cache: TransactionCache,
        txid_limit: usize,
    ) -> Arc<Query> {
136
        Arc::new(Query {
137
            app,
138
            tracker: RwLock::new(Tracker::new(metrics)),
139
            tx_cache,
140
            txid_limit,
141 142 143 144 145 146 147
            duration: metrics.histogram_vec(
                HistogramOpts::new(
                    "electrs_query_duration",
                    "Time to update mempool (in seconds)",
                ),
                &["type"],
            ),
148
        })
149 150
    }

151 152
    fn load_txns_by_prefix(
        &self,
153
        store: &dyn ReadStore,
154 155
        prefixes: Vec<HashPrefix>,
    ) -> Result<Vec<TxnHeight>> {
Roman Zeyde's avatar
Roman Zeyde committed
156
        let mut txns = vec![];
157
        for txid_prefix in prefixes {
Roman Zeyde's avatar
Roman Zeyde committed
158
            for tx_row in txrows_by_prefix(store, txid_prefix) {
159
                let txid: Sha256dHash = deserialize(&tx_row.key.txid).unwrap();
160
                let txn = self.load_txn(&txid, Some(tx_row.height))?;
161 162
                txns.push(TxnHeight {
                    txn,
163
                    height: tx_row.height,
164
                })
165 166
            }
        }
167
        Ok(txns)
168 169
    }

170 171
    fn find_spending_input(
        &self,
172
        store: &dyn ReadStore,
173
        funding: &FundingOutput,
174
    ) -> Result<Option<SpendingInput>> {
175
        let spending_txns: Vec<TxnHeight> = self.load_txns_by_prefix(
176
            store,
177
            txids_by_funding_output(store, &funding.txn_id, funding.output_index),
178
        )?;
Roman Zeyde's avatar
Roman Zeyde committed
179
        let mut spending_inputs = vec![];
Roman Zeyde's avatar
Roman Zeyde committed
180
        for t in &spending_txns {
181
            for input in t.txn.input.iter() {
182 183
                if input.previous_output.txid == funding.txn_id
                    && input.previous_output.vout == funding.output_index as u32
Roman Zeyde's avatar
Roman Zeyde committed
184 185 186 187
                {
                    spending_inputs.push(SpendingInput {
                        txn_id: t.txn.txid(),
                        height: t.height,
188
                        funding_output: (funding.txn_id, funding.output_index),
189
                        value: funding.value,
Roman Zeyde's avatar
Roman Zeyde committed
190 191 192 193 194
                    })
                }
            }
        }
        assert!(spending_inputs.len() <= 1);
195
        Ok(if spending_inputs.len() == 1 {
Roman Zeyde's avatar
Roman Zeyde committed
196
            Some(spending_inputs.remove(0))
197 198
        } else {
            None
199
        })
200 201
    }

202
    fn find_funding_outputs(&self, t: &TxnHeight, script_hash: &[u8]) -> Vec<FundingOutput> {
Roman Zeyde's avatar
Roman Zeyde committed
203
        let mut result = vec![];
204
        let txn_id = t.txn.txid();
Roman Zeyde's avatar
Roman Zeyde committed
205
        for (index, output) in t.txn.output.iter().enumerate() {
206 207
            if compute_script_hash(&output.script_pubkey[..]) == script_hash {
                result.push(FundingOutput {
Roman Zeyde's avatar
Roman Zeyde committed
208
                    txn_id,
209 210 211 212 213 214 215 216 217
                    height: t.height,
                    output_index: index,
                    value: output.value,
                })
            }
        }
        result
    }

218 219 220 221
    fn confirmed_status(
        &self,
        script_hash: &[u8],
    ) -> Result<(Vec<FundingOutput>, Vec<SpendingInput>)> {
222 223
        let mut funding = vec![];
        let mut spending = vec![];
224 225
        let read_store = self.app.read_store();
        let txid_prefixes = txids_by_script_hash(read_store, script_hash);
226
        // if the limit is enabled
Roman Zeyde's avatar
Roman Zeyde committed
227 228 229 230 231
        if self.txid_limit > 0 && txid_prefixes.len() > self.txid_limit {
            bail!(
                "{}+ transactions found, query may take a long time",
                txid_prefixes.len()
            );
232
        }
233
        for t in self.load_txns_by_prefix(read_store, txid_prefixes)? {
234
            funding.extend(self.find_funding_outputs(&t, script_hash));
235
        }
236
        for funding_output in &funding {
237
            if let Some(spent) = self.find_spending_input(read_store, &funding_output)? {
238
                spending.push(spent);
239 240
            }
        }
241
        Ok((funding, spending))
242 243
    }

244 245 246 247
    fn mempool_status(
        &self,
        script_hash: &[u8],
        confirmed_funding: &[FundingOutput],
248
    ) -> Result<(Vec<FundingOutput>, Vec<SpendingInput>)> {
249 250
        let mut funding = vec![];
        let mut spending = vec![];
251
        let tracker = self.tracker.read().unwrap();
252 253
        let txid_prefixes = txids_by_script_hash(tracker.index(), script_hash);
        for t in self.load_txns_by_prefix(tracker.index(), txid_prefixes)? {
254
            funding.extend(self.find_funding_outputs(&t, script_hash));
255
        }
256
        // // TODO: dedup outputs (somehow) both confirmed and in mempool (e.g. reorg?)
257
        for funding_output in funding.iter().chain(confirmed_funding.iter()) {
258
            if let Some(spent) = self.find_spending_input(tracker.index(), &funding_output)? {
259
                spending.push(spent);
Roman Zeyde's avatar
Roman Zeyde committed
260 261
            }
        }
262
        Ok((funding, spending))
263
    }
264

265
    pub fn status(&self, script_hash: &[u8]) -> Result<Status> {
266 267 268 269
        let timer = self
            .duration
            .with_label_values(&["confirmed_status"])
            .start_timer();
Roman Zeyde's avatar
Roman Zeyde committed
270 271
        let confirmed = self
            .confirmed_status(script_hash)
272
            .chain_err(|| "failed to get confirmed status")?;
273 274 275 276 277 278
        timer.observe_duration();

        let timer = self
            .duration
            .with_label_values(&["mempool_status"])
            .start_timer();
Roman Zeyde's avatar
Roman Zeyde committed
279 280
        let mempool = self
            .mempool_status(script_hash, &confirmed.0)
281
            .chain_err(|| "failed to get mempool status")?;
282 283
        timer.observe_duration();

284
        Ok(Status { confirmed, mempool })
285 286
    }

287
    fn lookup_confirmed_blockhash(
288 289 290
        &self,
        tx_hash: &Sha256dHash,
        block_height: Option<u32>,
291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
    ) -> Result<Option<Sha256dHash>> {
        let blockhash = if self.tracker.read().unwrap().get_txn(&tx_hash).is_some() {
            None // found in mempool (as unconfirmed transaction)
        } else {
            // Lookup in confirmed transactions' index
            let height = match block_height {
                Some(height) => height,
                None => {
                    txrow_by_txid(self.app.read_store(), &tx_hash)
                        .chain_err(|| format!("not indexed tx {}", tx_hash))?
                        .height
                }
            };
            let header = self
                .app
                .index()
                .get_header(height as usize)
                .chain_err(|| format!("missing header at height {}", height))?;
            Some(*header.hash())
310
        };
311 312 313 314
        Ok(blockhash)
    }

    // Internal API for transaction retrieval
315
    fn load_txn(&self, txid: &Sha256dHash, block_height: Option<u32>) -> Result<Transaction> {
Roman Zeyde's avatar
Roman Zeyde committed
316
        let _timer = self.duration.with_label_values(&["load_txn"]).start_timer();
317 318
        self.tx_cache.get_or_else(&txid, || {
            let blockhash = self.lookup_confirmed_blockhash(txid, block_height)?;
319 320 321 322 323 324
            let value: Value = self
                .app
                .daemon()
                .gettransaction_raw(txid, blockhash, /*verbose*/ false)?;
            let value_hex: &str = value.as_str().chain_err(|| "non-string tx")?;
            hex::decode(&value_hex).chain_err(|| "non-hex tx")
325
        })
326 327
    }

328
    pub fn get_best_header(&self) -> Result<HeaderEntry> {
329
        let last_header = self.app.index().best_header();
330
        Ok(last_header.chain_err(|| "no headers indexed")?.clone())
Roman Zeyde's avatar
Roman Zeyde committed
331
    }
Roman Zeyde's avatar
Roman Zeyde committed
332

Roman Zeyde's avatar
Roman Zeyde committed
333
    pub fn update_mempool(&self) -> Result<()> {
334 335 336 337
        let _timer = self
            .duration
            .with_label_values(&["update_mempool"])
            .start_timer();
Roman Zeyde's avatar
Roman Zeyde committed
338
        self.tracker.write().unwrap().update(self.app.daemon())
339
    }
340
}