Skip to content

Commit

Permalink
Enable faster sync (#3108)
Browse files Browse the repository at this point in the history
* add bitmap accumulator
refactor vec backend so we can use it outside of tests
introduce a "hash only" vec backend for the accumulator

* get core tests passing

* initial test coverage for bitmap_accumulator

* better test coverage for bitmap accumulator and cleanup code

* refactor txhashset roots, call validate() on roots during block validation

* fix store tests

* log the "merged" root when validating roots

* cleanup, revise based on feedback

* cleanup

* rework it to pass explicit size into bitmap accumulator when applying
  • Loading branch information
antiochp authored Nov 26, 2019
1 parent 41896f0 commit 11ac7d8
Show file tree
Hide file tree
Showing 22 changed files with 901 additions and 175 deletions.
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion api/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ impl TxHashSet {
pub fn from_head(head: Arc<chain::Chain>) -> TxHashSet {
let roots = head.get_txhashset_roots();
TxHashSet {
output_root_hash: roots.output_root.to_hex(),
output_root_hash: roots.output_root().to_hex(),
range_proof_root_hash: roots.rproof_root.to_hex(),
kernel_root_hash: roots.kernel_root.to_hex(),
}
Expand Down
1 change: 1 addition & 0 deletions chain/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ workspace = ".."
edition = "2018"

[dependencies]
bit-vec = "0.6"
bitflags = "1"
byteorder = "1"
failure = "0.1"
Expand Down
2 changes: 1 addition & 1 deletion chain/src/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,7 @@ impl Chain {
b.header.prev_root = prev_root;

// Set the output, rangeproof and kernel MMR roots.
b.header.output_root = roots.output_root;
b.header.output_root = roots.output_root();
b.header.range_proof_root = roots.rproof_root;
b.header.kernel_root = roots.kernel_root;

Expand Down
2 changes: 2 additions & 0 deletions chain/src/txhashset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
//! Utility structs to handle the 3 hashtrees (output, range proof,
//! kernel) more conveniently and transactionally.
mod bitmap_accumulator;
mod rewindable_kernel_view;
mod txhashset;
mod utxo_view;

pub use self::bitmap_accumulator::*;
pub use self::rewindable_kernel_view::*;
pub use self::txhashset::*;
pub use self::utxo_view::*;
239 changes: 239 additions & 0 deletions chain/src/txhashset/bitmap_accumulator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
// Copyright 2019 The Grin Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::convert::TryFrom;
use std::time::Instant;

use bit_vec::BitVec;
use croaring::Bitmap;

use crate::core::core::hash::{DefaultHashable, Hash};
use crate::core::core::pmmr::{self, ReadonlyPMMR, VecBackend, PMMR};
use crate::core::ser::{self, FixedLength, PMMRable, Readable, Reader, Writeable, Writer};
use crate::error::{Error, ErrorKind};

/// The "bitmap accumulator" allows us to commit to a specific bitmap by splitting it into
/// fragments and inserting these fragments into an MMR to produce an overall root hash.
/// Leaves in the MMR are fragments of the bitmap consisting of 1024 contiguous bits
/// from the overall bitmap. The first (leftmost) leaf in the MMR represents the first 1024 bits
/// of the bitmap, the next leaf is the next 1024 bits of the bitmap etc.
///
/// Flipping a single bit does not require the full bitmap to be rehashed, only the path from the
/// relevant leaf up to its associated peak.
///
/// Flipping multiple bits *within* a single chunk is no more expensive than flipping a single bit
/// as a leaf node in the MMR represents a sequence of 1024 bits. Flipping multiple bits located
/// close together is a relatively cheap operation with minimal rehashing required to update the
/// relevant peaks and the overall MMR root.
///
/// It is also possible to generate Merkle proofs for these 1024 bit fragments, proving
/// both inclusion and location in the overall "accumulator" MMR. We plan to take advantage of
/// this during fast sync, allowing for validation of partial data.
///
#[derive(Clone)]
pub struct BitmapAccumulator {
backend: VecBackend<BitmapChunk>,
}

impl BitmapAccumulator {
/// Crate a new empty bitmap accumulator.
pub fn new() -> BitmapAccumulator {
BitmapAccumulator {
backend: VecBackend::new_hash_only(),
}
}

/// Initialize a bitmap accumulator given the provided idx iterator.
pub fn init<T: IntoIterator<Item = u64>>(&mut self, idx: T, size: u64) -> Result<(), Error> {
self.apply_from(idx, 0, size)
}

/// Find the start of the first "chunk" of 1024 bits from the provided idx.
/// Zero the last 10 bits to round down to multiple of 1024.
pub fn chunk_start_idx(idx: u64) -> u64 {
idx & !0x3ff
}

/// The first 1024 belong to chunk 0, the next 1024 to chunk 1 etc.
fn chunk_idx(idx: u64) -> u64 {
idx / 1024
}

/// Apply the provided idx iterator to our bitmap accumulator.
/// We start at the chunk containing from_idx and rebuild chunks as necessary
/// for the bitmap, limiting it to size (in bits).
/// If from_idx is 1023 and size is 1024 then we rebuild a single chunk.
fn apply_from<T>(&mut self, idx: T, from_idx: u64, size: u64) -> Result<(), Error>
where
T: IntoIterator<Item = u64>,
{
let now = Instant::now();

// Find the (1024 bit chunk) chunk_idx for the (individual bit) from_idx.
let from_chunk_idx = BitmapAccumulator::chunk_idx(from_idx);
let mut chunk_idx = from_chunk_idx;

let mut chunk = BitmapChunk::new();

let mut idx_iter = idx.into_iter().filter(|&x| x < size).peekable();
while let Some(x) = idx_iter.peek() {
if *x < chunk_idx * 1024 {
// skip until we reach our first chunk
idx_iter.next();
} else if *x < (chunk_idx + 1) * 1024 {
let idx = idx_iter.next().expect("next after peek");
chunk.set(idx % 1024, true);
} else {
self.append_chunk(chunk)?;
chunk_idx += 1;
chunk = BitmapChunk::new();
}
}
if chunk.any() {
self.append_chunk(chunk)?;
}
debug!(
"applied {} chunks from idx {} to idx {} ({}ms)",
1 + chunk_idx - from_chunk_idx,
from_chunk_idx,
chunk_idx,
now.elapsed().as_millis(),
);
Ok(())
}

/// Apply updates to the bitmap accumulator given an iterator of invalidated idx and
/// an iterator of idx to be set to true.
/// We determine the existing chunks to be rebuilt given the invalidated idx.
/// We then rebuild given idx, extending the accumulator with new chunk(s) as necessary.
/// Resulting bitmap accumulator will contain sufficient bitmap chunks to cover size.
/// If size is 1 then we will have a single chunk.
/// If size is 1023 then we will have a single chunk (bits 0 to 1023 inclusive).
/// If the size is 1024 then we will have two chunks.
pub fn apply<T, U>(&mut self, invalidated_idx: T, idx: U, size: u64) -> Result<(), Error>
where
T: IntoIterator<Item = u64>,
U: IntoIterator<Item = u64>,
{
// Determine the earliest chunk by looking at the min invalidated idx (assume sorted).
// Rewind prior to this and reapply new_idx.
// Note: We rebuild everything after rewind point but much of the bitmap may be
// unchanged. This can be further optimized by only rebuilding necessary chunks and
// rehashing.
if let Some(from_idx) = invalidated_idx.into_iter().next() {
self.rewind_prior(from_idx)?;
self.pad_left(from_idx)?;
self.apply_from(idx, from_idx, size)?;
}

Ok(())
}

/// Given the provided (bit) idx rewind the bitmap accumulator to the end of the
/// previous chunk ready for the updated chunk to be appended.
fn rewind_prior(&mut self, from_idx: u64) -> Result<(), Error> {
let chunk_idx = BitmapAccumulator::chunk_idx(from_idx);
let last_pos = self.backend.size();
let mut pmmr = PMMR::at(&mut self.backend, last_pos);
let chunk_pos = pmmr::insertion_to_pmmr_index(chunk_idx + 1);
let rewind_pos = chunk_pos.saturating_sub(1);
pmmr.rewind(rewind_pos, &Bitmap::create())
.map_err(|e| ErrorKind::Other(e))?;
Ok(())
}

/// Make sure we append empty chunks to fill in any gap before we append the chunk
/// we actually care about. This effectively pads the bitmap with 1024 chunks of 0s
/// as necessary to put the new chunk at the correct place.
fn pad_left(&mut self, from_idx: u64) -> Result<(), Error> {
let chunk_idx = BitmapAccumulator::chunk_idx(from_idx);
let current_chunk_idx = pmmr::n_leaves(self.backend.size());
for _ in current_chunk_idx..chunk_idx {
self.append_chunk(BitmapChunk::new())?;
}
Ok(())
}

/// Append a new chunk to the BitmapAccumulator.
/// Append parent hashes (if any) as necessary to build associated peak.
pub fn append_chunk(&mut self, chunk: BitmapChunk) -> Result<u64, Error> {
let last_pos = self.backend.size();
PMMR::at(&mut self.backend, last_pos)
.push(&chunk)
.map_err(|e| ErrorKind::Other(e).into())
}

/// The root hash of the bitmap accumulator MMR.
pub fn root(&self) -> Hash {
ReadonlyPMMR::at(&self.backend, self.backend.size()).root()
}
}

/// A bitmap "chunk" representing 1024 contiguous bits of the overall bitmap.
/// The first 1024 bits belong in one chunk. The next 1024 bits in the next chunk, etc.
#[derive(Clone, Debug)]
pub struct BitmapChunk(BitVec);

impl BitmapChunk {
const LEN_BITS: usize = 1024;
const LEN_BYTES: usize = Self::LEN_BITS / 8;

/// Create a new bitmap chunk, defaulting all bits in the chunk to false.
pub fn new() -> BitmapChunk {
BitmapChunk(BitVec::from_elem(Self::LEN_BITS, false))
}

/// Set a single bit in this chunk.
/// 0-indexed from start of chunk.
/// Panics if idx is outside the valid range of bits in a chunk.
pub fn set(&mut self, idx: u64, value: bool) {
let idx = usize::try_from(idx).expect("usize from u64");
assert!(idx < Self::LEN_BITS);
self.0.set(idx, value)
}

/// Does this bitmap chunk have any bits set to 1?
pub fn any(&self) -> bool {
self.0.any()
}
}

impl PMMRable for BitmapChunk {
type E = Self;

fn as_elmt(&self) -> Self::E {
self.clone()
}
}

impl FixedLength for BitmapChunk {
const LEN: usize = Self::LEN_BYTES;
}

impl DefaultHashable for BitmapChunk {}

impl Writeable for BitmapChunk {
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ser::Error> {
self.0.to_bytes().write(writer)
}
}

impl Readable for BitmapChunk {
/// Reading is not currently supported, just return an empty one for now.
/// We store the underlying roaring bitmap externally for the bitmap accumulator
/// and the "hash only" backend means we never actually read these chunks.
fn read(_reader: &mut dyn Reader) -> Result<BitmapChunk, ser::Error> {
Ok(BitmapChunk::new())
}
}
Loading

0 comments on commit 11ac7d8

Please sign in to comment.