No description, website, or topics provided.
Switch branches/tags
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
backend
docker
frontend
LICENSE
README.md

README.md

Russian-post demo

This project demonstrates how to use simple blockchain in Russian-post based on Exonum blockchain.

Exonum blockchain keeps balances of users and handles secure transactions between them.

It implements most basic operations:

  • Create a new user
  • Add funds to the user's balance
  • Transfer funds between users
  • Issue user's funds
  • Prepare some funds for stamping
  • Accept preparation transaction

Configuration

Let's edit Cargo.toml in exonum configuration.

[workspace]
members = [
    "exonum",
    "testkit",
    "testkit/server",
    "services/configuration",
    "services/time",
    "examples/cryptocurrency",
    "examples/cryptocurrency-advanced/backend",
    "examples/timestamping/backend",
    "examples/russian-post/backend",
]
exclude = [ "exonum/fuzz" ]

Then set configuration in russian-post folder in Cargo.toml file.

[package]
name = "exonum-russian-post"
version = "0.9.0"
authors = ["The Exonum Team <exonum@bitfury.com>"]
homepage = "https://exonum.com/"
repository = "https://github.com/exonum/exonum"
readme = "README.md"
license = "Apache-2.0"
keywords = ["exonum", "blockchain", "example"]
categories = ["rust-patterns", "development-tools::testing"]
description = "Exonum blockchain example implementing a post office."

[badges]
travis-ci = { repository = "exonum/exonum" }
circle-ci = { repository = "exonum/exonum" }

[dependencies]
exonum = { version = "0.9.0", path = "../../../exonum" }
exonum-configuration = { version = "0.9.0", path = "../../../services/configuration" }
exonum-time = { version = "0.9.0", path = "../../../services/time" }
serde = "1.0.0"
serde_derive = "1.0.0"
failure = "=0.1.2"
serde_json = "1.0.24"
chrono = "0.4.5"

[dev-dependencies]
exonum-testkit = { version = "0.9.0", path = "../../../testkit" }
serde_json = "1.0.24"
pretty_assertions = "=0.5.1"
assert_matches = "1.2.0"

[api]
enable_blockchain_explorer = true

Wallet

Firstly, to describe user interaction we need to add wallet object, because the main feature is exchange of tokens

use exonum::crypto::{Hash, PublicKey};

encoding_struct! {
    /// Wallet information stored in the database.
    struct Wallet {
        pub_key:            &PublicKey,
        name:               &str,
        balance:            u64,
        history_len:        u64,
        history_hash:       &Hash,
        freezed_balance:    u64,
    }
}

pub_key is a field of the wallet holder, name is a field of the wallet name, balance is a field of the wallet balance, freezed_balance is a field of the wallet that cannot be spent (Used in MailPreparation Transaction). Two other fields show the information about history of the wallet.

impl Wallet {
    /// Returns a copy of this wallet with updated balance.
    pub fn set_balance(self, balance: u64, history_hash: &Hash, freezed_balance: u64) -> Self {
        Self::new(
            self.pub_key(),
            self.name(),
            balance,
            self.history_len() + 1,
            history_hash,
            freezed_balance,
        )
    }
}

Here we can set balance of the wallet.

Transactions

Now we have wallet. Let's define transactions, they will describe the interaction between users of the blockchain. First, we are using some imports, that will help us to use necessary files(DataBase, Macroses, DataTypes and etc.).

#![ allow( bare_trait_objects ) ]

extern crate serde_json;
extern crate serde;

use serde::{Deserialize, Serialize, Deserializer, Serializer};


use exonum::blockchain::{ExecutionError, ExecutionResult, Transaction};
use exonum::crypto::{CryptoHash, PublicKey, Hash};
use exonum::messages::Message;
use exonum::storage::Fork;
use exonum_time::schema::TimeSchema;

use POST_SERVICE_ID;
use schema::{CurrencySchema, TimestampEntry};

/// Error codes emitted by wallet transactions during execution.
#[derive(Debug, Fail)]
#[repr(u8)]
pub enum Error {
    /// Wallet already exists.
    ///
    /// Can be emitted by `CreateWallet`.
    #[fail(display = "Wallet already exists")]
    WalletAlreadyExists = 0,

    /// Sender doesn't exist.
    ///
    /// Can be emitted by `Transfer`.
    #[fail(display = "Sender doesn't exist")]
    SenderNotFound = 1,

    /// Receiver doesn't exist.
    ///
    /// Can be emitted by `Transfer` or `Issue`.
    #[fail(display = "Receiver doesn't exist")]
    ReceiverNotFound = 2,

    /// Insufficient currency amount.
    ///
    /// Can be emitted by `Transfer`.
    #[fail(display = "Insufficient currency amount")]
    InsufficientCurrencyAmount = 3,

    #[fail(display = "Time is up")]
    Timeisup = 4,

    #[fail(display = "Pubkey doesn`t belong to inspector")]
    NotInspector = 5,

    #[fail(display = "Pubkey doesn`t belong to issuer")]
    NotIssuer = 6,
}

impl From<Error> for ExecutionError {
    fn from(value: Error) -> ExecutionError {
        let description = format!("{}", value);
        ExecutionError::with_description(value as u8, description)
    }
}

transactions! {
    pub WalletTransactions {
        const SERVICE_ID = POST_SERVICE_ID;

        /// Transfer `amount` of the currency from one wallet to another.
        struct Transfer {
            from:    &PublicKey,
            to:      &PublicKey,
            amount:  u64,
            seed:    u64,
        }

        /// Issue `amount` of the currency to the `wallet`.
        struct Issue {
            pub_key:  &PublicKey,
            issuer_key: &PublicKey,
            amount:  u64,
            seed:    u64,
        }

        /// Create wallet with the given `name`. 1 - inspector, 0 - user, 2 - issuer
        struct CreateWallet {
            pub_key: &PublicKey,
            name:    &str,
            user_type: u64,
        }

        /// Prepare tokens for stamping 
        struct MailPreparation {
            meta: &str,
            pub_key: &PublicKey,
            amount: u64,
            seed: u64,
        }

        /// Accept or reject prepared tokens
        struct MailAcceptance {
            pub_key: &PublicKey,
            sender_key: &PublicKey,
            amount: u64,
            accept:  bool,
            seed: u64,
        }
        
        /// Cancel particular transaction
        struct Cancellation {
            pub_key: &PublicKey,
            sender: &PublicKey,
            tx_hash: &Hash,
        }
    }
}

impl Transaction for Issue {
    fn verify(&self) -> bool {
        self.verify_signature(self.issuer_key())
    }

    fn execute(&self, fork: &mut Fork) -> ExecutionResult {
        let time = TimeSchema::new(&fork)
            .time()
            .get();
        let mut schema = CurrencySchema :: new(fork);
        let pub_key = self.pub_key();
        if !schema.issuers().contains(self.issuer_key()) {
        	Err(Error::NotIssuer)?
        }
        if let Some(wallet) = schema.wallet(pub_key) {
            let amount = self.amount();
            schema.increase_wallet_balance(wallet, amount, &self.hash(), 0);

            let entry = TimestampEntry::new(&self.hash(), time.unwrap());
            schema.add_timestamp(entry);

            Ok(())
        } else {
            Err(Error::ReceiverNotFound)?
        }
    }
}

impl Transaction for Transfer {
    fn verify(&self) -> bool {
        (self.from() != self.to()) && self.verify_signature(self.from())
    }

    fn execute(&self, fork: &mut Fork) -> ExecutionResult {
        let time = TimeSchema::new(&fork)
            .time()
            .get();
        
        let mut schema = CurrencySchema::new(fork);
        let from = self.from();
        let to = self.to();
        let hash = self.hash();
        let amount = self.amount();
        let freezed_balance = 0;

        let sender = schema.wallet(from).ok_or(Error :: SenderNotFound)?;
        let receiver = schema.wallet(to).ok_or(Error :: ReceiverNotFound)?;

        if sender.balance() < amount {
            Err(Error::InsufficientCurrencyAmount)?;

        }

        schema.decrease_wallet_balance(sender, amount, &hash, freezed_balance);
        schema.increase_wallet_balance(receiver, amount, &hash, freezed_balance);
        
        let entry = TimestampEntry::new(&self.hash(), time.unwrap());
        schema.add_timestamp(entry);
        Ok(())
    }
}

impl Transaction for CreateWallet {
    fn verify(&self) -> bool {
        self.verify_signature(self.pub_key())
    }

    fn execute(&self, fork: &mut Fork) -> ExecutionResult {
        let time = TimeSchema::new(&fork)
            .time()
            .get();
        let mut schema = CurrencySchema::new(fork);
        let pub_key = self.pub_key();
        let hash = self.hash();
        if schema.wallet(pub_key).is_none(){
            let name = self.name();
            let freezed_balance = 0;
            schema.create_wallet(pub_key, name, &hash, freezed_balance);

            let entry = TimestampEntry::new(&self.hash(), time.unwrap());
            schema.add_timestamp(entry);
            schema.add_inspector(pub_key, self.user_type());
            schema.add_issuer(pub_key, self.user_type());
            Ok(())
        } else {
            Err(Error::WalletAlreadyExists)?
        } 
    }
}

impl Transaction for MailPreparation {
    fn verify(&self) -> bool {
        self.verify_signature(self.pub_key())
    }

    fn execute(&self, fork: &mut Fork) -> ExecutionResult {
        let time = TimeSchema::new(&fork)
            .time()
            .get();
        let mut schema = CurrencySchema :: new(fork);
        let pub_key = self.pub_key();
        let amount = self.amount();
        let hash = self.hash();
        let sender = schema.wallet(pub_key).ok_or(Error :: SenderNotFound)?;
        if sender.balance() < amount {
            Err(Error::InsufficientCurrencyAmount)?;
        }
        schema.decrease_wallet_balance(sender, amount, &hash, amount);
        let entry = TimestampEntry::new(&self.hash(), time.unwrap());
        schema.add_timestamp(entry);
        Ok(())
    }
}

impl Transaction for MailAcceptance {
    fn verify(&self) -> bool {
        self.verify_signature(self.pub_key())
    }

    fn execute(&self, fork: &mut Fork) -> ExecutionResult {
        let time = TimeSchema::new(&fork)
            .time()
            .get();
        let mut schema = CurrencySchema :: new(fork);
        let sender_key = self.sender_key();
        let accept = self.accept();
        let hash = self.hash();
        if !schema.inspectors().contains(self.pub_key()) {
        	Err(Error::NotInspector)?
        }
        let sender = schema.wallet(sender_key).ok_or(Error :: SenderNotFound)?;
        if accept {
            let freezed_balance = 0;
            schema.decrease_wallet_balance(sender, freezed_balance, &hash, freezed_balance);
        } else {
            let amount = sender.freezed_balance();
            let freezed_balance = 0;
            schema.increase_wallet_balance(sender, amount, &hash, freezed_balance);
        }
        let entry = TimestampEntry::new(&self.hash(), time.unwrap());
        schema.add_timestamp(entry);
        Ok(())
    }
}

impl Transaction for Cancellation {
    fn verify(&self) -> bool {
        self.verify_signature(self.pub_key())
    }

    fn execute(&self, fork: &mut Fork) -> ExecutionResult {
        let n = 100000;
        let time = TimeSchema::new(&fork)
            .time()
            .get()
            .unwrap();
        let mut schema = CurrencySchema :: new(fork);
        let tx_hash = self.tx_hash();
        let hash = self.hash();
        if !schema.inspectors().contains(self.pub_key()) {
        	Err(Error::NotInspector)?
        }
        let tx_time = schema.timestamps().get(&tx_hash).unwrap();
        if time.timestamp() - tx_time < n {
            let raw_tx = match schema.transactions().get(&tx_hash) {
            	Some(x) => x,
            	None => panic!("Transaction not found!"),
            };
            let id = raw_tx.message_type();
            match id {
                0 => {
                	let transaction: Transfer = Message::from_raw(raw_tx.clone()).unwrap();
	                let from = transaction.from();
	                let to = transaction.to();
	                let amount = transaction.amount();
	                let wallet_from = schema.wallet(&from).ok_or(Error :: SenderNotFound)?;
	                let wallet_to = schema.wallet(to).ok_or(Error :: ReceiverNotFound)?;
	                schema.decrease_wallet_balance(wallet_to, amount, &tx_hash, 0);
	                schema.increase_wallet_balance(wallet_from, amount, &tx_hash, 0);
	            },
	            1 => {
	            	let transaction: Issue = Message::from_raw(raw_tx.clone()).unwrap();
	                let pub_key = transaction.pub_key();
	                let amount = transaction.amount();
	                let sender = schema.wallet(&pub_key).ok_or(Error :: ReceiverNotFound)?;
	                schema.decrease_wallet_balance(sender, amount, &tx_hash, 0);
	              
	            },
	            3 => {
	                let transaction: MailPreparation = Message::from_raw(raw_tx.clone()).unwrap();
	                let pub_key = transaction.pub_key();
	                let amount = transaction.amount();
	                let sender = schema.wallet(&pub_key).ok_or(Error :: ReceiverNotFound)?;
	                schema.increase_wallet_balance(sender, amount, &hash, 0);
	               
	            },
                4 => {
                	let transaction: MailAcceptance = Message::from_raw(raw_tx.clone()).unwrap();
                	if transaction.accept() {
                    	let pub_key = transaction.sender_key();
                    	let amount = transaction.amount();
                    	let sender = schema.wallet(&pub_key).ok_or(Error :: ReceiverNotFound)?;
                    	schema.increase_wallet_balance(sender, amount, &hash, 0);
                    }
                    
                },
                _ => panic!("Transaction is not defined"),
       		};
       		let entry = TimestampEntry::new(&self.hash(), time);
        	schema.add_timestamp(entry);
       	} else {
       		Err(Error::Timeisup)?;
       	}
        Ok(())
    }
}

Transfer

Transfer transaction has 4 fields. The first one is from. This field contains sender's public key. The second one is to. This field contains recipient's public key. The third one is amount. It contains information "How many funds we are going to transfer". The last one is seed. This field is special, because we need it, to avoid repetition of the equal transactions.

Issue

Issue transaction has 4 fields. The first one is pub_key. This field contains public key of the wallet holder, whose wallet balance should be increased. The second one is issuer_key. This field contains public key of the issuer. The third one is amount. It contains information "How many funds we are going to issue". The last one is seed. This field is special, because we need it, to avoid repetition of the equal transactions.

Create Wallet

Create Wallet transaction has 2 fields. The first one is pub_key. This field contains public key of the wallet creator. The second one is name. This field contains the name of the wallet.

Mail Preparation

This kind of transactions describes proccess of the token stamping. If entity wants to stamp some tokens, he need this transaction. Mail Preparation transaction has 4 fields. The first one is meta. It contains information about stamping. F.e. "I would like to stamp 3000 tokens". The second one is pub_key. It contains information about entity public key. The third one is amount. This field is about the number of tokens that should be stamped. The last field is seed field.

Mail Acceptance

Only inspectors can accept or reject Mail Preparation transaction. To accept/reject Mail Preparation, they use Mail Acceptance Transaction. Mail Acceptance transaction has 5 fields. The first one is pub_key. It contains information about inspector public key. The second one is sender_key. It contains information about person public key who wants to stamp tokens. The third one is amount. This field is about the number of tokens that should be stamped. The fourth one is accept. This field is about inspector's decision (Accept or reject). The last field is seed field.

Cancellation

This kind of transaction needs three fields. The first one is pub_key. This field contains public key of the inspector. The second one is sender_key. This field contains the public key of the transaction creator. The last one is tx_hash. This field contains hash of transaction that should be cancelled.

Api

To work with our blockchain we need to proccess requests. Also we want to get/post some requests and see what's going on (transactions, wallet info and etc.).

use exonum::{
    api::{self, ServiceApiBuilder, ServiceApiState},
    blockchain::{self, BlockProof, Transaction, TransactionSet}, crypto::{Hash, PublicKey},
    helpers::Height, node::TransactionSend, storage::{ListProof, MapProof},
};

use transactions::WalletTransactions;
use wallet::Wallet;
use {CurrencySchema, POST_SERVICE_ID};

/// The structure describes the query parameters for the `get_wallet` endpoint.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub struct WalletQuery {
    /// Public key of the queried wallet.
    pub pub_key: PublicKey,
}

/// The structure returned by the REST API.
#[derive(Debug, Serialize, Deserialize)]
pub struct TransactionResponse {
    /// Hash of the transaction.
    pub tx_hash: Hash,
}

/// Proof of existence for specific wallet.
#[derive(Debug, Serialize, Deserialize)]
pub struct WalletProof {
    /// Proof to the whole database table.
    pub to_table: MapProof<Hash, Hash>,
    /// Proof to the specific wallet in this table.
    pub to_wallet: MapProof<PublicKey, Wallet>,
}

/// Wallet history.
#[derive(Debug, Serialize, Deserialize)]
pub struct WalletHistory {
    pub proof: ListProof<Hash>,
    pub transactions: Vec<WalletTransactions>,
}

/// Wallet information.
#[derive(Debug, Serialize, Deserialize)]
pub struct WalletInfo {
    pub block_proof: BlockProof,
    pub wallet_proof: WalletProof,
    pub wallet_history: Option<WalletHistory>,
}

// TODO: Add documentation. (ECR-1638)
/// Public service API description.
#[derive(Debug, Clone, Copy)]
pub struct CryptocurrencyApi;

impl CryptocurrencyApi {
    pub fn wallet_info(state: &ServiceApiState, query: WalletQuery) -> api::Result<WalletInfo> {
        let snapshot = state.snapshot();
        let general_schema = blockchain::Schema::new(&snapshot);
        let currency_schema = CurrencySchema::new(&snapshot);

        let max_height = general_schema.block_hashes_by_height().len() - 1;

        let block_proof = general_schema
            .block_and_precommits(Height(max_height))
            .unwrap();

        let to_table: MapProof<Hash, Hash> =
            general_schema.get_proof_to_service_table(POST_SERVICE_ID, 0);

        let to_wallet: MapProof<PublicKey, Wallet> =
            currency_schema.wallets().get_proof(query.pub_key);

        let wallet_proof = WalletProof {
            to_table,
            to_wallet,
        };

        let wallet = currency_schema.wallet(&query.pub_key);

        let wallet_history = wallet.map(|_| {
            let history = currency_schema.wallet_history(&query.pub_key);
            let proof = history.get_range_proof(0, history.len());

            let transactions: Vec<WalletTransactions> = history
                .iter()
                .map(|record| general_schema.transactions().get(&record).unwrap())
                .map(|raw| WalletTransactions::tx_from_raw(raw).unwrap())
                .collect::<Vec<_>>();

            WalletHistory {
                proof,
                transactions,
            }
        });

        Ok(WalletInfo {
            block_proof,
            wallet_proof,
            wallet_history,
        })
    }

    pub fn post_transaction(
        state: &ServiceApiState,
        query: WalletTransactions,
    ) -> api::Result<TransactionResponse> {
        let transaction: Box<dyn Transaction> = query.into();
        let tx_hash = transaction.hash();
        state.sender().send(transaction)?;
        Ok(TransactionResponse { tx_hash })
    }

    pub fn wire(builder: &mut ServiceApiBuilder) {
        builder
            .public_scope()
            .endpoint("v1/wallets/info", Self::wallet_info)
            .endpoint_mut("v1/wallets/transaction", Self::post_transaction);
    }
}

Schema

Now, we have api, wallet, transactions. To combine this files we need the storage. Let's do it.

use exonum::{
    crypto::{Hash, PublicKey}, storage::{Fork, ProofListIndex, ProofMapIndex, Snapshot, MapIndex},
    messages::{RawMessage},
};

use chrono::{DateTime, Utc};

use wallet::Wallet;
use INITIAL_BALANCE;


encoding_struct! {
    /// Timestamp entry.
    struct TimestampEntry {

        /// Hash of transaction.
        tx_hash: &Hash,

        /// Timestamp time.
        time: DateTime<Utc>,
    }
}



/// Database schema for the cryptocurrency.
#[derive(Debug)]
pub struct CurrencySchema<T> {
    view: T,
}

impl<T> AsMut<T> for CurrencySchema<T> {
    fn as_mut(&mut self) -> &mut T {
        &mut self.view
    }
}

impl<T> CurrencySchema<T>
where
    T: AsRef<dyn Snapshot>,
{
    /// Constructs schema from the database view.
    pub fn new(view: T) -> Self {
        CurrencySchema { view }
    }

    /// Returns `MerklePatriciaTable` with wallets.
    pub fn wallets(&self) -> ProofMapIndex<&T, PublicKey, Wallet> {
        ProofMapIndex::new("cryptocurrency.wallets", &self.view)
    }

    /// Returns history of the wallet with the given public key.
    pub fn wallet_history(&self, public_key: &PublicKey) -> ProofListIndex<&T, Hash> {
        ProofListIndex::new_in_family("cryptocurrency.wallet_history", public_key, &self.view)
    }

    /// Returns wallet for the given public key.
    pub fn wallet(&self, pub_key: &PublicKey) -> Option<Wallet> {
        self.wallets().get(pub_key)
    }

    /// Returns state hash of service database.
    pub fn state_hash(&self) -> Vec<Hash> {
        vec![self.wallets().merkle_root()]
    }

    /// Returns table that represents a map from transaction hash into raw transaction message.
    pub fn transactions(&self) -> MapIndex<&T, Hash, RawMessage> {
        MapIndex::new("core.transactions", &self.view)
    }

    /// Returns the `ProofMapIndex` of timestamps.
    pub fn timestamps(&self) -> ProofMapIndex<&T, Hash, i64> {
        ProofMapIndex::new("cryptocurrency.timestamps", &self.view)
    }

    /// Returns the state hash of the timestamping service.
    pub fn state_hash_timestamps(&self) -> Vec<Hash> {
        vec![self.timestamps().merkle_root()]
    }

    pub fn inspectors(&self) -> MapIndex<&T, PublicKey, u64>{
        MapIndex::new("cryptocurrency.inspectors_pubkey", &self.view)
    }

    pub fn issuers(&self) -> MapIndex<&T, PublicKey, u64>{
        MapIndex::new("cryptocurrency.issuers_pubkey", &self.view)
    }
}

/// Implementation of mutable methods.
impl<'a> CurrencySchema<&'a mut Fork> {
    /// Returns mutable `MerklePatriciaTable` with wallets.
    pub fn wallets_mut(&mut self) -> ProofMapIndex<&mut Fork, PublicKey, Wallet> {
        ProofMapIndex::new("cryptocurrency.wallets", &mut self.view)
    }

    /// Returns history for the wallet by the given public key.
    pub fn wallet_history_mut(
        &mut self,
        public_key: &PublicKey,
    ) -> ProofListIndex<&mut Fork, Hash> {
        ProofListIndex::new_in_family("cryptocurrency.wallet_history", public_key, &mut self.view)
    }

    /// Increase balance of the wallet and append new record to its history.
    ///
    /// Panics if there is no wallet with given public key.
    pub fn increase_wallet_balance(&mut self, wallet: Wallet, amount: u64, transaction: &Hash, freezed_balance: u64) {
        let wallet = {
            let mut history = self.wallet_history_mut(wallet.pub_key());
            history.push(*transaction);
            let history_hash = history.merkle_root();
            let balance = wallet.balance();
            /////////////////////////////
            wallet.set_balance(balance + amount, &history_hash, freezed_balance)
        };
        self.wallets_mut().put(wallet.pub_key(), wallet.clone());
    }

    /// Decrease balance of the wallet and append new record to its history.
    ///
    /// Panics if there is no wallet with given public key.
    pub fn decrease_wallet_balance(&mut self, wallet: Wallet, amount: u64, transaction: &Hash, freezed_balance: u64) {
        let wallet = {
            let mut history = self.wallet_history_mut(wallet.pub_key());
            history.push(*transaction);
            let history_hash = history.merkle_root();
            let balance = wallet.balance();
            wallet.set_balance(balance - amount, &history_hash, freezed_balance)
        };
        self.wallets_mut().put(wallet.pub_key(), wallet.clone());
    }

    /// Create new wallet and append first record to its history.
    pub fn create_wallet(&mut self, key: &PublicKey, name: &str, transaction: &Hash, freezed_balance: u64) {
        let wallet = {
            let mut history = self.wallet_history_mut(key);
            history.push(*transaction);
            let history_hash = history.merkle_root();
            let freezed_balance = 0;
            Wallet::new(key, name, INITIAL_BALANCE, history.len(), &history_hash, freezed_balance)
        };
        self.wallets_mut().put(key, wallet);
    }

    /// Returns mut table that represents a map from transaction hash into raw transaction message.
    pub fn transactions_mut(&mut self) -> MapIndex<&mut Fork, Hash, RawMessage> {
        MapIndex::new("core.transactions", &mut self.view)
    }

    /// Returns the mutable `ProofMapIndex` of timestamps.
    pub fn timestamps_mut(&mut self) -> ProofMapIndex<&mut Fork, Hash, i64> {
        ProofMapIndex::new("cryptocurrency.timestamps", &mut self.view)
    }

    /// Adds the timestamp entry to the database.
    pub fn add_timestamp(&mut self, timestamp_entry: TimestampEntry) {
        let tx_hash = timestamp_entry.tx_hash();
        let time = timestamp_entry.time();

        // Check that timestamp with given content_hash does not exist.
        if self.timestamps().contains(tx_hash) {
            return;
        }
        // Add timestamp
        self.timestamps_mut().put(tx_hash, time.timestamp());
    }
    pub fn inspectors_mut(&mut self) -> MapIndex<&mut Fork, PublicKey, u64> {
        MapIndex::new("cryptocurrency.inspectors_pubkey", &mut self.view)
    }
    pub fn add_inspector(&mut self, pub_key: &PublicKey, user: u64) {
        if self.inspectors().contains(pub_key) || user == 0 || user == 2{
            return;
        }

        self.inspectors_mut().put(&pub_key, user);
    }

    pub fn issuers_mut(&mut self) -> MapIndex<&mut Fork, PublicKey, u64>{
        MapIndex::new("cryptocurrency.issuers_pubkey", &mut self.view)
    }

    pub fn add_issuer(&mut self, pub_key: &PublicKey, user: u64) {
        if self.issuers().contains(pub_key) || user == 0 || user == 1{
            return;
        }

        self.issuers_mut().put(&pub_key, user);
    }
}

Now, we described all methods that we need in our database. So, now we can extract neccessary transactions, wallets and etc. Almost everything is ready to launch the blockchain.

Main

Main file is all we need to launch.

extern crate exonum;
extern crate exonum_configuration;
extern crate exonum_russian_post;
extern crate exonum_time;

use exonum::helpers::{self, fabric::NodeBuilder};
use exonum_configuration as configuration;
use exonum_russian_post as cryptocurrency;
use exonum_time::TimeServiceFactory;


fn main() {
    exonum::crypto::init();
    helpers::init_logger().unwrap();

    let node = NodeBuilder::new()
        .with_service(Box::new(configuration::ServiceFactory))
        .with_service(Box::new(TimeServiceFactory))
        .with_service(Box::new(cryptocurrency::ServiceFactory));
    node.run();
}

Ok, everything is ready. Let's run our project.

Install and run

Below you will find a step-by-step guide to starting the post-office service on 4 nodes on the local machine.

Build the project:

cd examples/russian-post/backend

cargo install

Generate template:

mkdir example

./exonum-russian-post generate-template example/common.toml --validators-count 4

Generate public and secrets keys for each node:

./exonum-russian-post generate-config example/common.toml  example/pub_1.toml example/sec_1.toml --peer-address 127.0.0.1:6331

./exonum-russian-post generate-config example/common.toml  example/pub_2.toml example/sec_2.toml --peer-address 127.0.0.1:6332

./exonum-russian-post generate-config example/common.toml  example/pub_3.toml example/sec_3.toml --peer-address 127.0.0.1:6333

./exonum-russian-post generate-config example/common.toml  example/pub_4.toml example/sec_4.toml --peer-address 127.0.0.1:6334

Finalize configs:

./exonum-russian-post finalize --public-api-address 0.0.0.0:8200 --private-api-address 0.0.0.0:8091 example/sec_1.toml example/node_1_cfg.toml --public-configs example/pub_1.toml example/pub_2.toml example/pub_3.toml example/pub_4.toml

./exonum-russian-post finalize --public-api-address 0.0.0.0:8201 --private-api-address 0.0.0.0:8092 example/sec_2.toml example/node_2_cfg.toml --public-configs example/pub_1.toml example/pub_2.toml example/pub_3.toml example/pub_4.toml

./exonum-russian-post finalize --public-api-address 0.0.0.0:8202 --private-api-address 0.0.0.0:8093 example/sec_3.toml example/node_3_cfg.toml --public-configs example/pub_1.toml example/pub_2.toml example/pub_3.toml example/pub_4.toml

./exonum-russian-post finalize --public-api-address 0.0.0.0:8203 --private-api-address 0.0.0.0:8094 example/sec_4.toml example/node_4_cfg.toml --public-configs example/pub_1.toml example/pub_2.toml example/pub_3.toml example/pub_4.toml

Run nodes:

./exonum-russian-post run --node-config example/node_1_cfg.toml --db-path example/db1 --public-api-address 0.0.0.0:8200

./exonum-russian-post run --node-config example/node_2_cfg.toml --db-path example/db2 --public-api-address 0.0.0.0:8201

./exonum-russian-post run --node-config example/node_3_cfg.toml --db-path example/db3 --public-api-address 0.0.0.0:8202

./exonum-russian-post run --node-config example/node_4_cfg.toml --db-path example/db4 --public-api-address 0.0.0.0:8203

Interaction

To interact with blockchain we will use .json files. There are the examples of Mail Preparation and Mail Acceptance transactions below.

Mail Preparation
{
  "body": {
    "amount": "11",
    "meta": "",
    "pub_key": "ae5a9a90348ca866e3d6f878c35f2130f9b6d4d8daabfadc7ff15c65c1994bf5",
    "seed": "0"
  },
  "message_id": 3,
  "protocol_version": 0,
  "service_id": 128,
  "signature": "89234fdb23065155e82e120f8bac5e21726107c968330e6e60ab6b9ff9b8a48a71b0fc0afab90118056a7a3510243db980179e53fe8f564ebc81608f1f87270e"
}
Mail Acceptance
{
  "body": {
    "accept": false,
    "amount": "11",
    "pub_key": "5f3eb2f672baab17a9a06de555672a019630818a8167b994066bc90d4d7efa77",
    "seed": "1",
    "sender_key": "ae5a9a90348ca866e3d6f878c35f2130f9b6d4d8daabfadc7ff15c65c1994bf5"
  },
  "message_id": 4,
  "protocol_version": 0,
  "service_id": 128,
  "signature": "3a6310285c4b82f1e14205018e64b28a4cb4fa79261a7e60483f0510b524752775ca12853835bc27b124819574f8351874d7ea95996b6211fe33ce27b76ccb01"
}

To send requests you may use Postman or curl. Other examples can be seen at https://github.com/korepkorep/russian-post/blob/master/backend/tests/examples_of_transactions

License

Cryptocurrency demo is licensed under the Apache License (Version 2.0). See LICENSE for details.