Skip to content

Commit cb2b909

Browse files
authored
Block input bitmap rework (#3236)
* first pass at rewind_single_block and reworking rewind to simply iterate over blocks, rewinding each incrementally * commit * commit * cleanup * add test coverage for output_pos index transactional semantics during rewind * commit * do not store commitments in spent_index just use the order of the inputs in the block * compare key with commitment when cleaning output_pos index * remove unused OutputPos struct
1 parent ef853ae commit cb2b909

File tree

7 files changed

+384
-189
lines changed

7 files changed

+384
-189
lines changed

api/src/handlers/utils.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ pub fn get_output(
5656
match res {
5757
Ok(output_pos) => {
5858
return Ok((
59-
Output::new(&commit, output_pos.height, output_pos.position),
59+
Output::new(&commit, output_pos.height, output_pos.pos),
6060
x.clone(),
6161
));
6262
}
@@ -100,7 +100,7 @@ pub fn get_output_v2(
100100
for x in outputs.iter() {
101101
let res = chain.is_unspent(x);
102102
match res {
103-
Ok(output_pos) => match chain.get_unspent_output_at(output_pos.position) {
103+
Ok(output_pos) => match chain.get_unspent_output_at(output_pos.pos) {
104104
Ok(output) => {
105105
let header = if include_merkle_proof && output.is_coinbase() {
106106
chain.get_header_by_height(output_pos.height).ok()

chain/src/chain.rs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ use crate::store;
3030
use crate::txhashset;
3131
use crate::txhashset::{PMMRHandle, TxHashSet};
3232
use crate::types::{
33-
BlockStatus, ChainAdapter, NoStatus, Options, OutputMMRPosition, Tip, TxHashsetWriteStatus,
33+
BlockStatus, ChainAdapter, CommitPos, NoStatus, Options, Tip, TxHashsetWriteStatus,
3434
};
3535
use crate::util::secp::pedersen::{Commitment, RangeProof};
3636
use crate::util::RwLock;
@@ -199,6 +199,15 @@ impl Chain {
199199
&mut txhashset,
200200
)?;
201201

202+
// Initialize the output_pos index based on UTXO set.
203+
// This is fast as we only look for stale and missing entries
204+
// and do not need to rebuild the entire index.
205+
{
206+
let batch = store.batch()?;
207+
txhashset.init_output_pos_index(&header_pmmr, &batch)?;
208+
batch.commit()?;
209+
}
210+
202211
let chain = Chain {
203212
db_root,
204213
store,
@@ -495,9 +504,8 @@ impl Chain {
495504
/// spent. This querying is done in a way that is consistent with the
496505
/// current chain state, specifically the current winning (valid, most
497506
/// work) fork.
498-
pub fn is_unspent(&self, output_ref: &OutputIdentifier) -> Result<OutputMMRPosition, Error> {
499-
let txhashset = self.txhashset.read();
500-
txhashset.is_unspent(output_ref)
507+
pub fn is_unspent(&self, output_ref: &OutputIdentifier) -> Result<CommitPos, Error> {
508+
self.txhashset.read().is_unspent(output_ref)
501509
}
502510

503511
/// Retrieves an unspent output using its PMMR position
@@ -973,7 +981,7 @@ impl Chain {
973981
}
974982

975983
// Rebuild our output_pos index in the db based on fresh UTXO set.
976-
txhashset.init_output_pos_index(&header_pmmr, &mut batch)?;
984+
txhashset.init_output_pos_index(&header_pmmr, &batch)?;
977985

978986
// Commit all the changes to the db.
979987
batch.commit()?;
@@ -1015,7 +1023,7 @@ impl Chain {
10151023
fn remove_historical_blocks(
10161024
&self,
10171025
header_pmmr: &txhashset::PMMRHandle<BlockHeader>,
1018-
batch: &mut store::Batch<'_>,
1026+
batch: &store::Batch<'_>,
10191027
) -> Result<(), Error> {
10201028
if self.archive_mode {
10211029
return Ok(());
@@ -1089,7 +1097,7 @@ impl Chain {
10891097
// Take a write lock on the txhashet and start a new writeable db batch.
10901098
let header_pmmr = self.header_pmmr.read();
10911099
let mut txhashset = self.txhashset.write();
1092-
let mut batch = self.store.batch()?;
1100+
let batch = self.store.batch()?;
10931101

10941102
// Compact the txhashset itself (rewriting the pruned backend files).
10951103
{
@@ -1100,14 +1108,17 @@ impl Chain {
11001108
let horizon_hash = header_pmmr.get_header_hash_by_height(horizon_height)?;
11011109
let horizon_header = batch.get_block_header(&horizon_hash)?;
11021110

1103-
txhashset.compact(&horizon_header, &mut batch)?;
1111+
txhashset.compact(&horizon_header, &batch)?;
11041112
}
11051113

11061114
// If we are not in archival mode remove historical blocks from the db.
11071115
if !self.archive_mode {
1108-
self.remove_historical_blocks(&header_pmmr, &mut batch)?;
1116+
self.remove_historical_blocks(&header_pmmr, &batch)?;
11091117
}
11101118

1119+
// Make sure our output_pos index is consistent with the UTXO set.
1120+
txhashset.init_output_pos_index(&header_pmmr, &batch)?;
1121+
11111122
// Commit all the above db changes.
11121123
batch.commit()?;
11131124

@@ -1510,6 +1521,7 @@ fn setup_head(
15101521
// We will update this later once we have the correct header_root.
15111522
batch.save_block_header(&genesis.header)?;
15121523
batch.save_block(&genesis)?;
1524+
batch.save_spent_index(&genesis.hash(), &vec![])?;
15131525
batch.save_body_head(&Tip::from_header(&genesis.header))?;
15141526

15151527
if !genesis.kernels().is_empty() {

chain/src/pipe.rs

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use crate::core::pow;
2323
use crate::error::{Error, ErrorKind};
2424
use crate::store;
2525
use crate::txhashset;
26-
use crate::types::{Options, Tip};
26+
use crate::types::{CommitPos, Options, Tip};
2727
use crate::util::RwLock;
2828
use grin_store;
2929
use std::sync::Arc;
@@ -121,7 +121,7 @@ pub fn process_block(b: &Block, ctx: &mut BlockContext<'_>) -> Result<Option<Tip
121121
let ref mut header_pmmr = &mut ctx.header_pmmr;
122122
let ref mut txhashset = &mut ctx.txhashset;
123123
let ref mut batch = &mut ctx.batch;
124-
let block_sums = txhashset::extending(header_pmmr, txhashset, batch, |ext, batch| {
124+
let (block_sums, spent) = txhashset::extending(header_pmmr, txhashset, batch, |ext, batch| {
125125
rewind_and_apply_fork(&prev, ext, batch)?;
126126

127127
// Check any coinbase being spent have matured sufficiently.
@@ -143,22 +143,24 @@ pub fn process_block(b: &Block, ctx: &mut BlockContext<'_>) -> Result<Option<Tip
143143
// Apply the block to the txhashset state.
144144
// Validate the txhashset roots and sizes against the block header.
145145
// Block is invalid if there are any discrepencies.
146-
apply_block_to_txhashset(b, ext, batch)?;
146+
let spent = apply_block_to_txhashset(b, ext, batch)?;
147147

148148
// If applying this block does not increase the work on the chain then
149149
// we know we have not yet updated the chain to produce a new chain head.
150+
// We discard the "child" batch used in this extension (original ctx batch still active).
151+
// We discard any MMR modifications applied in this extension.
150152
let head = batch.head()?;
151153
if !has_more_work(&b.header, &head) {
152154
ext.extension.force_rollback();
153155
}
154156

155-
Ok(block_sums)
157+
Ok((block_sums, spent))
156158
})?;
157159

158160
// Add the validated block to the db along with the corresponding block_sums.
159161
// We do this even if we have not increased the total cumulative work
160162
// so we can maintain multiple (in progress) forks.
161-
add_block(b, &block_sums, &ctx.batch)?;
163+
add_block(b, &block_sums, &spent, &ctx.batch)?;
162164

163165
// If we have no "tail" then set it now.
164166
if ctx.batch.tail().is_err() {
@@ -429,20 +431,25 @@ fn apply_block_to_txhashset(
429431
block: &Block,
430432
ext: &mut txhashset::ExtensionPair<'_>,
431433
batch: &store::Batch<'_>,
432-
) -> Result<(), Error> {
433-
ext.extension.apply_block(block, batch)?;
434+
) -> Result<Vec<CommitPos>, Error> {
435+
let spent = ext.extension.apply_block(block, batch)?;
434436
ext.extension.validate_roots(&block.header)?;
435437
ext.extension.validate_sizes(&block.header)?;
436-
Ok(())
438+
Ok(spent)
437439
}
438440

439-
/// Officially adds the block to our chain.
441+
/// Officially adds the block to our chain (possibly on a losing fork).
442+
/// Adds the associated block_sums and spent_index as well.
440443
/// Header must be added separately (assume this has been done previously).
441-
fn add_block(b: &Block, block_sums: &BlockSums, batch: &store::Batch<'_>) -> Result<(), Error> {
442-
batch
443-
.save_block(b)
444-
.map_err(|e| ErrorKind::StoreErr(e, "pipe save block".to_owned()))?;
444+
fn add_block(
445+
b: &Block,
446+
block_sums: &BlockSums,
447+
spent: &Vec<CommitPos>,
448+
batch: &store::Batch<'_>,
449+
) -> Result<(), Error> {
450+
batch.save_block(b)?;
445451
batch.save_block_sums(&b.hash(), block_sums)?;
452+
batch.save_spent_index(&b.hash(), spent)?;
446453
Ok(())
447454
}
448455

chain/src/store.rs

Lines changed: 57 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@ use crate::core::core::hash::{Hash, Hashed};
1919
use crate::core::core::{Block, BlockHeader, BlockSums};
2020
use crate::core::pow::Difficulty;
2121
use crate::core::ser::ProtocolVersion;
22-
use crate::types::Tip;
22+
use crate::types::{CommitPos, Tip};
2323
use crate::util::secp::pedersen::Commitment;
2424
use croaring::Bitmap;
2525
use grin_store as store;
2626
use grin_store::{option_to_not_found, to_key, Error, SerIterator};
27+
use std::convert::TryInto;
2728
use std::sync::Arc;
2829

2930
const STORE_SUBPATH: &str = "chain";
@@ -35,6 +36,7 @@ const TAIL_PREFIX: u8 = b'T';
3536
const OUTPUT_POS_PREFIX: u8 = b'p';
3637
const BLOCK_INPUT_BITMAP_PREFIX: u8 = b'B';
3738
const BLOCK_SUMS_PREFIX: u8 = b'M';
39+
const BLOCK_SPENT_PREFIX: u8 = b'S';
3840

3941
/// All chain-related database operations
4042
pub struct ChainStore {
@@ -178,16 +180,19 @@ impl<'a> Batch<'a> {
178180
self.db.exists(&to_key(BLOCK_PREFIX, &mut h.to_vec()))
179181
}
180182

181-
/// Save the block and the associated input bitmap.
183+
/// Save the block to the db.
182184
/// Note: the block header is not saved to the db here, assumes this has already been done.
183185
pub fn save_block(&self, b: &Block) -> Result<(), Error> {
184-
// Build the "input bitmap" for this new block and store it in the db.
185-
self.build_and_store_block_input_bitmap(&b)?;
186-
187-
// Save the block itself to the db.
188186
self.db
189187
.put_ser(&to_key(BLOCK_PREFIX, &mut b.hash().to_vec())[..], b)?;
188+
Ok(())
189+
}
190190

191+
/// We maintain a "spent" index for each full block to allow the output_pos
192+
/// to be easily reverted during rewind.
193+
pub fn save_spent_index(&self, h: &Hash, spent: &Vec<CommitPos>) -> Result<(), Error> {
194+
self.db
195+
.put_ser(&to_key(BLOCK_SPENT_PREFIX, &mut h.to_vec())[..], spent)?;
191196
Ok(())
192197
}
193198

@@ -217,7 +222,7 @@ impl<'a> Batch<'a> {
217222
// Not an error if these fail.
218223
{
219224
let _ = self.delete_block_sums(bh);
220-
let _ = self.delete_block_input_bitmap(bh);
225+
let _ = self.delete_spent_index(bh);
221226
}
222227

223228
Ok(())
@@ -247,6 +252,20 @@ impl<'a> Batch<'a> {
247252
)
248253
}
249254

255+
/// Delete the output_pos index entry for a spent output.
256+
pub fn delete_output_pos_height(&self, commit: &Commitment) -> Result<(), Error> {
257+
self.db
258+
.delete(&to_key(OUTPUT_POS_PREFIX, &mut commit.as_ref().to_vec()))
259+
}
260+
261+
/// When using the output_pos iterator we have access to the index keys but not the
262+
/// original commitment that the key is constructed from. So we need a way of comparing
263+
/// a key with another commitment without reconstructing the commitment from the key bytes.
264+
pub fn is_match_output_pos_key(&self, key: &[u8], commit: &Commitment) -> bool {
265+
let commit_key = to_key(OUTPUT_POS_PREFIX, &mut commit.as_ref().to_vec());
266+
commit_key == key
267+
}
268+
250269
/// Iterator over the output_pos index.
251270
pub fn output_pos_iter(&self) -> Result<SerIterator<(u64, u64)>, Error> {
252271
let key = to_key(OUTPUT_POS_PREFIX, &mut "".to_string().into_bytes());
@@ -281,18 +300,15 @@ impl<'a> Batch<'a> {
281300
)
282301
}
283302

284-
/// Save the input bitmap for the block.
285-
fn save_block_input_bitmap(&self, bh: &Hash, bm: &Bitmap) -> Result<(), Error> {
286-
self.db.put(
287-
&to_key(BLOCK_INPUT_BITMAP_PREFIX, &mut bh.to_vec())[..],
288-
&bm.serialize(),
289-
)
290-
}
303+
/// Delete the block spent index.
304+
fn delete_spent_index(&self, bh: &Hash) -> Result<(), Error> {
305+
// Clean up the legacy input bitmap as well.
306+
let _ = self
307+
.db
308+
.delete(&to_key(BLOCK_INPUT_BITMAP_PREFIX, &mut bh.to_vec()));
291309

292-
/// Delete the block input bitmap.
293-
fn delete_block_input_bitmap(&self, bh: &Hash) -> Result<(), Error> {
294310
self.db
295-
.delete(&to_key(BLOCK_INPUT_BITMAP_PREFIX, &mut bh.to_vec()))
311+
.delete(&to_key(BLOCK_SPENT_PREFIX, &mut bh.to_vec()))
296312
}
297313

298314
/// Save block_sums for the block.
@@ -314,47 +330,41 @@ impl<'a> Batch<'a> {
314330
self.db.delete(&to_key(BLOCK_SUMS_PREFIX, &mut bh.to_vec()))
315331
}
316332

317-
/// Build the input bitmap for the given block.
318-
fn build_block_input_bitmap(&self, block: &Block) -> Result<Bitmap, Error> {
319-
let bitmap = block
320-
.inputs()
321-
.iter()
322-
.filter_map(|x| self.get_output_pos(&x.commitment()).ok())
323-
.map(|x| x as u32)
324-
.collect();
325-
Ok(bitmap)
326-
}
327-
328-
/// Build and store the input bitmap for the given block.
329-
fn build_and_store_block_input_bitmap(&self, block: &Block) -> Result<Bitmap, Error> {
330-
// Build the bitmap.
331-
let bitmap = self.build_block_input_bitmap(block)?;
332-
333-
// Save the bitmap to the db (via the batch).
334-
self.save_block_input_bitmap(&block.hash(), &bitmap)?;
335-
336-
Ok(bitmap)
333+
/// Get the block input bitmap based on our spent index.
334+
/// Fallback to legacy block input bitmap from the db.
335+
pub fn get_block_input_bitmap(&self, bh: &Hash) -> Result<Bitmap, Error> {
336+
if let Ok(spent) = self.get_spent_index(bh) {
337+
let bitmap = spent
338+
.into_iter()
339+
.map(|x| x.pos.try_into().unwrap())
340+
.collect();
341+
Ok(bitmap)
342+
} else {
343+
self.get_legacy_input_bitmap(bh)
344+
}
337345
}
338346

339-
/// Get the block input bitmap from the db or build the bitmap from
340-
/// the full block from the db (if the block is found).
341-
pub fn get_block_input_bitmap(&self, bh: &Hash) -> Result<Bitmap, Error> {
347+
fn get_legacy_input_bitmap(&self, bh: &Hash) -> Result<Bitmap, Error> {
342348
if let Ok(Some(bytes)) = self
343349
.db
344350
.get(&to_key(BLOCK_INPUT_BITMAP_PREFIX, &mut bh.to_vec()))
345351
{
346352
Ok(Bitmap::deserialize(&bytes))
347353
} else {
348-
match self.get_block(bh) {
349-
Ok(block) => {
350-
let bitmap = self.build_and_store_block_input_bitmap(&block)?;
351-
Ok(bitmap)
352-
}
353-
Err(e) => Err(e),
354-
}
354+
Err(Error::NotFoundErr("legacy block input bitmap".to_string()).into())
355355
}
356356
}
357357

358+
/// Get the "spent index" from the db for the specified block.
359+
/// If we need to rewind a block then we use this to "unspend" the spent outputs.
360+
pub fn get_spent_index(&self, bh: &Hash) -> Result<Vec<CommitPos>, Error> {
361+
option_to_not_found(
362+
self.db
363+
.get_ser(&to_key(BLOCK_SPENT_PREFIX, &mut bh.to_vec())),
364+
|| format!("spent index: {}", bh),
365+
)
366+
}
367+
358368
/// Commits this batch. If it's a child batch, it will be merged with the
359369
/// parent, otherwise the batch is written to db.
360370
pub fn commit(self) -> Result<(), Error> {

0 commit comments

Comments
 (0)