#![warn(
missing_debug_implementations,
missing_docs,
unsafe_code,
bare_trait_objects
)]
#![warn(clippy::pedantic)]
#![allow(
// Next `cast_*` lints don't give alternatives.
clippy::cast_possible_wrap, clippy::cast_possible_truncation, clippy::cast_sign_loss,
// Next lints produce too much noise/false positives.
clippy::module_name_repetitions, clippy::similar_names, clippy::must_use_candidate,
clippy::pub_enum_variant_names,
// '... may panic' lints.
clippy::indexing_slicing,
// Too much work to fix.
clippy::missing_errors_doc,
// False positive: WebSocket
clippy::doc_markdown
)]
use chrono::{DateTime, Utc};
use exonum::{
blockchain::{Block, CallInBlock, CallProof, Schema, TxLocation},
crypto::Hash,
helpers::Height,
merkledb::{ListProof, ObjectHash, Snapshot},
messages::{AnyTx, Precommit, Verified},
runtime::{ExecutionError, ExecutionStatus},
};
use serde::{Serialize, Serializer};
use serde_derive::*;
use std::{
cell::{Ref, RefCell},
collections::{BTreeMap, Bound},
fmt,
ops::{Index, RangeBounds},
slice,
time::UNIX_EPOCH,
};
pub mod api;
fn end_height(bound: Bound<&Height>, max: Height) -> Height {
use std::cmp::min;
let inner_end = match bound {
Bound::Included(height) => height.next(),
Bound::Excluded(height) => *height,
Bound::Unbounded => max.next(),
};
min(inner_end, max.next())
}
#[derive(Debug)]
pub struct BlockInfo<'a> {
header: Block,
explorer: &'a BlockchainExplorer<'a>,
precommits: RefCell<Option<Vec<Verified<Precommit>>>>,
txs: RefCell<Option<Vec<Hash>>>,
}
impl<'a> BlockInfo<'a> {
fn new(explorer: &'a BlockchainExplorer<'_>, height: Height) -> Self {
let schema = explorer.schema;
let hashes = schema.block_hashes_by_height();
let blocks = schema.blocks();
let block_hash = hashes
.get(height.0)
.unwrap_or_else(|| panic!("Block not found, height: {:?}", height));
let header = blocks
.get(&block_hash)
.unwrap_or_else(|| panic!("Block not found, hash: {:?}", block_hash));
BlockInfo {
explorer,
header,
precommits: RefCell::new(None),
txs: RefCell::new(None),
}
}
pub fn header(&self) -> &Block {
&self.header
}
pub fn into_header(self) -> Block {
self.header
}
pub fn height(&self) -> Height {
self.header.height
}
pub fn len(&self) -> usize {
self.header.tx_count as usize
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn precommits(&self) -> Ref<'_, [Verified<Precommit>]> {
if self.precommits.borrow().is_none() {
let precommits = self.explorer.precommits(&self.header);
*self.precommits.borrow_mut() = Some(precommits);
}
Ref::map(self.precommits.borrow(), |cache| {
cache.as_ref().unwrap().as_ref()
})
}
pub fn transaction_hashes(&self) -> Ref<'_, [Hash]> {
if self.txs.borrow().is_none() {
let txs = self.explorer.transaction_hashes(&self.header);
*self.txs.borrow_mut() = Some(txs);
}
Ref::map(self.txs.borrow(), |cache| cache.as_ref().unwrap().as_ref())
}
pub fn transaction(&self, index: usize) -> Option<CommittedTransaction> {
self.transaction_hashes()
.get(index)
.map(|hash| self.explorer.committed_transaction(hash, None))
}
pub fn call_proof(&self, call_location: CallInBlock) -> CallProof {
self.explorer
.schema
.call_records(self.header.height)
.unwrap() .get_proof(call_location)
}
pub fn iter(&self) -> Transactions<'_, '_> {
Transactions {
block: self,
ptr: 0,
len: self.len(),
}
}
pub fn with_transactions(self) -> BlockWithTransactions {
let (explorer, header, precommits, transactions) =
(self.explorer, self.header, self.precommits, self.txs);
let precommits = precommits
.into_inner()
.unwrap_or_else(|| explorer.precommits(&header));
let transactions = transactions
.into_inner()
.unwrap_or_else(|| explorer.transaction_hashes(&header))
.iter()
.map(|tx_hash| explorer.committed_transaction(tx_hash, None))
.collect();
let errors = self
.explorer
.schema
.call_records(header.height)
.expect("No call record for a committed block");
let errors: Vec<_> = errors
.errors()
.map(|(location, error)| ErrorWithLocation { location, error })
.collect();
BlockWithTransactions {
header,
precommits,
transactions,
errors,
}
}
}
impl<'a> Serialize for BlockInfo<'a> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let mut s = serializer.serialize_struct("BlockInfo", 3)?;
s.serialize_field("block", &self.header)?;
s.serialize_field("precommits", &*self.precommits())?;
s.serialize_field("txs", &*self.transaction_hashes())?;
s.end()
}
}
#[derive(Debug)]
pub struct Transactions<'r, 'a> {
block: &'r BlockInfo<'a>,
ptr: usize,
len: usize,
}
impl<'a, 'r> Iterator for Transactions<'a, 'r> {
type Item = CommittedTransaction;
fn next(&mut self) -> Option<CommittedTransaction> {
if self.ptr == self.len {
None
} else {
let transaction = self.block.transaction(self.ptr);
self.ptr += 1;
transaction
}
}
}
impl<'a, 'r: 'a> IntoIterator for &'r BlockInfo<'a> {
type Item = CommittedTransaction;
type IntoIter = Transactions<'a, 'r>;
fn into_iter(self) -> Transactions<'a, 'r> {
self.iter()
}
}
#[derive(Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub struct BlockWithTransactions {
#[serde(rename = "block")]
pub header: Block,
pub precommits: Vec<Verified<Precommit>>,
pub transactions: Vec<CommittedTransaction>,
pub errors: Vec<ErrorWithLocation>,
}
#[derive(Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ErrorWithLocation {
pub location: CallInBlock,
pub error: ExecutionError,
}
impl fmt::Display for ErrorWithLocation {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "In {}: {}", self.location, self.error)
}
}
impl BlockWithTransactions {
pub fn height(&self) -> Height {
self.header.height
}
pub fn len(&self) -> usize {
self.transactions.len()
}
pub fn is_empty(&self) -> bool {
self.transactions.is_empty()
}
pub fn iter(&self) -> EagerTransactions<'_> {
self.transactions.iter()
}
pub fn error_map(&self) -> BTreeMap<CallInBlock, &ExecutionError> {
self.errors.iter().map(|e| (e.location, &e.error)).collect()
}
}
pub type EagerTransactions<'a> = slice::Iter<'a, CommittedTransaction>;
impl Index<usize> for BlockWithTransactions {
type Output = CommittedTransaction;
fn index(&self, index: usize) -> &CommittedTransaction {
self.transactions.get(index).unwrap_or_else(|| {
panic!(
"Index exceeds number of transactions in block {}",
self.len()
);
})
}
}
impl Index<Hash> for BlockWithTransactions {
type Output = CommittedTransaction;
fn index(&self, index: Hash) -> &CommittedTransaction {
self.transactions
.iter()
.find(|&tx| tx.message.object_hash() == index)
.unwrap_or_else(|| {
panic!("No transaction with hash {} in the block", index);
})
}
}
impl<'a> IntoIterator for &'a BlockWithTransactions {
type Item = &'a CommittedTransaction;
type IntoIter = EagerTransactions<'a>;
fn into_iter(self) -> EagerTransactions<'a> {
self.iter()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CommittedTransaction {
message: Verified<AnyTx>,
location: TxLocation,
location_proof: ListProof<Hash>,
status: ExecutionStatus,
time: DateTime<Utc>,
}
impl CommittedTransaction {
pub fn message(&self) -> &Verified<AnyTx> {
&self.message
}
pub fn location(&self) -> &TxLocation {
&self.location
}
pub fn location_proof(&self) -> &ListProof<Hash> {
&self.location_proof
}
pub fn status(&self) -> Result<(), &ExecutionError> {
self.status.0.as_ref().map(drop)
}
pub fn time(&self) -> &DateTime<Utc> {
&self.time
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum TransactionInfo {
InPool {
message: Verified<AnyTx>,
},
Committed(CommittedTransaction),
}
impl TransactionInfo {
pub fn message(&self) -> &Verified<AnyTx> {
match *self {
TransactionInfo::InPool { ref message } => message,
TransactionInfo::Committed(ref tx) => tx.message(),
}
}
pub fn is_in_pool(&self) -> bool {
match *self {
TransactionInfo::InPool { .. } => true,
_ => false,
}
}
pub fn is_committed(&self) -> bool {
match *self {
TransactionInfo::Committed(_) => true,
_ => false,
}
}
pub fn as_committed(&self) -> Option<&CommittedTransaction> {
match *self {
TransactionInfo::Committed(ref tx) => Some(tx),
_ => None,
}
}
}
#[derive(Debug, Copy, Clone)]
pub struct BlockchainExplorer<'a> {
schema: Schema<&'a dyn Snapshot>,
}
impl<'a> BlockchainExplorer<'a> {
pub fn new(snapshot: &'a dyn Snapshot) -> Self {
BlockchainExplorer {
schema: Schema::new(snapshot),
}
}
pub fn from_schema(schema: Schema<&'a dyn Snapshot>) -> Self {
BlockchainExplorer { schema }
}
pub fn transaction(&self, tx_hash: &Hash) -> Option<TransactionInfo> {
let message = self.transaction_without_proof(tx_hash)?;
if self.schema.transactions_pool().contains(tx_hash) {
return Some(TransactionInfo::InPool { message });
}
let tx = self.committed_transaction(tx_hash, Some(message));
Some(TransactionInfo::Committed(tx))
}
pub fn call_status(
&self,
block_height: Height,
call_location: CallInBlock,
) -> Result<(), ExecutionError> {
match self.schema.call_records(block_height) {
Some(errors) => errors.get(call_location),
None => Ok(()),
}
}
pub fn transaction_without_proof(&self, tx_hash: &Hash) -> Option<Verified<AnyTx>> {
self.schema.transactions().get(tx_hash)
}
fn precommits(&self, block: &Block) -> Vec<Verified<Precommit>> {
self.schema
.precommits(&block.object_hash())
.iter()
.collect()
}
fn transaction_hashes(&self, block: &Block) -> Vec<Hash> {
let tx_hashes_table = self.schema.block_transactions(block.height);
tx_hashes_table.iter().collect()
}
fn committed_transaction(
&self,
tx_hash: &Hash,
maybe_content: Option<Verified<AnyTx>>,
) -> CommittedTransaction {
let location = self
.schema
.transactions_locations()
.get(tx_hash)
.unwrap_or_else(|| panic!("Location not found for transaction hash {:?}", tx_hash));
let location_proof = self
.schema
.block_transactions(location.block_height())
.get_proof(u64::from(location.position_in_block()));
let block_precommits = self
.schema
.block_and_precommits(location.block_height())
.unwrap();
let time = median_precommits_time(&block_precommits.precommits);
let status = self.schema.transaction_result(location).unwrap();
CommittedTransaction {
message: maybe_content.unwrap_or_else(|| {
self.schema
.transactions()
.get(tx_hash)
.expect("BUG: Cannot find transaction in database")
}),
location,
location_proof,
status: ExecutionStatus(status),
time,
}
}
pub fn height(&self) -> Height {
self.schema.height()
}
pub fn block(&self, height: Height) -> Option<BlockInfo<'_>> {
if self.height() >= height {
Some(BlockInfo::new(self, height))
} else {
None
}
}
pub fn block_with_txs(&self, height: Height) -> Option<BlockWithTransactions> {
let txs_table = self.schema.block_transactions(height);
let block_proof = self.schema.block_and_precommits(height)?;
let errors = self.schema.call_records(height)?;
Some(BlockWithTransactions {
header: block_proof.block,
precommits: block_proof.precommits,
transactions: txs_table
.iter()
.map(|tx_hash| self.committed_transaction(&tx_hash, None))
.collect(),
errors: errors
.errors()
.map(|(location, error)| ErrorWithLocation { location, error })
.collect(),
})
}
pub fn blocks<R: RangeBounds<Height>>(&self, heights: R) -> Blocks<'_> {
use std::cmp::max;
let max_height = self.schema.height();
let ptr = match heights.start_bound() {
Bound::Included(height) => *height,
Bound::Excluded(height) => height.next(),
Bound::Unbounded => Height(0),
};
Blocks {
explorer: self,
ptr,
back: max(ptr, end_height(heights.end_bound(), max_height)),
}
}
}
pub struct Blocks<'a> {
explorer: &'a BlockchainExplorer<'a>,
ptr: Height,
back: Height,
}
impl<'a> fmt::Debug for Blocks<'a> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
formatter
.debug_struct("Blocks")
.field("ptr", &self.ptr)
.field("back", &self.back)
.finish()
}
}
impl<'a> Iterator for Blocks<'a> {
type Item = BlockInfo<'a>;
fn next(&mut self) -> Option<BlockInfo<'a>> {
if self.ptr == self.back {
return None;
}
let block = BlockInfo::new(self.explorer, self.ptr);
self.ptr = self.ptr.next();
Some(block)
}
fn size_hint(&self) -> (usize, Option<usize>) {
let exact = (self.back.0 - self.ptr.0) as usize;
(exact, Some(exact))
}
fn count(self) -> usize {
(self.back.0 - self.ptr.0) as usize
}
fn nth(&mut self, n: usize) -> Option<BlockInfo<'a>> {
if self.ptr.0 + n as u64 >= self.back.0 {
self.ptr = self.back;
None
} else {
self.ptr = Height(self.ptr.0 + n as u64);
let block = BlockInfo::new(self.explorer, self.ptr);
self.ptr = self.ptr.next();
Some(block)
}
}
}
impl<'a> DoubleEndedIterator for Blocks<'a> {
fn next_back(&mut self) -> Option<BlockInfo<'a>> {
if self.ptr == self.back {
return None;
}
self.back = self.back.previous();
Some(BlockInfo::new(self.explorer, self.back))
}
}
pub fn median_precommits_time(precommits: &[Verified<Precommit>]) -> DateTime<Utc> {
if precommits.is_empty() {
UNIX_EPOCH.into()
} else {
let mut times: Vec<_> = precommits.iter().map(|p| p.payload().time).collect();
times.sort();
times[times.len() / 2]
}
}