Skip to content

Commit 0e82155

Browse files
committed
Implement multi-transaction TxRaw lookup using MultiGet
- Change lookup_txns to use MultiGet - Use lookup_txns for block transactions and reconstruction too (GET /block/:hash/txs and GET /block/:hash/raw) (This was already possible with the V1 schema, but related to and builds upon the other V2 changes.) Plus some related changes: - Remove expensive sanity check assertion in lookup_txn (involved txid computation and wasn't really necessary) - Add test for raw block reconstruction
1 parent 678e97e commit 0e82155

File tree

3 files changed

+98
-66
lines changed

3 files changed

+98
-66
lines changed

src/new_index/schema.rs

Lines changed: 70 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,28 @@ impl ChainQuery {
494494
}
495495
}
496496

497+
pub fn get_block_txs(
498+
&self,
499+
hash: &BlockHash,
500+
start_index: usize,
501+
limit: usize,
502+
) -> Result<Vec<Transaction>> {
503+
let txids = self.get_block_txids(hash).chain_err(|| "block not found")?;
504+
ensure!(start_index < txids.len(), "start index out of range");
505+
506+
let txids_with_blockhash = txids
507+
.into_iter()
508+
.skip(start_index)
509+
.take(limit)
510+
.map(|txid| (txid, *hash))
511+
.collect::<Vec<_>>();
512+
513+
self.lookup_txns(&txids_with_blockhash)
514+
515+
// XXX use getblock in lightmode? a single RPC call, but would fetch all txs to get one page
516+
// self.daemon.getblock(hash)?.txdata.into_iter().skip(start_index).take(limit).collect()
517+
}
518+
497519
pub fn get_block_meta(&self, hash: &BlockHash) -> Option<BlockMeta> {
498520
let _timer = self.start_timer("get_block_meta");
499521

@@ -519,17 +541,19 @@ impl ChainQuery {
519541
let entry = self.header_by_hash(hash)?;
520542
let meta = self.get_block_meta(hash)?;
521543
let txids = self.get_block_txids(hash)?;
544+
let txids_with_blockhash: Vec<_> =
545+
txids.into_iter().map(|txid| (txid, *hash)).collect();
546+
let raw_txs = self.lookup_raw_txns(&txids_with_blockhash).ok()?; // TODO avoid hiding all errors as None, return a Result
522547

523548
// Reconstruct the raw block using the header and txids,
524549
// as <raw header><tx count varint><raw txs>
525550
let mut raw = Vec::with_capacity(meta.size as usize);
526551

527552
raw.append(&mut serialize(entry.header()));
528-
raw.append(&mut serialize(&VarInt(txids.len() as u64)));
553+
raw.append(&mut serialize(&VarInt(raw_txs.len() as u64)));
529554

530-
for txid in txids {
531-
// we don't need to provide the blockhash because we know we're not in light mode
532-
raw.append(&mut self.lookup_raw_txn(&txid, None)?);
555+
for mut raw_tx in raw_txs {
556+
raw.append(&mut raw_tx);
533557
}
534558

535559
Some(raw)
@@ -589,7 +613,7 @@ impl ChainQuery {
589613
) -> Vec<(Transaction, BlockId)> {
590614
let _timer_scan = self.start_timer("history");
591615
let headers = self.store.indexed_headers.read().unwrap();
592-
let txs_conf = self
616+
let history_iter = self
593617
.history_iter_scan_reverse(code, hash)
594618
.map(TxHistoryRow::from_row)
595619
.map(|row| (row.get_txid(), row.key.confirmed_height as usize))
@@ -605,16 +629,22 @@ impl ChainQuery {
605629
None => 0,
606630
})
607631
// skip over entries that point to non-existing heights (may happen during reorg handling)
608-
.filter_map(|(txid, height)| Some((txid, headers.header_by_height(height)?.into())))
609-
.take(limit)
610-
.collect::<Vec<(Txid, BlockId)>>();
632+
.filter_map(|(txid, height)| Some((txid, headers.header_by_height(height)?)))
633+
.take(limit);
634+
635+
let mut txids_with_blockhash = Vec::with_capacity(limit);
636+
let mut blockids = Vec::with_capacity(limit);
637+
for (txid, header) in history_iter {
638+
txids_with_blockhash.push((txid, *header.hash()));
639+
blockids.push(BlockId::from(header));
640+
}
611641
drop(headers);
612642

613-
self.lookup_txns(&txs_conf)
643+
self.lookup_txns(&txids_with_blockhash)
614644
.expect("failed looking up txs in history index")
615645
.into_iter()
616-
.zip(txs_conf)
617-
.map(|(tx, (_, blockid))| (tx, blockid))
646+
.zip(blockids)
647+
.map(|(tx, blockid)| (tx, blockid))
618648
.collect()
619649
}
620650

@@ -926,26 +956,40 @@ impl ChainQuery {
926956
.clone()
927957
}
928958

929-
// TODO: can we pass txids as a "generic iterable"?
930-
// TODO: should also use a custom ThreadPoolBuilder?
931-
pub fn lookup_txns(&self, txids: &[(Txid, BlockId)]) -> Result<Vec<Transaction>> {
959+
pub fn lookup_txns(&self, txids: &[(Txid, BlockHash)]) -> Result<Vec<Transaction>> {
932960
let _timer = self.start_timer("lookup_txns");
933-
txids
934-
.par_iter()
935-
.map(|(txid, blockid)| {
936-
self.lookup_txn(txid, Some(&blockid.hash))
937-
.chain_err(|| "missing tx")
938-
})
939-
.collect::<Result<Vec<Transaction>>>()
961+
Ok(self
962+
.lookup_raw_txns(txids)?
963+
.into_iter()
964+
.map(|rawtx| deserialize(&rawtx).expect("failed to parse Transaction"))
965+
.collect())
940966
}
941967

942968
pub fn lookup_txn(&self, txid: &Txid, blockhash: Option<&BlockHash>) -> Option<Transaction> {
943969
let _timer = self.start_timer("lookup_txn");
944-
self.lookup_raw_txn(txid, blockhash).map(|rawtx| {
945-
let txn: Transaction = deserialize(&rawtx).expect("failed to parse Transaction");
946-
assert_eq!(*txid, txn.compute_txid());
947-
txn
948-
})
970+
let rawtx = self.lookup_raw_txn(txid, blockhash)?;
971+
Some(deserialize(&rawtx).expect("failed to parse Transaction"))
972+
}
973+
974+
pub fn lookup_raw_txns(&self, txids: &[(Txid, BlockHash)]) -> Result<Vec<Bytes>> {
975+
let _timer = self.start_timer("lookup_raw_txns");
976+
if self.light_mode {
977+
txids
978+
.par_iter()
979+
.map(|(txid, blockhash)| {
980+
self.lookup_raw_txn(txid, Some(blockhash))
981+
.chain_err(|| "missing tx")
982+
})
983+
.collect()
984+
} else {
985+
let keys = txids.iter().map(|(txid, _)| TxRow::key(&txid[..]));
986+
self.store
987+
.txstore_db
988+
.multi_get(keys)
989+
.into_iter()
990+
.map(|val| val.unwrap().chain_err(|| "missing tx"))
991+
.collect()
992+
}
949993
}
950994

951995
pub fn lookup_raw_txn(&self, txid: &Txid, blockhash: Option<&BlockHash>) -> Option<Bytes> {

src/rest.rs

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -718,41 +718,28 @@ fn handle_request(
718718
}
719719
(&Method::GET, Some(&"block"), Some(hash), Some(&"txs"), start_index, None) => {
720720
let hash = BlockHash::from_str(hash)?;
721-
let txids = query
722-
.chain()
723-
.get_block_txids(&hash)
724-
.ok_or_else(|| HttpError::not_found("Block not found".to_string()))?;
725-
726721
let start_index = start_index
727722
.map_or(0u32, |el| el.parse().unwrap_or(0))
728723
.max(0u32) as usize;
729-
if start_index >= txids.len() {
730-
bail!(HttpError::not_found("start index out of range".to_string()));
731-
} else if start_index % CHAIN_TXS_PER_PAGE != 0 {
732-
bail!(HttpError::from(format!(
733-
"start index must be a multipication of {}",
734-
CHAIN_TXS_PER_PAGE
735-
)));
736-
}
737724

738-
// blockid_by_hash() only returns the BlockId for non-orphaned blocks,
739-
// or None for orphaned
740-
let confirmed_blockid = query.chain().blockid_by_hash(&hash);
725+
ensure!(
726+
start_index % CHAIN_TXS_PER_PAGE == 0,
727+
"start index must be a multipication of {}",
728+
CHAIN_TXS_PER_PAGE
729+
);
730+
731+
// The BlockId would not be available for stale blocks
732+
let blockid = query.chain().blockid_by_hash(&hash);
741733

742-
let txs = txids
743-
.iter()
744-
.skip(start_index)
745-
.take(CHAIN_TXS_PER_PAGE)
746-
.map(|txid| {
747-
query
748-
.lookup_txn(&txid)
749-
.map(|tx| (tx, confirmed_blockid.clone()))
750-
.ok_or_else(|| "missing tx".to_string())
751-
})
752-
.collect::<Result<Vec<(Transaction, Option<BlockId>)>, _>>()?;
734+
let txs = query
735+
.chain()
736+
.get_block_txs(&hash, start_index, CHAIN_TXS_PER_PAGE)?
737+
.into_iter()
738+
.map(|tx| (tx, blockid))
739+
.collect();
753740

754-
// XXX orphraned blocks alway get TTL_SHORT
755-
let ttl = ttl_by_depth(confirmed_blockid.map(|b| b.height), query);
741+
// XXX stale blocks alway get TTL_SHORT
742+
let ttl = ttl_by_depth(blockid.map(|b| b.height), query);
756743

757744
json_response(prepare_txs(txs, query, config), ttl)
758745
}

tests/rest.rs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use bitcoin::hex::FromHex;
12
use bitcoind::bitcoincore_rpc::RpcApi;
23
use serde_json::Value;
34
use std::collections::HashSet;
@@ -15,17 +16,9 @@ use common::Result;
1516
fn test_rest() -> Result<()> {
1617
let (rest_handle, rest_addr, mut tester) = common::init_rest_tester().unwrap();
1718

18-
let get_json = |path: &str| -> Result<Value> {
19-
Ok(ureq::get(&format!("http://{}{}", rest_addr, path))
20-
.call()?
21-
.into_json::<Value>()?)
22-
};
23-
24-
let get_plain = |path: &str| -> Result<String> {
25-
Ok(ureq::get(&format!("http://{}{}", rest_addr, path))
26-
.call()?
27-
.into_string()?)
28-
};
19+
let get = |path: &str| ureq::get(&format!("http://{}{}", rest_addr, path)).call();
20+
let get_json = |path: &str| -> Result<Value> { Ok(get(path)?.into_json::<Value>()?) };
21+
let get_plain = |path: &str| -> Result<String> { Ok(get(path)?.into_string()?) };
2922

3023
// Send transaction and confirm it
3124
let addr1 = tester.newaddress()?;
@@ -141,6 +134,14 @@ fn test_rest() -> Result<()> {
141134
);
142135
assert_eq!(res["tx_count"].as_u64(), Some(2));
143136

137+
// Test GET /block/:hash/raw
138+
let mut res = get(&format!("/block/{}/raw", blockhash))?.into_reader();
139+
let mut rest_rawblock = Vec::new();
140+
res.read_to_end(&mut rest_rawblock).unwrap();
141+
let node_hexblock = // uses low-level call() to support Elements
142+
tester.call::<String>("getblock", &[blockhash.to_string().into(), 0.into()])?;
143+
assert_eq!(rest_rawblock, Vec::from_hex(&node_hexblock).unwrap());
144+
144145
// Test GET /block/:hash/txs
145146
let res = get_json(&format!("/block/{}/txs", blockhash))?;
146147
let block_txs = res.as_array().expect("list of txs");

0 commit comments

Comments
 (0)