From cba313733851347cd08f6afe24d2814719fb10a6 Mon Sep 17 00:00:00 2001 From: Antioch Peverell Date: Mon, 23 Nov 2020 19:07:07 +0000 Subject: [PATCH] add segmenter for generating segments from txhashset with consistent rewind (#3482) * add segmenter for generating segments from txhashset with consistent rewind * rework segmenter to take a txhashset wrapped in rwlock rework our rewindable pmmr so we can convert to readonly easily * placeholder code for rewinding readonly txhashset extension to build a rangeproof segment * segment creation for outputs/rangeproofs/kernels/bitmaps * placeholder segment impl * commit * rework segmenter to use a cached bitmap (rewind is expensive) * cache segmenter instance based on current archive header * integrate the real segment and segment identifier with our segmenter * exercise the segmenter code on chain init * wrap accumulator in an arc, no need to clone each time --- chain/src/chain.rs | 83 +++++++++- chain/src/error.rs | 12 ++ chain/src/txhashset.rs | 2 + chain/src/txhashset/bitmap_accumulator.rs | 9 +- chain/src/txhashset/rewindable_kernel_view.rs | 12 +- chain/src/txhashset/segmenter.rs | 149 ++++++++++++++++++ chain/src/txhashset/txhashset.rs | 56 ++++++- core/src/core/pmmr/rewindable_pmmr.rs | 68 +------- core/src/core/pmmr/segment.rs | 16 ++ store/tests/lmdb.rs | 1 - util/src/lib.rs | 3 +- 11 files changed, 335 insertions(+), 76 deletions(-) create mode 100644 chain/src/txhashset/segmenter.rs diff --git a/chain/src/chain.rs b/chain/src/chain.rs index cd0d14e0ea..cd0afd3878 100644 --- a/chain/src/chain.rs +++ b/chain/src/chain.rs @@ -20,7 +20,7 @@ use crate::core::core::merkle_proof::MerkleProof; use crate::core::core::verifier_cache::VerifierCache; use crate::core::core::{ Block, BlockHeader, BlockSums, Committed, Inputs, KernelFeatures, Output, OutputIdentifier, - Transaction, TxKernel, + SegmentIdentifier, Transaction, TxKernel, }; use crate::core::global; use crate::core::pow; @@ -29,12 +29,13 @@ use crate::error::{Error, ErrorKind}; use crate::pipe; use crate::store; use crate::txhashset; -use crate::txhashset::{PMMRHandle, TxHashSet}; +use crate::txhashset::{PMMRHandle, Segmenter, TxHashSet}; use crate::types::{ BlockStatus, ChainAdapter, CommitPos, NoStatus, Options, Tip, TxHashsetWriteStatus, }; use crate::util::secp::pedersen::{Commitment, RangeProof}; -use crate::{util::RwLock, ChainStore}; +use crate::util::RwLock; +use crate::ChainStore; use grin_core::ser; use grin_store::Error::NotFoundErr; use std::fs::{self, File}; @@ -152,6 +153,7 @@ pub struct Chain { header_pmmr: Arc>>, sync_pmmr: Arc>>, verifier_cache: Arc>, + pibd_segmenter: Arc>>, // POW verification function pow_verifier: fn(&BlockHeader) -> Result<(), pow::Error>, archive_mode: bool, @@ -217,6 +219,7 @@ impl Chain { txhashset: Arc::new(RwLock::new(txhashset)), header_pmmr: Arc::new(RwLock::new(header_pmmr)), sync_pmmr: Arc::new(RwLock::new(sync_pmmr)), + pibd_segmenter: Arc::new(RwLock::new(None)), pow_verifier, verifier_cache, archive_mode, @@ -225,6 +228,22 @@ impl Chain { chain.log_heads()?; + // Temporarily exercising the initialization process. + // Note: This is *really* slow because we are starting from cold. + // + // This is not required as we will lazily initialize our segmenter as required + // once we start receiving PIBD segment requests. + // In reality we will do this based on PIBD segment requests. + // Initialization (once per 12 hour period) will not be this slow once lmdb and PMMRs + // are warmed up. + { + let segmenter = chain.segmenter()?; + let _ = segmenter.kernel_segment(SegmentIdentifier { height: 9, idx: 0 }); + let _ = segmenter.bitmap_segment(SegmentIdentifier { height: 9, idx: 0 }); + let _ = segmenter.output_segment(SegmentIdentifier { height: 11, idx: 0 }); + let _ = segmenter.rangeproof_segment(SegmentIdentifier { height: 7, idx: 0 }); + } + Ok(chain) } @@ -815,6 +834,64 @@ impl Chain { }) } + /// The segmenter is responsible for generation PIBD segments. + /// We cache a segmenter instance based on the current archve period (new period every 12 hours). + /// This allows us to efficiently generate bitmap segments for the current archive period. + /// + /// It is a relatively expensive operation to initializa and cache a new segmenter instance + /// as this involves rewinding the txhashet by approx 720 blocks (12 hours). + /// + /// Caller is responsible for only doing this when required. + /// Caller should verify a peer segment request is valid before calling this for example. + /// + pub fn segmenter(&self) -> Result { + // The archive header corresponds to the data we will segment. + let ref archive_header = self.txhashset_archive_header()?; + + // Use our cached segmenter if we have one and the associated header matches. + if let Some(x) = self.pibd_segmenter.read().as_ref() { + if x.header() == archive_header { + return Ok(x.clone()); + } + } + + // We have no cached segmenter or the cached segmenter is no longer useful. + // Initialize a new segment, cache it and return it. + let segmenter = self.init_segmenter(archive_header)?; + let mut cache = self.pibd_segmenter.write(); + *cache = Some(segmenter.clone()); + + return Ok(segmenter); + } + + /// This is an expensive rewind to recreate bitmap state but we only need to do this once. + /// Caller is responsible for "caching" the segmenter (per archive period) for reuse. + fn init_segmenter(&self, header: &BlockHeader) -> Result { + let now = Instant::now(); + debug!( + "init_segmenter: initializing new segmenter for {} at {}", + header.hash(), + header.height + ); + + let mut header_pmmr = self.header_pmmr.write(); + let mut txhashset = self.txhashset.write(); + + let bitmap_snapshot = + txhashset::extending_readonly(&mut header_pmmr, &mut txhashset, |ext, batch| { + ext.extension.rewind(header, batch)?; + Ok(ext.extension.bitmap_accumulator()) + })?; + + debug!("init_segmenter: done, took {}ms", now.elapsed().as_millis()); + + Ok(Segmenter::new( + self.txhashset(), + Arc::new(bitmap_snapshot), + header.clone(), + )) + } + /// To support the ability to download the txhashset from multiple peers in parallel, /// the peers must all agree on the exact binary representation of the txhashset. /// This means compacting and rewinding to the exact same header. diff --git a/chain/src/error.rs b/chain/src/error.rs index d4d7dba529..561c11df3e 100644 --- a/chain/src/error.rs +++ b/chain/src/error.rs @@ -13,6 +13,7 @@ // limitations under the License. //! Error types for chain +use crate::core::core::pmmr::segment; use crate::core::core::{block, committed, transaction}; use crate::core::ser; use crate::keychain; @@ -149,6 +150,9 @@ pub enum ErrorKind { /// Error during chain sync #[fail(display = "Sync error")] SyncError(String), + /// PIBD segment related error + #[fail(display = "Segment error")] + SegmentError(segment::SegmentError), } impl Display for Error { @@ -273,6 +277,14 @@ impl From for Error { } } +impl From for Error { + fn from(error: segment::SegmentError) -> Error { + Error { + inner: Context::new(ErrorKind::SegmentError(error)), + } + } +} + impl From for Error { fn from(e: secp::Error) -> Error { Error { diff --git a/chain/src/txhashset.rs b/chain/src/txhashset.rs index 094df1cb96..1926d7d79a 100644 --- a/chain/src/txhashset.rs +++ b/chain/src/txhashset.rs @@ -17,10 +17,12 @@ mod bitmap_accumulator; mod rewindable_kernel_view; +mod segmenter; mod txhashset; mod utxo_view; pub use self::bitmap_accumulator::*; pub use self::rewindable_kernel_view::*; +pub use self::segmenter::*; pub use self::txhashset::*; pub use self::utxo_view::*; diff --git a/chain/src/txhashset/bitmap_accumulator.rs b/chain/src/txhashset/bitmap_accumulator.rs index f27ab1a47a..67fa61aa28 100644 --- a/chain/src/txhashset/bitmap_accumulator.rs +++ b/chain/src/txhashset/bitmap_accumulator.rs @@ -50,7 +50,7 @@ impl BitmapAccumulator { /// Crate a new empty bitmap accumulator. pub fn new() -> BitmapAccumulator { BitmapAccumulator { - backend: VecBackend::new_hash_only(), + backend: VecBackend::new(), } } @@ -176,9 +176,12 @@ impl BitmapAccumulator { /// The root hash of the bitmap accumulator MMR. pub fn root(&self) -> Hash { + self.readonly_pmmr().root().expect("no root, invalid tree") + } + + /// Readonly access to our internal data. + pub fn readonly_pmmr(&self) -> ReadonlyPMMR> { ReadonlyPMMR::at(&self.backend, self.backend.size()) - .root() - .expect("no root, invalid tree") } } diff --git a/chain/src/txhashset/rewindable_kernel_view.rs b/chain/src/txhashset/rewindable_kernel_view.rs index 96b83db7ca..3fcec1a586 100644 --- a/chain/src/txhashset/rewindable_kernel_view.rs +++ b/chain/src/txhashset/rewindable_kernel_view.rs @@ -14,7 +14,7 @@ //! Lightweight readonly view into kernel MMR for convenience. -use crate::core::core::pmmr::RewindablePMMR; +use crate::core::core::pmmr::{ReadablePMMR, ReadonlyPMMR, RewindablePMMR}; use crate::core::core::{BlockHeader, TxKernel}; use crate::error::{Error, ErrorKind}; use grin_store::pmmr::PMMRBackend; @@ -54,7 +54,10 @@ impl<'a> RewindableKernelView<'a> { /// fast sync where a reorg past the horizon could allow a whole rewrite of /// the kernel set. pub fn validate_root(&self) -> Result<(), Error> { - let root = self.pmmr.root().map_err(|_| ErrorKind::InvalidRoot)?; + let root = self + .readonly_pmmr() + .root() + .map_err(|_| ErrorKind::InvalidRoot)?; if root != self.header.kernel_root { return Err(ErrorKind::InvalidTxHashSet(format!( "Kernel root at {} does not match", @@ -64,4 +67,9 @@ impl<'a> RewindableKernelView<'a> { } Ok(()) } + + /// Readonly view of our internal data. + pub fn readonly_pmmr(&self) -> ReadonlyPMMR> { + self.pmmr.as_readonly() + } } diff --git a/chain/src/txhashset/segmenter.rs b/chain/src/txhashset/segmenter.rs new file mode 100644 index 0000000000..ccf21eeb51 --- /dev/null +++ b/chain/src/txhashset/segmenter.rs @@ -0,0 +1,149 @@ +// Copyright 2020 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. + +//! Generation of the various necessary segments requested during PIBD. + +use std::{sync::Arc, time::Instant}; + +use crate::core::core::hash::Hash; +use crate::core::core::pmmr::ReadablePMMR; +use crate::core::core::{BlockHeader, OutputIdentifier, Segment, SegmentIdentifier, TxKernel}; +use crate::error::{Error, ErrorKind}; +use crate::txhashset::{BitmapAccumulator, BitmapChunk, TxHashSet}; +use crate::util::secp::pedersen::RangeProof; +use crate::util::RwLock; + +/// Segmenter for generating PIBD segments. +#[derive(Clone)] +pub struct Segmenter { + txhashset: Arc>, + bitmap_snapshot: Arc, + header: BlockHeader, +} + +impl Segmenter { + /// Create a new segmenter based on the provided txhashset. + pub fn new( + txhashset: Arc>, + bitmap_snapshot: Arc, + header: BlockHeader, + ) -> Segmenter { + Segmenter { + txhashset, + bitmap_snapshot, + header, + } + } + + /// Header associated with this segmenter instance. + /// The bitmap "snapshot" corresponds to rewound state at this header. + pub fn header(&self) -> &BlockHeader { + &self.header + } + + /// Create a kernel segment. + pub fn kernel_segment(&self, id: SegmentIdentifier) -> Result, Error> { + let now = Instant::now(); + let txhashset = self.txhashset.read(); + let kernel_pmmr = txhashset.kernel_pmmr_at(&self.header); + let segment = Segment::from_pmmr(id, &kernel_pmmr, false)?; + debug!( + "kernel_segment: id: ({}, {}), leaves: {}, hashes: {}, proof hashes: {}, took {}ms", + segment.id().height, + segment.id().idx, + segment.leaf_iter().count(), + segment.hash_iter().count(), + segment.proof().size(), + now.elapsed().as_millis() + ); + Ok(segment) + } + + /// The root of the output PMMR based on size from the header. + fn output_root(&self) -> Result { + let txhashset = self.txhashset.read(); + let pmmr = txhashset.output_pmmr_at(&self.header); + let root = pmmr.root().map_err(&ErrorKind::TxHashSetErr)?; + Ok(root) + } + + /// The root of the bitmap snapshot PMMR. + fn bitmap_root(&self) -> Result { + let pmmr = self.bitmap_snapshot.readonly_pmmr(); + let root = pmmr.root().map_err(&ErrorKind::TxHashSetErr)?; + Ok(root) + } + + /// Create a utxo bitmap segment based on our bitmap "snapshot" and return it with + /// the corresponding output root. + pub fn bitmap_segment( + &self, + id: SegmentIdentifier, + ) -> Result<(Segment, Hash), Error> { + let now = Instant::now(); + let bitmap_pmmr = self.bitmap_snapshot.readonly_pmmr(); + let segment = Segment::from_pmmr(id, &bitmap_pmmr, false)?; + let output_root = self.output_root()?; + debug!( + "bitmap_segment: id: ({}, {}), leaves: {}, hashes: {}, proof hashes: {}, took {}ms", + segment.id().height, + segment.id().idx, + segment.leaf_iter().count(), + segment.hash_iter().count(), + segment.proof().size(), + now.elapsed().as_millis() + ); + Ok((segment, output_root)) + } + + /// Create an output segment and return it with the corresponding bitmap root. + pub fn output_segment( + &self, + id: SegmentIdentifier, + ) -> Result<(Segment, Hash), Error> { + let now = Instant::now(); + let txhashset = self.txhashset.read(); + let output_pmmr = txhashset.output_pmmr_at(&self.header); + let segment = Segment::from_pmmr(id, &output_pmmr, true)?; + let bitmap_root = self.bitmap_root()?; + debug!( + "output_segment: id: ({}, {}), leaves: {}, hashes: {}, proof hashes: {}, took {}ms", + segment.id().height, + segment.id().idx, + segment.leaf_iter().count(), + segment.hash_iter().count(), + segment.proof().size(), + now.elapsed().as_millis() + ); + Ok((segment, bitmap_root)) + } + + /// Create a rangeproof segment. + pub fn rangeproof_segment(&self, id: SegmentIdentifier) -> Result, Error> { + let now = Instant::now(); + let txhashset = self.txhashset.read(); + let pmmr = txhashset.rangeproof_pmmr_at(&self.header); + let segment = Segment::from_pmmr(id, &pmmr, true)?; + debug!( + "rangeproof_segment: id: ({}, {}), leaves: {}, hashes: {}, proof hashes: {}, took {}ms", + segment.id().height, + segment.id().idx, + segment.leaf_iter().count(), + segment.hash_iter().count(), + segment.proof().size(), + now.elapsed().as_millis() + ); + Ok(segment) + } +} diff --git a/chain/src/txhashset/txhashset.rs b/chain/src/txhashset/txhashset.rs index bc8089dff4..c0ec9d394a 100644 --- a/chain/src/txhashset/txhashset.rs +++ b/chain/src/txhashset/txhashset.rs @@ -19,14 +19,16 @@ use crate::core::consensus::WEEK_HEIGHT; use crate::core::core::committed::Committed; use crate::core::core::hash::{Hash, Hashed}; use crate::core::core::merkle_proof::MerkleProof; -use crate::core::core::pmmr::{self, Backend, ReadablePMMR, ReadonlyPMMR, RewindablePMMR, PMMR}; +use crate::core::core::pmmr::{ + self, Backend, ReadablePMMR, ReadonlyPMMR, RewindablePMMR, VecBackend, PMMR, +}; use crate::core::core::{Block, BlockHeader, KernelFeatures, Output, OutputIdentifier, TxKernel}; use crate::core::global; use crate::core::ser::{PMMRable, ProtocolVersion}; use crate::error::{Error, ErrorKind}; use crate::linked_list::{ListIndex, PruneableListIndex, RewindableListIndex}; use crate::store::{self, Batch, ChainStore}; -use crate::txhashset::bitmap_accumulator::BitmapAccumulator; +use crate::txhashset::bitmap_accumulator::{BitmapAccumulator, BitmapChunk}; use crate::txhashset::{RewindableKernelView, UTXOView}; use crate::types::{CommitPos, OutputRoots, Tip, TxHashSetRoots, TxHashsetWriteStatus}; use crate::util::secp::pedersen::{Commitment, RangeProof}; @@ -301,6 +303,30 @@ impl TxHashSet { .get_last_n_insertions(distance) } + /// Efficient view into the kernel PMMR based on size in header. + pub fn kernel_pmmr_at( + &self, + header: &BlockHeader, + ) -> ReadonlyPMMR> { + ReadonlyPMMR::at(&self.kernel_pmmr_h.backend, header.kernel_mmr_size) + } + + /// Efficient view into the output PMMR based on size in header. + pub fn output_pmmr_at( + &self, + header: &BlockHeader, + ) -> ReadonlyPMMR> { + ReadonlyPMMR::at(&self.output_pmmr_h.backend, header.output_mmr_size) + } + + /// Efficient view into the rangeproof PMMR based on size in header. + pub fn rangeproof_pmmr_at( + &self, + header: &BlockHeader, + ) -> ReadonlyPMMR> { + ReadonlyPMMR::at(&self.rproof_pmmr_h.backend, header.output_mmr_size) + } + /// Convenience function to query the db for a header by its hash. pub fn get_block_header(&self, hash: &Hash) -> Result { Ok(self.commit_index.get_block_header(&hash)?) @@ -1069,11 +1095,33 @@ impl<'a> Extension<'a> { pub fn utxo_view(&'a self, header_ext: &'a HeaderExtension<'a>) -> UTXOView<'a> { UTXOView::new( header_ext.pmmr.readonly_pmmr(), - self.output_pmmr.readonly_pmmr(), - self.rproof_pmmr.readonly_pmmr(), + self.output_readonly_pmmr(), + self.rproof_readonly_pmmr(), ) } + /// Readonly view of our output data. + pub fn output_readonly_pmmr( + &self, + ) -> ReadonlyPMMR> { + self.output_pmmr.readonly_pmmr() + } + + /// Take a snapshot of our bitmap accumulator + pub fn bitmap_accumulator(&self) -> BitmapAccumulator { + self.bitmap_accumulator.clone() + } + + /// Readonly view of our bitmap accumulator data. + pub fn bitmap_readonly_pmmr(&self) -> ReadonlyPMMR> { + self.bitmap_accumulator.readonly_pmmr() + } + + /// Readonly view of our rangeproof data. + pub fn rproof_readonly_pmmr(&self) -> ReadonlyPMMR> { + self.rproof_pmmr.readonly_pmmr() + } + /// Apply a new block to the current txhashet extension (output, rangeproof, kernel MMRs). /// Returns a vec of commit_pos representing the pos and height of the outputs spent /// by this block. diff --git a/core/src/core/pmmr/rewindable_pmmr.rs b/core/src/core/pmmr/rewindable_pmmr.rs index 1e33c6fa35..a3b9274347 100644 --- a/core/src/core/pmmr/rewindable_pmmr.rs +++ b/core/src/core/pmmr/rewindable_pmmr.rs @@ -17,9 +17,8 @@ use std::marker; -use crate::core::hash::{Hash, ZERO_HASH}; -use crate::core::pmmr::{bintree_postorder_height, is_leaf, peaks, Backend}; -use crate::ser::{PMMRIndexHashable, PMMRable}; +use crate::core::pmmr::{bintree_postorder_height, Backend, ReadonlyPMMR}; +use crate::ser::PMMRable; /// Rewindable (but still readonly) view of a PMMR. pub struct RewindablePMMR<'a, T, B> @@ -49,11 +48,6 @@ where } } - /// Reference to the underlying storage backend. - pub fn backend(&'a self) -> &dyn Backend { - self.backend - } - /// Build a new readonly PMMR pre-initialized to /// last_pos with the provided backend. pub fn at(backend: &'a B, last_pos: u64) -> RewindablePMMR<'_, T, B> { @@ -74,62 +68,14 @@ where while bintree_postorder_height(pos + 1) > 0 { pos += 1; } - self.last_pos = pos; Ok(()) } - /// Get the data element at provided position in the MMR. - pub fn get_data(&self, pos: u64) -> Option { - if pos > self.last_pos { - // If we are beyond the rhs of the MMR return None. - None - } else if is_leaf(pos) { - // If we are a leaf then get data from the backend. - self.backend.get_data(pos) - } else { - // If we are not a leaf then return None as only leaves have data. - None - } - } - - /// Is the MMR empty? - pub fn is_empty(&self) -> bool { - self.last_pos == 0 - } - - /// Computes the root of the MMR. Find all the peaks in the current - /// tree and "bags" them to get a single peak. - pub fn root(&self) -> Result { - if self.is_empty() { - return Ok(ZERO_HASH); - } - let mut res = None; - for peak in self.peaks().iter().rev() { - res = match res { - None => Some(*peak), - Some(rhash) => Some((*peak, rhash).hash_with_index(self.unpruned_size())), - } - } - res.ok_or_else(|| "no root, invalid tree".to_owned()) - } - - /// Returns a vec of the peaks of this MMR. - pub fn peaks(&self) -> Vec { - let peaks_pos = peaks(self.last_pos); - peaks_pos - .into_iter() - .filter_map(|pi| { - // here we want to get from underlying hash file - // as the pos *may* have been "removed" - self.backend.get_from_file(pi) - }) - .collect() - } - - /// Total size of the tree, including intermediary nodes and ignoring any - /// pruning. - pub fn unpruned_size(&self) -> u64 { - self.last_pos + /// Allows conversion of a "rewindable" PMMR into a "readonly" PMMR. + /// Intended usage is to create a rewindable PMMR, rewind it, + /// then convert to "readonly" and read from it. + pub fn as_readonly(&self) -> ReadonlyPMMR<'a, T, B> { + ReadonlyPMMR::at(&self.backend, self.last_pos) } } diff --git a/core/src/core/pmmr/segment.rs b/core/src/core/pmmr/segment.rs index 551254c8bf..4e870817f2 100644 --- a/core/src/core/pmmr/segment.rs +++ b/core/src/core/pmmr/segment.rs @@ -132,6 +132,7 @@ impl Segment { (first, last) } + /// TODO - binary_search_by_key() here (can we assume these are sorted by pos?) fn get_hash(&self, pos: u64) -> Result { self.hash_pos .iter() @@ -153,6 +154,16 @@ impl Segment { .zip(&self.hashes) .map(|(&p, &h)| (p, h)) } + + /// Segment proof + pub fn proof(&self) -> &SegmentProof { + &self.proof + } + + /// Segment identifier + pub fn id(&self) -> SegmentIdentifier { + self.identifier + } } impl Segment @@ -539,6 +550,11 @@ impl SegmentProof { Ok(proof) } + /// Size of the proof in hashes. + pub fn size(&self) -> usize { + self.hashes.len() + } + /// Reconstruct PMMR root using this proof pub fn reconstruct_root( &self, diff --git a/store/tests/lmdb.rs b/store/tests/lmdb.rs index 6dfebc16b5..0bbf3c4c4e 100644 --- a/store/tests/lmdb.rs +++ b/store/tests/lmdb.rs @@ -15,7 +15,6 @@ use grin_core as core; use grin_store as store; use grin_util as util; -use store::PrefixIterator; use crate::core::global; use crate::core::ser::{self, Readable, Reader, Writeable, Writer}; diff --git a/util/src/lib.rs b/util/src/lib.rs index fbe280272e..ad948e9edc 100644 --- a/util/src/lib.rs +++ b/util/src/lib.rs @@ -28,8 +28,7 @@ extern crate lazy_static; #[macro_use] extern crate serde_derive; // Re-export so only has to be included once -pub use parking_lot::Mutex; -pub use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +pub use parking_lot::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; // Re-export so only has to be included once pub use secp256k1zkp as secp;