diff --git a/.circleci/config.yml b/.circleci/config.yml index 65162d3fe..35f059aac 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -54,6 +54,9 @@ jobs: done - store_test_results: path: ~/test-results + - store_artifacts: + path: ~/test-results + destination: test_results - persist_to_workspace: root: "." paths: @@ -80,6 +83,9 @@ jobs: command: cargo junit --name ~/test-results/${CIRCLE_JOB}/results.xml - store_test_results: path: ~/test-results + - store_artifacts: + path: ~/test-results + destination: test_results test_nightly: docker: - image: rustlang/rust:nightly @@ -102,6 +108,9 @@ jobs: command: cargo junit --name ~/test-results/${CIRCLE_JOB}/results.xml - store_test_results: path: ~/test-results + - store_artifacts: + path: ~/test-results + destination: test_results coverage: docker: - image: ragnaroek/kcov:v33 diff --git a/Cargo.toml b/Cargo.toml index 3af8b6419..54fad96da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "chain-storage-sqlite", "chain-impl-mockchain", "cardano", + "network-grpc", "protocol", "protocol-tokio", "storage-units", diff --git a/cardano/src/block/block.rs b/cardano/src/block/block.rs index 3501bb149..80133c558 100644 --- a/cardano/src/block/block.rs +++ b/cardano/src/block/block.rs @@ -80,6 +80,8 @@ pub enum BlockHeader { MainBlockHeader(normal::BlockHeader), } +impl core::property::Header for BlockHeader {} + /// BlockHeaders is a vector of block headers, as produced by /// MsgBlocks. #[derive(Debug, Clone)] @@ -204,6 +206,8 @@ impl fmt::Display for Block { impl chain_core::property::Block for Block { type Id = HeaderHash; + type Date = BlockDate; + type Header = BlockHeader; fn id(&self) -> Self::Id { self.get_header().compute_hash() @@ -213,14 +217,28 @@ impl chain_core::property::Block for Block { self.get_header().get_previous_header() } - type Date = BlockDate; - fn date(&self) -> Self::Date { - self.get_header().get_blockdate() + match self { + Block::MainBlock(ref block) => block.header.consensus.slot_id.into(), + Block::BoundaryBlock(ref block) => block.header.consensus.epoch.into(), + } + } + + fn header(&self) -> BlockHeader { + self.get_header() } } -impl chain_core::property::Serializable for Block { +impl core::property::HasTransaction for Block { + fn transactions<'a>(&'a self) -> std::slice::Iter<'a, TxAux> { + match self { + &Block::BoundaryBlock(_) => [].iter(), + &Block::MainBlock(ref blk) => blk.body.tx.iter(), + } + } +} + +impl core::property::Serialize for Block { type Error = cbor_event::Error; fn serialize(&self, mut writer: W) -> Result<(), Self::Error> { @@ -228,6 +246,10 @@ impl chain_core::property::Serializable for Block { writer.write(&bytes)?; Ok(()) } +} + +impl core::property::Deserialize for Block { + type Error = cbor_event::Error; fn deserialize(reader: R) -> Result { Deserialize::deserialize(&mut Deserializer::from(reader)) diff --git a/cardano/src/block/date.rs b/cardano/src/block/date.rs index 83ac1a700..47008d5a2 100644 --- a/cardano/src/block/date.rs +++ b/cardano/src/block/date.rs @@ -1,5 +1,7 @@ use super::types::{EpochId, EpochSlotId, SlotId}; +use chain_core::property; + use std::{ cmp::{Ord, Ordering}, error::Error, @@ -16,6 +18,9 @@ pub enum BlockDate { Boundary(EpochId), Normal(EpochSlotId), } + +impl property::BlockDate for BlockDate {} + impl ::std::ops::Sub for BlockDate { type Output = usize; fn sub(self, rhs: Self) -> Self::Output { diff --git a/cardano/src/block/types.rs b/cardano/src/block/types.rs index 3ee7b4c15..445aedcef 100644 --- a/cardano/src/block/types.rs +++ b/cardano/src/block/types.rs @@ -1,5 +1,6 @@ use super::normal::SscPayload; use cbor_event::{self, de::Deserializer, se::Serializer}; +use chain_core::property; use hash::Blake2b256; use util::try_from_slice::TryFromSlice; @@ -47,6 +48,9 @@ impl HeaderHash { self.0.as_hash_bytes() } } + +impl property::BlockId for HeaderHash {} + impl fmt::Display for HeaderHash { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) diff --git a/cardano/src/lib.rs b/cardano/src/lib.rs index 52bc6d1b3..8b6ad2d12 100644 --- a/cardano/src/lib.rs +++ b/cardano/src/lib.rs @@ -15,11 +15,14 @@ //! #![cfg_attr(feature = "with-bench", feature(test))] +extern crate chain_core; + #[cfg(feature = "generic-serialization")] #[macro_use] extern crate serde_derive; #[cfg(feature = "generic-serialization")] extern crate serde; + #[cfg(test)] extern crate serde_json; #[cfg(test)] diff --git a/cardano/src/tx.rs b/cardano/src/tx.rs index 4747f0249..91eaa5d7b 100644 --- a/cardano/src/tx.rs +++ b/cardano/src/tx.rs @@ -11,7 +11,15 @@ use std::{ io::{BufRead, Write}, }; -use hash::Blake2b256; +use crate::{ + address::{AddrType, Attributes, ExtendedAddr, SpendingData}, + coin::{self, Coin}, + config::ProtocolMagic, + hash::Blake2b256, + hdwallet::{Signature, XPrv, XPub, SIGNATURE_SIZE, XPUB_SIZE}, + merkle, redeem, + tags::SigningTag, +}; use cbor_event::{self, de::Deserializer, se::Serializer}; use config::ProtocolMagic; @@ -29,6 +37,9 @@ use chain_core; // given Tx, or a hash of a redeem address. pub type TxId = Blake2b256; +// FIXME: This is dodgy because TxId is not currently a dedicated type. +impl property::TransactionId for TxId {} + pub fn redeem_pubkey_to_txid( pubkey: &redeem::PublicKey, protocol_magic: ProtocolMagic, @@ -624,7 +635,7 @@ impl cbor_event::de::Deserialize for TxProof { } } -impl chain_core::property::Serializable for Tx { +impl chain_core::property::Serialize for Tx { type Error = cbor_event::Error; fn serialize(&self, writer: W) -> Result<(), Self::Error> { @@ -633,13 +644,18 @@ impl chain_core::property::Serializable for Tx { serializer.finalize(); Ok(()) } +} + +impl chain_core::property::Deserialize for Tx { + type Error = cbor_event::Error; fn deserialize(reader: R) -> Result { let mut deserializer = cbor_event::de::Deserializer::from(reader); deserializer.deserialize::() } } -impl chain_core::property::Serializable for TxAux { + +impl chain_core::property::Serialize for TxAux { type Error = cbor_event::Error; fn serialize(&self, writer: W) -> Result<(), Self::Error> { @@ -648,6 +664,10 @@ impl chain_core::property::Serializable for TxAux { serializer.finalize(); Ok(()) } +} + +impl chain_core::property::Deserialize for TxAux { + type Error = cbor_event::Error; fn deserialize(reader: R) -> Result { let mut deserializer = cbor_event::de::Deserializer::from(reader); diff --git a/chain-core/src/property.rs b/chain-core/src/property.rs index 0499a466c..4d2388116 100644 --- a/chain-core/src/property.rs +++ b/chain-core/src/property.rs @@ -38,6 +38,29 @@ use std::fmt::Debug; use std::hash::Hash; +/// Trait identifying the block identifier type. +pub trait BlockId: Eq + Ord + Clone + Debug + Hash + AsRef<[u8]> { + // FIXME: constant representing id length? + + /// Construct a BlockId from a slice. FIXME: use TryFromSlice trait. + fn try_from_slice(slice: &[u8]) -> Option; +} + +/// A trait representing block dates. Dates can be compared, ordered +/// and serialized as integers. +pub trait BlockDate: Eq + Ord + Clone { + fn serialize(&self) -> u64; + fn deserialize(n: u64) -> Self; +} + +/// Trait identifying the transaction identifier type. +pub trait TransactionId: Eq + Hash {} + +/// Trait identifying the block header type. +pub trait Header {} + +impl Header for () {} + /// Block property /// /// a block is part of a chain of block called Blockchain. @@ -48,7 +71,7 @@ use std::hash::Hash; /// recent block to the furthest/oldest block. /// /// The Oldest block is called the Genesis Block. -pub trait Block: Serializable { +pub trait Block: Serialize + Deserialize { /// the Block identifier. It must be unique. This mean that /// 2 different blocks have 2 different identifiers. /// @@ -62,6 +85,13 @@ pub trait Block: Serializable { /// identifying the position of a block in a given epoch or era. type Date: BlockDate; + /// The block header. If provided by the blockchain, the header + /// can be used to transmit block's metadata via a network protocol + /// or in other uses where the full content of the block is not desirable. + /// An implementation that does not feature headers can use the unit + /// type `()`. + type Header: Header; + /// return the Block's identifier. fn id(&self) -> Self::Id; @@ -71,34 +101,23 @@ pub trait Block: Serializable { /// get the block date of the block fn date(&self) -> Self::Date; -} - -pub trait BlockId: Eq + Ord + Clone + Debug + Hash + AsRef<[u8]> { - // FIXME: constant representing id length? - /// Construct a BlockId from a slice. FIXME: use TryFromSlice trait. - fn try_from_slice(slice: &[u8]) -> Option; -} - -/// A trait representing block dates. Dates can be compared, ordered -/// and serialized as integers. -pub trait BlockDate: Eq + Ord + Clone { - fn serialize(&self) -> u64; - fn deserialize(n: u64) -> Self; + /// Gets the block's header. + fn header(&self) -> Self::Header; } /// define a transaction within the blockchain. This transaction can be used /// for the UTxO model. However it can also be used for any other elements that /// the blockchain has (a transaction type to add Stacking Pools and so on...). /// -pub trait Transaction: Serializable { +pub trait Transaction: Serialize + Deserialize { /// the input type of the transaction (if none use `()`). type Input; /// the output type of the transaction (if none use `()`). type Output; /// a unique identifier of the transaction. For 2 different transactions /// we must have 2 different `Id` values. - type Id; + type Id: TransactionId; fn inputs<'a>(&'a self) -> std::slice::Iter<'a, Self::Input>; fn outputs<'a>(&'a self) -> std::slice::Iter<'a, Self::Output>; @@ -213,15 +232,12 @@ pub trait LeaderSelection { fn is_leader_at(&self, date: ::Date) -> Result; } -/// Define that an object can be written in a `Write` object or read from the -/// `Read` object. -pub trait Serializable: Sized { +/// Define that an object can be written to a `Write` object. +pub trait Serialize { type Error: std::error::Error + From; fn serialize(&self, writer: W) -> Result<(), Self::Error>; - fn deserialize(reader: R) -> Result; - /// Convenience method to serialize into a byte vector. fn serialize_as_vec(&self) -> Result, Self::Error> { let mut data = vec![]; @@ -230,23 +246,36 @@ pub trait Serializable: Sized { } } +/// Define that an object can be read from a `Read` object. +pub trait Deserialize: Sized { + type Error: std::error::Error + From; + + fn deserialize(reader: R) -> Result; +} + +impl Serialize for &T { + type Error = T::Error; + + fn serialize(&self, writer: W) -> Result<(), T::Error> { + (**self).serialize(writer) + } +} + #[cfg(feature = "property-test-api")] pub mod testing { use super::*; use quickcheck::{Arbitrary, Gen}; - use std::io::Cursor; /// test that any arbitrary given object can serialize and deserialize /// back into itself (i.e. it is a bijection, or a one to one match /// between the serialized bytes and the object) pub fn serialization_bijection(t: T) -> bool where - T: Arbitrary + Serializable + Eq, + T: Arbitrary + Serialize + Deserialize + Eq, { let mut vec = Vec::new(); t.serialize(&mut vec).unwrap(); - let cursor = Cursor::new(vec); - let decoded_t = ::deserialize(cursor).unwrap(); + let decoded_t = T::deserialize(&mut &vec[..]).unwrap(); decoded_t == t } diff --git a/chain-impl-mockchain/src/block.rs b/chain-impl-mockchain/src/block.rs index 93e4643de..fc8398519 100644 --- a/chain-impl-mockchain/src/block.rs +++ b/chain-impl-mockchain/src/block.rs @@ -1,6 +1,7 @@ //! Representation of the block in the mockchain. use crate::key::*; use crate::transaction::*; +use bincode; use chain_core::property; /// Non unique identifier of the transaction position in the @@ -32,6 +33,7 @@ pub struct Block { impl property::Block for Block { type Id = Hash; type Date = SlotId; + type Header = (); /// Identifier of the block, currently the hash of the /// serialized transaction. @@ -49,9 +51,23 @@ impl property::Block for Block { fn date(&self) -> Self::Date { self.slot_id } + + fn header(&self) -> Self::Header { + () + } } -impl chain_core::property::Serializable for Block { +impl property::Serialize for Block { + // FIXME: decide on appropriate format for mock blockchain + + type Error = bincode::Error; + + fn serialize(&self, writer: W) -> Result<(), Self::Error> { + bincode::serialize_into(writer, self) + } +} + +impl property::Deserialize for Block { // FIXME: decide on appropriate format for mock blockchain type Error = bincode::Error; @@ -59,15 +75,88 @@ impl chain_core::property::Serializable for Block { fn deserialize(reader: R) -> Result { bincode::deserialize_from(reader) } +} + +impl property::HasTransaction for Block { + fn transactions<'a>(&'a self) -> std::slice::Iter<'a, SignedTransaction> { + self.transactions.iter() + } +} + +/// `Block` is an element of the blockchain it contains multiple +/// transaction and a reference to the parent block. Alongside +/// with the position of that block in the chain. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct SignedBlock { + /// Internal block. + block: Block, + /// Public key used to sign the block. + public_key: PublicKey, + /// List of cryptographic signatures that verifies the block. + signature: Signature, +} + +impl SignedBlock { + /// Create a new signed block. + pub fn new(block: Block, pkey: PrivateKey) -> Self { + use chain_core::property::Block; + let block_id = block.id(); + SignedBlock { + block: block, + public_key: pkey.public(), + signature: pkey.sign(block_id.as_ref()), + } + } + + /// Verify if block is correctly signed by the key. + /// Return `false` if there is no such signature or + /// if it can't be verified. + pub fn verify(&self) -> bool { + use chain_core::property::Block; + let block_id = self.block.id(); + self.public_key.verify(block_id.as_ref(), &self.signature) + } +} + +impl property::Serialize for SignedBlock { + type Error = bincode::Error; fn serialize(&self, writer: W) -> Result<(), Self::Error> { bincode::serialize_into(writer, self) } } -impl property::HasTransaction for Block { - fn transactions<'a>(&'a self) -> std::slice::Iter<'a, SignedTransaction> { - self.transactions.iter() +impl property::Deserialize for SignedBlock { + type Error = bincode::Error; + + fn deserialize(reader: R) -> Result { + bincode::deserialize_from(reader) + } +} + +impl property::Block for SignedBlock { + type Id = ::Id; + type Date = ::Date; + type Header = ::Header; + + /// Identifier of the block, currently the hash of the + /// serialized transaction. + fn id(&self) -> Self::Id { + self.block.id() + } + + fn header(&self) -> Self::Header { + self.block.header() + } + + /// Id of the parent block. + fn parent_id(&self) -> Self::Id { + self.block.parent_id() + } + + /// Date of the block. + fn date(&self) -> Self::Date { + self.block.date() } } @@ -77,6 +166,16 @@ mod test { use super::*; use quickcheck::{Arbitrary, Gen}; + quickcheck! { + fn block_serialization_bijection(b: Block) -> bool { + property::testing::serialization_bijection(b) + } + + fn signed_block_serialization_bijection(b: SignedBlock) -> bool { + property::testing::serialization_bijection(b) + } + } + impl Arbitrary for Block { fn arbitrary(g: &mut G) -> Self { Block { @@ -87,6 +186,16 @@ mod test { } } + impl Arbitrary for SignedBlock { + fn arbitrary(g: &mut G) -> Self { + SignedBlock { + block: Arbitrary::arbitrary(g), + public_key: Arbitrary::arbitrary(g), + signature: Arbitrary::arbitrary(g), + } + } + } + impl Arbitrary for SlotId { fn arbitrary(g: &mut G) -> Self { SlotId(Arbitrary::arbitrary(g), Arbitrary::arbitrary(g)) diff --git a/chain-impl-mockchain/src/environment.rs b/chain-impl-mockchain/src/environment.rs index f0b206a16..b07e41781 100644 --- a/chain-impl-mockchain/src/environment.rs +++ b/chain-impl-mockchain/src/environment.rs @@ -26,7 +26,7 @@ pub struct Environment { impl Environment { /// Create new environment. pub fn new() -> Self { - let g = StdGen::new(thread_rng(), 10); + let g = StdGen::new(rand::thread_rng(), 10); Environment { ledger: Ledger::new(HashMap::new()), gen: g, diff --git a/chain-impl-mockchain/src/error.rs b/chain-impl-mockchain/src/error.rs index c80a016c8..9ed049ca5 100644 --- a/chain-impl-mockchain/src/error.rs +++ b/chain-impl-mockchain/src/error.rs @@ -1,7 +1,7 @@ //! Errors taht may happen in the ledger and mockchain. use crate::transaction::*; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum Error { /// If the Ledger could not find the given input in the UTxO list it will /// report this error. @@ -36,6 +36,11 @@ pub enum Error { /// Transaction sum is not equal to zero, this means that we /// either generate or lose some money during the transaction. TransactionSumIsNonZero(u64, u64), + + /// Transaction does not have enough signatures. + /// First value represents number of inputs (required signatures) + /// Send value represents actual number of singatures. + NotEnoughSignatures(usize, usize), } impl std::fmt::Display for Error { @@ -49,6 +54,11 @@ impl std::fmt::Display for Error { Error::InvalidSignature(_, _, _) => write!(f, "Input is not signed properly"), Error::InvalidTxSignature(_) => write!(f, "Transaction was not signed"), Error::TransactionSumIsNonZero(_, _) => write!(f, "Transaction sum is non zero"), + Error::NotEnoughSignatures(required, actual) => write!( + f, + "Transaction has not enough signatures: {} out of {}", + actual, required + ), } } } diff --git a/chain-impl-mockchain/src/key.rs b/chain-impl-mockchain/src/key.rs index 6584deac8..9966d4774 100644 --- a/chain-impl-mockchain/src/key.rs +++ b/chain-impl-mockchain/src/key.rs @@ -39,6 +39,9 @@ impl PrivateKey { pub fn sign(&self, data: &[u8]) -> Signature { Signature(self.0.sign(data)) } + pub fn normalize_bytes(xprv: [u8; crypto::XPRV_SIZE]) -> Self { + PrivateKey(crypto::XPrv::normalize_bytes(xprv)) + } } /// Hash that is used as an address of the various components. diff --git a/chain-impl-mockchain/src/ledger.rs b/chain-impl-mockchain/src/ledger.rs index 91deb0380..5bd137a14 100644 --- a/chain-impl-mockchain/src/ledger.rs +++ b/chain-impl-mockchain/src/ledger.rs @@ -22,7 +22,7 @@ impl Ledger { } /// Diff of the ledger state. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct Diff { /// List of the outputs that were spent in the transaction. spent_outputs: HashMap, @@ -64,6 +64,14 @@ impl property::Ledger for Ledger { let mut diff = Diff::new(); let id = transaction.id(); + // 0. verify that number of signatures matches number of + // transactions + if transaction.tx.inputs.len() > transaction.witnesses.len() { + return Err(Error::NotEnoughSignatures( + transaction.tx.inputs.len(), + transaction.witnesses.len(), + )); + } // 1. validate transaction without looking into the context // and that each input is validated by the matching key. for (input, witness) in transaction @@ -140,6 +148,9 @@ impl property::Ledger for Ledger { mod test { use super::*; + use crate::address::Address; + use crate::key::{Hash, PrivateKey}; + use cardano::hdwallet as crypto; use quickcheck::{Arbitrary, Gen}; impl Arbitrary for Ledger { @@ -150,4 +161,68 @@ mod test { } } + #[test] + pub fn tx_no_witness() -> () { + use chain_core::property::Ledger; + let pk1 = PrivateKey::normalize_bytes([0; crypto::XPRV_SIZE]); + let user1_address = Address::new(&pk1.public()); + let tx0_id = TransactionId(Hash::hash_bytes(&[0])); + let utxo0 = UtxoPointer { + transaction_id: tx0_id, + output_index: 0, + }; + let ledger = crate::ledger::Ledger::new( + vec![(utxo0, Output(user1_address, Value(1)))] + .iter() + .cloned() + .collect(), + ); + let tx = Transaction { + inputs: vec![utxo0], + outputs: vec![Output(user1_address, Value(1))], + }; + let signed_tx = SignedTransaction { + tx: tx, + witnesses: vec![], + }; + assert_eq!( + Err(Error::NotEnoughSignatures(1, 0)), + ledger.diff_transaction(&signed_tx) + ) + } + + #[test] + pub fn tx_wrong_witness() -> () { + use chain_core::property::Ledger; + use chain_core::property::Transaction; + let pk1 = PrivateKey::normalize_bytes([0; crypto::XPRV_SIZE]); + let user1_address = Address::new(&pk1.public()); + let tx0_id = TransactionId(Hash::hash_bytes(&[0])); + let utxo0 = UtxoPointer { + transaction_id: tx0_id, + output_index: 0, + }; + let ledger = crate::ledger::Ledger::new( + vec![(utxo0, Output(user1_address, Value(1)))] + .iter() + .cloned() + .collect(), + ); + let output0 = Output(user1_address, Value(1)); + let tx = crate::transaction::Transaction { + inputs: vec![utxo0], + outputs: vec![output0], + }; + let pk2 = PrivateKey::normalize_bytes([1; crypto::XPRV_SIZE]); + let witness = Witness::new(tx.id(), &pk2); + let signed_tx = SignedTransaction { + tx: tx, + witnesses: vec![witness.clone()], + }; + assert_eq!( + Err(Error::InvalidSignature(utxo0, output0, witness)), + ledger.diff_transaction(&signed_tx) + ) + } + } diff --git a/chain-impl-mockchain/src/transaction.rs b/chain-impl-mockchain/src/transaction.rs index a8e8108bc..d6462cdd1 100644 --- a/chain-impl-mockchain/src/transaction.rs +++ b/chain-impl-mockchain/src/transaction.rs @@ -66,13 +66,15 @@ pub struct Output(pub Address, pub Value); /// Id of the transaction. #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)] -pub struct TransactionId(Hash); +pub struct TransactionId(pub Hash); impl AsRef<[u8]> for TransactionId { fn as_ref(&self) -> &[u8] { self.0.as_ref() } } +impl property::TransactionId for TransactionId {} + /// Transaction, transaction maps old unspent tokens into the /// set of the new addresses. #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] @@ -97,18 +99,22 @@ impl property::Transaction for Transaction { } } -impl property::Serializable for Transaction { +impl property::Serialize for Transaction { type Error = bincode::Error; - fn deserialize(reader: R) -> Result { - bincode::deserialize_from(reader) - } - fn serialize(&self, writer: W) -> Result<(), bincode::Error> { bincode::serialize_into(writer, self) } } +impl property::Deserialize for Transaction { + type Error = bincode::Error; + + fn deserialize(reader: R) -> Result { + bincode::deserialize_from(reader) + } +} + /// Each transaction must be signed in order to be executed /// by the ledger. `SignedTransaction` represents such a transaction. #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] @@ -117,18 +123,22 @@ pub struct SignedTransaction { pub witnesses: Vec, } -impl property::Serializable for SignedTransaction { +impl property::Serialize for SignedTransaction { type Error = bincode::Error; - fn deserialize(reader: R) -> Result { - bincode::deserialize_from(reader) - } - fn serialize(&self, writer: W) -> Result<(), Self::Error> { bincode::serialize_into(writer, self) } } +impl property::Deserialize for SignedTransaction { + type Error = bincode::Error; + + fn deserialize(reader: R) -> Result { + bincode::deserialize_from(reader) + } +} + impl property::Transaction for SignedTransaction { type Input = UtxoPointer; type Output = Output; diff --git a/module.nix b/module.nix deleted file mode 100644 index b62f5ecac..000000000 --- a/module.nix +++ /dev/null @@ -1,65 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; - -let - - build = import ./. { /* inherit pkgs; */ }; - - cfg = config.services.hermes; - -in - -{ - - options = { - - services.hermes = { - - port = mkOption { - type = types.int; - default = 3080; - description = "The TCP port on which Hermes listens for HTTP connections."; - }; - - networks = mkOption { - type = types.listOf types.str; - default = [ "mainnet" ]; - example = [ "testnet" ]; - description = "The networks to support."; - }; - - }; - - }; - - config = { - - environment.systemPackages = [ build ]; - - users.users.hermes = { - description = "Hermes server user"; - isSystemUser = true; - group = "hermes"; - }; - - users.groups.hermes = {}; - - system.activationScripts.hermes = stringAfter [ "users" ] - '' - install -d -m 0755 -o hermes -g hermes /var/lib/hermes - ''; - - systemd.services.hermes = { - description = "Hermes Web Service"; - after = [ "syslog.target" "network.target" ]; - wantedBy = [ "multi-user.target" ]; - serviceConfig = { - User = "hermes"; - ExecStart = "${build}/bin/hermes start --port ${toString cfg.port} --networks-dir /var/lib/hermes/networks --template ${toString cfg.networks}"; - }; - }; - - }; - -} diff --git a/network-grpc/Cargo.toml b/network-grpc/Cargo.toml new file mode 100644 index 000000000..7a1258592 --- /dev/null +++ b/network-grpc/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "network-grpc" +version = "0.1.0-dev" +authors = [ + "Nicolas Di Prima ", + "Vincent Hanquez ", + "Mikhail Zabaluev ", + "Eelco Dolstra ", + "Alexander Vershilov ", +] +edition = "2018" + +[dependencies] +chain-core = { path = "../chain-core" } +bytes = "0.4" +futures = "0.1" +prost = "0.4" +prost-derive = "0.4" +tokio = "0.1" +tokio-connect = { git = "https://github.com/carllerche/tokio-connect" } +tower-h2 = { git = "https://github.com/tower-rs/tower-h2" } +tower-util = { git = "https://github.com/tower-rs/tower" } + +[dependencies.tower-grpc] +git = "https://github.com/tower-rs/tower-grpc" +rev = "6f355226654ac5ad90209db3e308223c662248cb" + +[build-dependencies.tower-grpc-build] +git = "https://github.com/tower-rs/tower-grpc" +rev = "6f355226654ac5ad90209db3e308223c662248cb" diff --git a/network-grpc/build.rs b/network-grpc/build.rs new file mode 100644 index 000000000..90541aa7e --- /dev/null +++ b/network-grpc/build.rs @@ -0,0 +1,15 @@ +extern crate tower_grpc_build; + +use std::io::{stderr, Write}; +use std::process; + +fn main() { + tower_grpc_build::Config::new() + .enable_client(true) + .enable_server(true) + .build(&["proto/node.proto"], &["proto/"]) + .unwrap_or_else(|e| { + writeln!(stderr(), "{}", e).unwrap(); + process::exit(1) + }); +} diff --git a/network-grpc/proto/node.proto b/network-grpc/proto/node.proto new file mode 100644 index 000000000..8508b3226 --- /dev/null +++ b/network-grpc/proto/node.proto @@ -0,0 +1,95 @@ +syntax = "proto3"; + +package iohk.chain.node; + +message TipRequest {} + +message TipResponse { + bytes id = 1; + string blockdate = 2; +} + +// Parameters for GetBlocks and GetHeaders +message GetBlocksRequest { + // The id of the tip to trace the chain from. + bytes tip = 1; + // The offset back in the chain to start block or header retrieval from, + // going backwards in the chain. + uint64 offset = 2; + // Maximum number of blocks or headers to retrieve. + uint64 size = 3; +} + +message StreamBlocksToTipRequest { + // The ids of alternative chain forks to start the blocks from. + repeated bytes from = 1; +} + +// Representation of a block. +message Block { + // The entire content of the block. + bytes content = 1; +} + +// Representation of a block header. +message Header { + // The entire content of the block header. + bytes content = 1; +} + +message ProposeTransactionsRequest { + repeated bytes ids = 1; +} + +message ProposeTransactionsResponse { + // Status of each proposed transaction. + enum Status { + // The transaction ID is new to the recipient node. + NEW = 0; + // The proposed transaction has already been recorded by the node. + ALREADY_EXISTS = 1; + } + + message Item { + bytes id = 1; + Status status = 2; + } + + repeated Item items = 1; +} + +message RecordTransactionRequest { + bytes tx = 1; +} + +message RecordTransactionResponse { + // The result of processing a transaction. + enum Result { + // The transaction has been accepted. + ACCEPTED = 0; + // Unknown/internal error. + UNKNOWN_ERROR = 1; + // The signature is invalid. + INVALID_SIGNATURE = 2; + // The proposed transaction would result in a double spend. + DOUBLE_SPEND = 3; + // The proposed transaction has already been recorded by the node. + ALREADY_EXISTS = 4; + } + + Result result = 1; + bytes id = 2; +} + +service Node { + rpc Tip (TipRequest) returns (TipResponse); + rpc GetBlocks (GetBlocksRequest) returns (stream Block) { + option idempotency_level = NO_SIDE_EFFECTS; + } + rpc GetHeaders (GetBlocksRequest) returns (stream Header) { + option idempotency_level = NO_SIDE_EFFECTS; + } + rpc StreamBlocksToTip (StreamBlocksToTipRequest) returns (stream Block); + rpc ProposeTransactions (ProposeTransactionsRequest) returns (ProposeTransactionsResponse); + rpc RecordTransaction (RecordTransactionRequest) returns (RecordTransactionResponse); +} diff --git a/network-grpc/src/lib.rs b/network-grpc/src/lib.rs new file mode 100644 index 000000000..18cab019c --- /dev/null +++ b/network-grpc/src/lib.rs @@ -0,0 +1,20 @@ +extern crate chain_core; +extern crate prost; +#[macro_use] +extern crate prost_derive; +extern crate tokio_connect; +extern crate tower_grpc; +extern crate tower_h2; +extern crate tower_util; + +// Included generated protobuf/gRPC code, + +#[allow(dead_code)] +mod gen { + include!(concat!(env!("OUT_DIR"), "/iohk.chain.node.rs")); +} + +pub mod server; + +// TODO: replace with network_core crate +mod network_core; diff --git a/network-grpc/src/network_core.rs b/network-grpc/src/network_core.rs new file mode 100644 index 000000000..bedab9893 --- /dev/null +++ b/network-grpc/src/network_core.rs @@ -0,0 +1,57 @@ +use chain_core::property; + +use futures::prelude::*; + +use std::{collections::HashMap, fmt}; + +// NOTE: protobuf-derived definitions used in would-be abstract core API +use super::gen; + +/// Represents errors that can be returned by the node implementation. +#[derive(Debug)] +pub struct Error(); // TODO: define specific error variants and details + +impl std::error::Error for Error {} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "unknown network node error") + } +} + +pub struct ProposeTransactionsResponse { + // TODO: define fully + _items: HashMap, +} + +pub struct RecordTransactionResponse { + // TODO: define + _id: Id, + _result: gen::record_transaction_response::Result, +} + +pub trait Node { + type BlockId: property::BlockId; + type BlockDate: property::BlockDate; + type Block: property::Block; + type Header: property::Header; + + type TipFuture: Future; + type GetBlocksStream: Stream; + type GetBlocksFuture: Future; + type GetHeadersStream: Stream; + type GetHeadersFuture: Future; + type StreamBlocksToTipStream: Stream; + type StreamBlocksToTipFuture: Future; + type ProposeTransactionsFuture: Future< + Item = ProposeTransactionsResponse, + Error = Error, + >; + type RecordTransactionFuture: Future< + Item = RecordTransactionResponse, + Error = Error, + >; + + fn tip(&mut self) -> Self::TipFuture; + fn stream_blocks_to_tip(&mut self, from: &[Self::BlockId]) -> Self::StreamBlocksToTipFuture; +} diff --git a/network-grpc/src/server.rs b/network-grpc/src/server.rs new file mode 100644 index 000000000..3c25ee525 --- /dev/null +++ b/network-grpc/src/server.rs @@ -0,0 +1,301 @@ +use chain_core::property::{Block, BlockDate, BlockId, Deserialize, Header, Serialize}; + +use futures::prelude::*; +use tower_grpc::Error::Grpc as GrpcError; +use tower_grpc::{self, Code, Request, Status}; + +use std::marker::PhantomData; + +use super::gen; +use super::network_core; + +pub enum FutureResponse { + Pending(F), + Err(Status), + Complete(PhantomData), +} + +impl FutureResponse +where + F: Future + ConvertResponse, +{ + fn new(future: F) -> Self { + FutureResponse::Pending(future) + } +} + +impl FutureResponse { + fn error(status: Status) -> Self { + FutureResponse::Err(status) + } +} + +fn convert_error(e: network_core::Error) -> tower_grpc::Error { + let status = Status::with_code_and_message(Code::Unknown, format!("{}", e)); + GrpcError(status) +} + +pub trait ConvertResponse: Future { + fn convert_item(item: Self::Item) -> Result; +} + +pub trait ConvertStream: Stream { + fn convert_item(item: Self::Item) -> Result; +} + +fn poll_and_convert_response( + future: &mut F, +) -> Poll, tower_grpc::Error> +where + F: Future + ConvertResponse, +{ + match future.poll() { + Ok(Async::NotReady) => Ok(Async::NotReady), + Ok(Async::Ready(item)) => { + let item = F::convert_item(item)?; + let response = tower_grpc::Response::new(item); + Ok(Async::Ready(response)) + } + Err(e) => Err(convert_error(e)), + } +} + +fn poll_and_convert_stream(stream: &mut S) -> Poll, tower_grpc::Error> +where + S: Stream + ConvertStream, +{ + match stream.poll() { + Ok(Async::NotReady) => Ok(Async::NotReady), + Ok(Async::Ready(None)) => Ok(Async::Ready(None)), + Ok(Async::Ready(Some(item))) => { + let item = S::convert_item(item)?; + Ok(Async::Ready(Some(item))) + } + Err(e) => Err(convert_error(e)), + } +} + +impl Future for FutureResponse +where + F: Future + ConvertResponse, +{ + type Item = tower_grpc::Response; + type Error = tower_grpc::Error; + + fn poll(&mut self) -> Poll { + let res = match self { + FutureResponse::Pending(f) => poll_and_convert_response(f), + FutureResponse::Err(status) => Err(GrpcError(status.clone())), + FutureResponse::Complete(_) => panic!("polled a finished response"), + }; + if let Ok(Async::NotReady) = res { + Ok(Async::NotReady) + } else { + *self = FutureResponse::Complete(PhantomData); + res + } + } +} + +pub struct ResponseStream { + inner: S, + _phantom: PhantomData, +} + +impl ResponseStream +where + S: Stream + ConvertStream, +{ + pub fn new(stream: S) -> Self { + ResponseStream { + inner: stream, + _phantom: PhantomData, + } + } +} + +impl Stream for ResponseStream +where + S: Stream + ConvertStream, +{ + type Item = T; + type Error = tower_grpc::Error; + + fn poll(&mut self) -> Poll, tower_grpc::Error> { + poll_and_convert_stream(&mut self.inner) + } +} + +#[derive(Clone)] +pub struct GrpcServer { + node: T, +} + +fn deserialize_vec(pb: &[Vec]) -> Result, tower_grpc::Error> { + match pb.iter().map(|v| H::deserialize(&mut &v[..])).collect() { + Ok(v) => Ok(v), + Err(e) => { + // FIXME: log the error + // (preferably with tower facilities outside of this implementation) + let status = Status::with_code_and_message(Code::InvalidArgument, format!("{}", e)); + Err(GrpcError(status)) + } + } +} + +fn serialize_to_bytes(obj: T) -> Result, tower_grpc::Error> +where + T: Serialize, +{ + let mut bytes = Vec::new(); + match obj.serialize(&mut bytes) { + Ok(()) => Ok(bytes), + Err(_e) => { + // FIXME: log the error + let status = Status::with_code(Code::Unknown); + Err(GrpcError(status)) + } + } +} + +impl ConvertResponse> for F +where + F: Future, + S: Stream + ConvertStream, +{ + fn convert_item(item: S) -> Result, tower_grpc::Error> { + let stream = ResponseStream::new(item); + Ok(stream) + } +} + +impl ConvertResponse for F +where + F: Future, + I: BlockId + Serialize, + D: BlockDate + ToString, +{ + fn convert_item(item: (I, D)) -> Result { + let id = serialize_to_bytes(item.0)?; + let blockdate = item.1.to_string(); + let response = gen::TipResponse { id, blockdate }; + Ok(response) + } +} + +impl ConvertStream for S +where + S: Stream, + B: Block + Serialize, +{ + fn convert_item(item: Self::Item) -> Result { + let content = serialize_to_bytes(item)?; + Ok(gen::Block { content }) + } +} + +impl ConvertStream for S +where + S: Stream, + H: Header + Serialize, +{ + fn convert_item(item: Self::Item) -> Result { + let content = serialize_to_bytes(item)?; + Ok(gen::Header { content }) + } +} + +impl ConvertResponse for F +where + F: Future, Error = network_core::Error>, + I: BlockId + Serialize, +{ + fn convert_item( + _item: Self::Item, + ) -> Result { + unimplemented!(); + } +} + +impl ConvertResponse for F +where + F: Future, Error = network_core::Error>, + I: BlockId + Serialize, +{ + fn convert_item( + _item: Self::Item, + ) -> Result { + unimplemented!(); + } +} + +impl gen::server::Node for GrpcServer +where + T: network_core::Node + Clone, + ::BlockId: Serialize + Deserialize, + ::BlockDate: ToString, + ::Header: Serialize, +{ + type TipFuture = FutureResponse::TipFuture>; + type GetBlocksStream = ResponseStream::GetBlocksStream>; + type GetBlocksFuture = + FutureResponse::GetBlocksFuture>; + type GetHeadersStream = + ResponseStream::GetHeadersStream>; + type GetHeadersFuture = + FutureResponse::GetHeadersFuture>; + type StreamBlocksToTipStream = + ResponseStream::StreamBlocksToTipStream>; + type StreamBlocksToTipFuture = FutureResponse< + Self::StreamBlocksToTipStream, + ::StreamBlocksToTipFuture, + >; + type ProposeTransactionsFuture = FutureResponse< + gen::ProposeTransactionsResponse, + ::ProposeTransactionsFuture, + >; + type RecordTransactionFuture = FutureResponse< + gen::RecordTransactionResponse, + ::RecordTransactionFuture, + >; + + fn tip(&mut self, _request: Request) -> Self::TipFuture { + FutureResponse::new(self.node.tip()) + } + + fn get_blocks(&mut self, _request: Request) -> Self::GetBlocksFuture { + unimplemented!() + } + + fn get_headers(&mut self, _request: Request) -> Self::GetHeadersFuture { + unimplemented!() + } + + fn stream_blocks_to_tip( + &mut self, + req: Request, + ) -> Self::StreamBlocksToTipFuture { + let block_ids = match deserialize_vec(&req.get_ref().from) { + Ok(block_ids) => block_ids, + Err(GrpcError(status)) => { + return FutureResponse::error(status); + } + Err(e) => panic!("unexpected error {:?}", e), + }; + FutureResponse::new(self.node.stream_blocks_to_tip(&block_ids)) + } + + fn propose_transactions( + &mut self, + _request: Request, + ) -> Self::ProposeTransactionsFuture { + unimplemented!() + } + + fn record_transaction( + &mut self, + _request: Request, + ) -> Self::RecordTransactionFuture { + unimplemented!() + } +}