use std::borrow::Cow;
use std::any::Any;
use std::error::Error;
use std::fmt;
use std::u8;
use serde::Serialize;
use serde::de::DeserializeOwned;
use messages::{Message, RawTransaction};
use storage::{Fork, StorageValue};
use crypto::{Hash, CryptoHash};
use encoding;
use encoding::serialize::json::ExonumJson;
#[cfg_attr(feature = "cargo-clippy", allow(cast_lossless))]
const MAX_ERROR_CODE: u16 = u8::MAX as u16;
const TRANSACTION_STATUS_OK: u16 = MAX_ERROR_CODE + 1;
const TRANSACTION_STATUS_PANIC: u16 = TRANSACTION_STATUS_OK + 1;
pub type ExecutionResult = Result<(), ExecutionError>;
pub type TransactionResult = Result<(), TransactionError>;
pub trait Transaction: Message + ExonumJson + 'static {
fn verify(&self) -> bool;
fn execute(&self, fork: &mut Fork) -> ExecutionResult;
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct ExecutionError {
code: u8,
description: Option<String>,
}
impl ExecutionError {
pub fn new(code: u8) -> Self {
Self {
code,
description: None,
}
}
pub fn with_description(code: u8, description: String) -> Self {
Self {
code,
description: Some(description),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum TransactionErrorType {
Panic,
Code(u8),
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct TransactionError {
error_type: TransactionErrorType,
description: Option<String>,
}
impl TransactionError {
fn new(error_type: TransactionErrorType, description: Option<String>) -> Self {
Self {
error_type,
description,
}
}
fn code(code: u8, description: Option<String>) -> Self {
Self::new(TransactionErrorType::Code(code), description)
}
fn panic(description: Option<String>) -> Self {
Self::new(TransactionErrorType::Panic, description)
}
pub(crate) fn from_panic(panic: &Box<Any + Send>) -> Self {
Self::panic(panic_description(panic))
}
pub fn error_type(&self) -> TransactionErrorType {
self.error_type
}
pub fn description(&self) -> Option<&str> {
self.description.as_ref().map(String::as_ref)
}
}
impl<'a, T: Transaction> From<T> for Box<Transaction + 'a> {
fn from(tx: T) -> Self {
Box::new(tx) as Box<Transaction>
}
}
impl fmt::Display for TransactionError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.error_type {
TransactionErrorType::Panic => write!(f, "Panic during execution")?,
TransactionErrorType::Code(c) => write!(f, "Error code: {}", c)?,
}
if let Some(ref description) = self.description {
write!(f, " description: {}", description)?;
}
Ok(())
}
}
impl CryptoHash for TransactionResult {
fn hash(&self) -> Hash {
u16::hash(&status_as_u16(self))
}
}
impl From<ExecutionError> for TransactionError {
fn from(error: ExecutionError) -> Self {
Self {
error_type: TransactionErrorType::Code(error.code),
description: error.description,
}
}
}
impl StorageValue for TransactionResult {
fn into_bytes(self) -> Vec<u8> {
let mut res = u16::into_bytes(status_as_u16(&self));
if let Some(description) = self.err().and_then(|e| e.description) {
res.extend(bool::into_bytes(true));
res.extend(String::into_bytes(description));
} else {
res.extend(bool::into_bytes(false));
}
res
}
fn from_bytes(bytes: Cow<[u8]>) -> Self {
let main_part = u16::from_bytes(Cow::Borrowed(&bytes));
let description = if bool::from_bytes(Cow::Borrowed(&bytes[2..3])) {
Some(String::from_bytes(Cow::Borrowed(&bytes[3..])))
} else {
None
};
match main_part {
value @ 0...MAX_ERROR_CODE => Err(TransactionError::code(value as u8, description)),
TRANSACTION_STATUS_OK => Ok(()),
TRANSACTION_STATUS_PANIC => Err(TransactionError::panic(description)),
value => panic!("Invalid TransactionResult value: {}", value),
}
}
}
fn status_as_u16(status: &TransactionResult) -> u16 {
match *status {
Ok(()) => TRANSACTION_STATUS_OK,
Err(ref e) => {
match e.error_type {
TransactionErrorType::Panic => TRANSACTION_STATUS_PANIC,
TransactionErrorType::Code(c) => u16::from(c),
}
}
}
}
pub trait TransactionSet
: Into<Box<Transaction>> + DeserializeOwned + Serialize + Clone {
fn tx_from_raw(raw: RawTransaction) -> Result<Self, encoding::Error>;
}
#[macro_export]
macro_rules! transactions {
{
$(#[$tx_set_attr:meta])*
$transaction_set:ident {
const SERVICE_ID = $service_id:expr;
$(
$(#[$tx_attr:meta])*
struct $name:ident {
$($def:tt)*
}
)*
}
} => {
messages! {
const SERVICE_ID = $service_id;
$(
$(#[$tx_attr])*
struct $name {
$($def)*
}
)*
}
#[derive(Clone, Debug)]
$($tx_set_attr)*
enum $transaction_set {
$($name($name),)*
}
transactions!(@implement $transaction_set, $($name)*);
};
{
$(#[$tx_set_attr:meta])*
pub $transaction_set:ident {
const SERVICE_ID = $service_id:expr;
$(
$(#[$tx_attr:meta])*
struct $name:ident {
$($def:tt)*
}
)*
}
} => {
messages! {
const SERVICE_ID = $service_id;
$(
$(#[$tx_attr])*
struct $name {
$($def)*
}
)*
}
#[derive(Clone, Debug)]
$($tx_set_attr)*
pub enum $transaction_set {
$($name($name),)*
}
transactions!(@implement $transaction_set, $($name)*);
};
{
$(#[$tx_set_attr:meta])*
pub($($vis:tt)+) $transaction_set:ident {
const SERVICE_ID = $service_id:expr;
$(
$(#[$tx_attr:meta])*
struct $name:ident {
$($def:tt)*
}
)*
}
} => {
messages! {
const SERVICE_ID = $service_id;
$(
$(#[$tx_attr])*
struct $name {
$($def)*
}
)*
}
#[derive(Clone, Debug)]
$($tx_set_attr)*
pub($($vis)+) enum $transaction_set {
$($name($name),)*
}
transactions!(@implement $transaction_set, $($name)*);
};
(@implement $transaction_set:ident, $($name:ident)*) => {
impl $crate::blockchain::TransactionSet for $transaction_set {
fn tx_from_raw(
raw: $crate::messages::RawTransaction
) -> ::std::result::Result<Self, $crate::encoding::Error> {
let message_type = raw.message_type();
match message_type {
$(
<$name as $crate::messages::ServiceMessage>::MESSAGE_ID => {
let tx = $crate::messages::Message::from_raw(raw)?;
Ok($transaction_set::$name(tx))
}
)*
_ => return Err($crate::encoding::Error::IncorrectMessageType { message_type })
}
}
}
impl Into<Box<$crate::blockchain::Transaction>> for $transaction_set {
fn into(self) -> Box<$crate::blockchain::Transaction> {
match self {$(
$transaction_set::$name(tx) => Box::new(tx),
)*}
}
}
impl<'de> $crate::encoding::serialize::reexport::Deserialize<'de> for $transaction_set {
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
where
D: $crate::encoding::serialize::reexport::Deserializer<'de>,
{
use $crate::encoding::serialize::json::reexport::{Value, from_value};
use $crate::encoding::serialize::reexport::{DeError, Deserialize};
let value = <Value as Deserialize>::deserialize(deserializer)?;
let message_id: Value = value.get("message_id")
.ok_or(D::Error::custom("Can't get message_id from json"))?
.clone();
let message_id: u16 = from_value(message_id)
.map_err(|e| D::Error::custom(
format!("Can't deserialize message_id: {}", e)
))?;
match message_id {
$(
<$name as $crate::messages::ServiceMessage>::MESSAGE_ID =>
<$name as $crate::encoding::serialize::json::ExonumJsonDeserialize>
::deserialize(&value)
.map_err(|e| D::Error::custom(
format!("Can't deserialize a value: {}", e.description())
))
.map($transaction_set::$name),
)*
_ => Err(D::Error::custom(format!("invalid message_id: {}", message_id))),
}
}
}
impl $crate::encoding::serialize::reexport::Serialize for $transaction_set {
fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
where
S: $crate::encoding::serialize::reexport::Serializer,
{
use $crate::encoding::serialize::reexport::Serialize;
match self {$(
&$transaction_set::$name(ref tx) => Serialize::serialize(tx, serializer),
)*}
}
}
};
}
fn panic_description(any: &Box<Any + Send>) -> Option<String> {
if let Some(s) = any.downcast_ref::<&str>() {
Some(s.to_string())
} else if let Some(s) = any.downcast_ref::<String>() {
Some(s.clone())
} else if let Some(error) = any.downcast_ref::<Box<Error + Send>>() {
Some(error.description().to_string())
} else {
None
}
}
#[cfg(test)]
mod tests {
use futures::sync::mpsc;
use std::collections::BTreeMap;
use std::sync::Mutex;
use std::panic;
use super::*;
use crypto;
use blockchain::Blockchain;
use storage::{Database, MemoryDB, Entry};
use node::ApiSender;
use helpers::{ValidatorId, Height};
lazy_static! {
static ref EXECUTION_STATUS: Mutex<ExecutionResult> = Mutex::new(Ok(()));
}
#[test]
fn execution_error_new() {
let codes = [0, 1, 100, 255];
for &code in &codes {
let error = ExecutionError::new(code);
assert_eq!(code, error.code);
assert_eq!(None, error.description);
}
}
#[test]
fn execution_error_with_description() {
let values = [(0, ""), (1, "test"), (100, "error"), (255, "hello")];
for value in &values {
let error = ExecutionError::with_description(value.0, value.1.to_owned());
assert_eq!(value.0, error.code);
assert_eq!(value.1, error.description.unwrap());
}
}
#[test]
fn transaction_error_new() {
let values = [
(TransactionErrorType::Panic, None),
(TransactionErrorType::Panic, Some("panic")),
(TransactionErrorType::Code(0), None),
(TransactionErrorType::Code(1), Some("")),
(TransactionErrorType::Code(100), None),
(TransactionErrorType::Code(255), Some("error description")),
];
for value in &values {
let error = TransactionError::new(value.0, value.1.map(str::to_owned));
assert_eq!(value.0, error.error_type());
assert_eq!(value.1.as_ref().map(|d| d.as_ref()), error.description());
}
}
#[test]
fn errors_conversion() {
let execution_errors = [
ExecutionError::new(0),
ExecutionError::new(255),
ExecutionError::with_description(1, "".to_owned()),
ExecutionError::with_description(1, "Terrible failure".to_owned()),
];
for execution_error in &execution_errors {
let transaction_error: TransactionError = execution_error.clone().into();
assert_eq!(execution_error.description, transaction_error.description);
let code = match transaction_error.error_type {
TransactionErrorType::Code(c) => c,
_ => panic!("Unexpected transaction error type"),
};
assert_eq!(execution_error.code, code);
}
}
#[test]
fn transaction_results_round_trip() {
let results = [
Ok(()),
Err(TransactionError::panic(None)),
Err(TransactionError::panic(Some("".to_owned()))),
Err(TransactionError::panic(
Some("Panic error description".to_owned()),
)),
Err(TransactionError::code(0, None)),
Err(TransactionError::code(
0,
Some("Some error description".to_owned()),
)),
Err(TransactionError::code(1, None)),
Err(TransactionError::code(1, Some("".to_owned()))),
Err(TransactionError::code(100, None)),
Err(TransactionError::code(100, Some("just error".to_owned()))),
Err(TransactionError::code(254, None)),
Err(TransactionError::code(254, Some("e".to_owned()))),
Err(TransactionError::code(255, None)),
Err(TransactionError::code(
255,
Some("(Not) really long error description".to_owned()),
)),
];
for result in &results {
let bytes = result.clone().into_bytes();
let new_result = TransactionResult::from_bytes(Cow::Borrowed(&bytes));
assert_eq!(*result, new_result);
}
}
#[test]
fn error_discards_transaction_changes() {
let statuses = [
Err(ExecutionError::new(0)),
Err(ExecutionError::with_description(
0,
"Strange error".to_owned(),
)),
Err(ExecutionError::new(255)),
Err(ExecutionError::with_description(
255,
"Error description...".to_owned(),
)),
Ok(()),
];
let (_, sec_key) = crypto::gen_keypair();
let (blockchain, mut pool) = create_blockchain();
let db = Box::new(MemoryDB::new());
for (index, status) in statuses.iter().enumerate() {
let index = index as u64;
*EXECUTION_STATUS.lock().unwrap() = status.clone();
let transaction = TxResult::new(index, &sec_key);
pool.insert(
transaction.hash(),
Box::new(transaction.clone()) as Box<Transaction>,
);
let (_, patch) = blockchain.create_patch(
ValidatorId::zero(),
Height(index),
&[transaction.hash()],
&pool,
);
db.merge(patch).unwrap();
let mut fork = db.fork();
let entry = create_entry(&mut fork);
if status.is_err() {
assert_eq!(None, entry.get());
} else {
assert_eq!(Some(index), entry.get());
}
}
}
#[test]
fn str_panic() {
let static_str = "Static string (&str)";
let panic = make_panic(static_str);
assert_eq!(Some(static_str.to_string()), panic_description(&panic));
}
#[test]
fn string_panic() {
let string = "Owned string (String)".to_owned();
let error = make_panic(string.clone());
assert_eq!(Some(string), panic_description(&error));
}
#[test]
fn box_error_panic() {
let error: Box<Error + Send> = Box::new("e".parse::<i32>().unwrap_err());
let description = error.description().to_owned();
let error = make_panic(error);
assert_eq!(Some(description), panic_description(&error));
}
#[test]
fn unknown_panic() {
let error = make_panic(1);
assert_eq!(None, panic_description(&error));
}
fn make_panic<T: Send + 'static>(val: T) -> Box<Any + Send> {
panic::catch_unwind(panic::AssertUnwindSafe(|| panic!(val))).unwrap_err()
}
fn create_blockchain() -> (Blockchain, BTreeMap<Hash, Box<Transaction>>) {
let service_keypair = crypto::gen_keypair();
let api_channel = mpsc::channel(1);
(
Blockchain::new(
MemoryDB::new(),
Vec::new(),
service_keypair.0,
service_keypair.1,
ApiSender::new(api_channel.0),
),
BTreeMap::new(),
)
}
transactions! {
Transactions {
const SERVICE_ID = 1;
struct TxResult {
index: u64,
}
}
}
impl Transaction for TxResult {
fn verify(&self) -> bool {
true
}
fn execute(&self, fork: &mut Fork) -> ExecutionResult {
let mut entry = create_entry(fork);
entry.set(self.index());
EXECUTION_STATUS.lock().unwrap().clone()
}
}
fn create_entry(fork: &mut Fork) -> Entry<&mut Fork, u64> {
Entry::new("transaction_status_test", fork)
}
}