⬆️ ⬇️

How to create a blockchain project on Exonum: a quick guide

Exonum is an open source framework for building blockchain-based applications. It is focused on working with closed blockchains and is applicable in any area: FinTech, GovTech and LegalTech.



Today we will conduct a small review of the solution, as well as in the framework of the educational format, let's look at how to build a simple blockchain-based project on Exonum. You 'll find all the code below in the repository on GitHub.





/ Exonum. Your next step to blockchain / Exonum

')

Exonum in a nutshell



The Exonum framework was created specifically for the development of private blockchains. This is a system in which only a predefined group of nodes can create new blocks in the blockchain. It is based on the desire of Bitfury specialists to create a tool that would allow relatively simple to launch a system similar in properties to public blockchains (reliability, immutability of data, auditability, etc.), but it would be more convenient to maintain and maintain.



Unlike Ethereum, which is a virtual decentralized machine and runs simultaneously on many nodes around the world, the blockchain built on Exonum works exclusively on the computing power of the validator nodes that are interested in the operation of this system and will ensure its reliable operation.



The Exonum private blockchain deployed on pre-determined nodes at least excludes the possibility of its sudden hard fork, clogging of the transaction pool and other problems typical of open blockchains, and the node operators monitor its efficient operation: they update the rules for processing transactions, etc.



In addition, the implementation of smart contracts for Ethereum is highly dependent on fluctuations in the rate of cryptocurrency - the ether, which makes it unpredictable for use, for example, in government agencies that cannot pay for the execution of transactions in currencies that are in an unregulated "gray zone". In Exonum, such dependencies are absent in principle.



Finally, the Exonum-blockchain works significantly faster than public blockchains (Bitcoin, Ethereum, etc.), namely, it processes several thousand transactions per second against several dozen processed by the latter. The choice of strategy is due to the general tendency to create a large number of independent blockchains that would interact with each other through sidechain technologies, linking to public blockchains (anchorage), etc.



The main components of Exonum are: the Byzantine consensus, light clients, binding to bitcoin and services.



The system uses a special Byzantine consensus algorithm to synchronize data among nodes. It guarantees data integrity and correct execution of transactions even in the event of a failure of up to 1/3 of the nodes due to a malfunction or intentional malicious activity, while not requiring mining of blocks.



Speaking about the advantages of Exonum over existing analogues, we can note the developed data model (storage), which is an index containing dependencies on each other (in fact, tables) - they allow you to implement an effective data structure aimed at solving particular problems. Clients of such a blockchain can receive cryptographic evidence of the correctness of the downloaded data (Merkle trees), which are checked locally on the client’s machine and cannot be tampered with even by the Exonum node operator.



Light clients are network nodes that store only a small portion of the blockchain of interest. They allow you to interact with the blockchain using mobile applications or web browsers. Clients “communicate” with one or more services on a full-featured node through an API . The work of such thin clients is specific for each individual service and is implemented as difficult as the specific service requires.



The essence of the work of Exonum thin clients and the construction of evidence is that the end-user who has linked to the Bitcoin blockchain may not trust the private blockchain operator. But he can be sure that the data that he displays is obtained in accordance with the rules laid down in this particular private blockchain.



The security of light clients in Exonum, comparable to that provided by the permissionless-blockchain, is provided by the bitcoin binding already mentioned above, the so-called anchorage. The service periodically sends block hashes to the public bitcoin blockchain in the form of transaction testimonials. In this case, even if the Exonum blockchain stops working, the data can still be verified. Moreover, to attack such a network, attackers have to overcome the defense mechanisms of both blockchains, which requires enormous computing power.



Finally, services are the foundation of the Exonum framework. They resemble smart contracts on other platforms and contain the business logic of blockchain applications. But, in contrast to smart contracts, services in Exonum are not “locked” in the virtual machine and not containerized.



This makes them more efficient and flexible. However, this approach requires more caution when programming (service isolation is marked on the Exonum roadmap). Services define transaction processing rules as well as open access to these external clients .



After reviewing the main components, we can proceed to the analysis of the example.



Creating services in Exonum



On November 2, the release of Exonum 0.3 was released, and further guidance was written taking into account changes and improvements to the system (you can read about them in the repository on GitHub). We will create a block node with a single node that implements cryptocurrency. The network will accept two types of transactions: “create a wallet” and “transfer funds from one wallet to another”.



Exonum is written in Rust, so you need to install a compiler. For this you can use our guide .



Node creation


First, create a new crate :



cargo new --bin cryptocurrency 


And add the necessary dependencies to the created cargo.toml :



 [package] name = "cryptocurrency" version = "0.3.0" authors = ["Your Name <your@email.com>"] [dependencies] iron = "0.5.1" bodyparser = "0.7.0" router = "0.5.1" serde = "1.0" serde_json = "1.0" serde_derive = "1.0" exonum = "0.3.0" 


We import crate with the necessary types. To do this, edit the src / main.rs file :



 extern crate serde; extern crate serde_json; #[macro_use] extern crate serde_derive; #[macro_use] extern crate exonum; extern crate router; extern crate bodyparser; extern crate iron; use exonum::blockchain::{Blockchain, Service, GenesisConfig, ValidatorKeys, Transaction, ApiContext}; use exonum::node::{Node, NodeConfig, NodeApiConfig, TransactionSend, ApiSender }; use exonum::messages::{RawTransaction, FromRaw, Message}; use exonum::storage::{Fork, MemoryDB, MapIndex}; use exonum::crypto::{PublicKey, Hash, HexValue}; use exonum::encoding::{self, Field}; use exonum::api::{Api, ApiError}; use iron::prelude::*; use iron::Handler; use router::Router; 


Define constants:



 // Service identifier const SERVICE_ID: u16 = 1; // Identifier for wallet creation transaction type const TX_CREATE_WALLET_ID: u16 = 1; // Identifier for coins transfer transaction type const TX_TRANSFER_ID: u16 = 2; // Starting balance of a newly created wallet const INIT_BALANCE: u64 = 100; 


And the main function:



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


All this allows you to configure the logger, which will display information about the activity of Exonum nodes in the console.



To form the blockchain itself, you need to create an instance of the database (in our case, MemoryDB, however, you can use RocksDB) and declare a list of services . We place this code after logger initialization:



 let db = MemoryDB::new(); let services: Vec<Box<Service>> = vec![ ]; let blockchain = Blockchain::new(Box::new(db), services); 


In fact, the blockchain is ready, but it will not work to interact with it - we don’t have a node and an API to access it. The node will need to be configured . In the configuration, the list of public keys of validators is specified (in our case it will be one). In essence, each node requires two pairs of public and private keys: one for interacting with other nodes in the process of reaching consensus, and the second for services. For our example, we create temporary public keys with the exonum :: crypto :: gen_keypair () command and write them into the configuration file.



 let validator_keys = ValidatorKeys { consensus_key: consensus_public_key, service_key: service_public_key, }; let genesis = GenesisConfig::new(vec![validator_keys].into_iter()); 


Next, we configure the REST API for working with external web requests — to do this, open port 8000. Also, open port 2000 so that the full Exonum network nodes can communicate with each other.



 let api_address = "0.0.0.0:8000".parse().unwrap(); let api_cfg = NodeApiConfig { public_api_address: Some(api_address), ..Default::default() }; let peer_address = "0.0.0.0:2000".parse().unwrap(); // Complete node configuration let node_cfg = NodeConfig { listen_address: peer_address, peers: vec![], service_public_key, service_secret_key, consensus_public_key, consensus_secret_key, genesis, external_address: None, network: Default::default(), whitelist: Default::default(), api: api_cfg, mempool: Default::default(), services_configs: Default::default(), }; let node = Node::new(blockchain, node_cfg); node.run().unwrap(); 


We declare data


At this stage, we need to determine what data we want to store in the blockchain. In our case, this is information about the wallet and balance, the public key for checking requests from the wallet owner and the name of the owner. The structure will look like this:



 encoding_struct! { struct Wallet { const SIZE = 48; field pub_key: &PublicKey [00 => 32] field name: &str [32 => 40] field balance: u64 [40 => 48] } } 


Macro encoding_struct! helps to declare the structure being ordered and to define the boundaries of the value fields. We need to change the balance of the wallet, because we add methods to the Wallet :



 impl Wallet { pub fn increase(self, amount: u64) -> Self { let balance = self.balance() + amount; Self::new(self.pub_key(), self.name(), balance) } pub fn decrease(self, amount: u64) -> Self { let balance = self.balance() - amount; Self::new(self.pub_key(), self.name(), balance) } } 


You also need to create a key-value storage in MemoryDB. To do this, we use fork to be able, as a last resort, to roll back all changes.



 pub struct CurrencySchema<'a> { view: &'a mut Fork, } 


However, fork gives access to any information in the database. To isolate the wallets, add a unique prefix and use the MapIndex abstraction map .



 impl<'a> CurrencySchema<'a> { pub fn wallets(&mut self) -> MapIndex<&mut Fork, PublicKey, Wallet> { let prefix = blockchain::gen_prefix(SERVICE_ID, 0, &()); MapIndex::new("cryptocurrency.wallets", self.view) } // Utility method to quickly get a separate wallet from the storage pub fn wallet(&mut self, pub_key: &PublicKey) -> Option<Wallet> { self.wallets().get(pub_key) } } 


Determine the transaction


As already noted, for the work of our educational example we will need the following types of transactions : create a wallet and add funds to it, as well as transfer them to another wallet.



The transaction for creating a wallet must contain its public key and user name.



 message! { struct TxCreateWallet { const TYPE = SERVICE_ID; const ID = TX_CREATE_WALLET_ID; const SIZE = 40; field pub_key: &PublicKey [00 => 32] field name: &str [32 => 40] } } 


Before creating a wallet, we will check its uniqueness. We also credit 100 coins to it.



 impl Transaction for TxCreateWallet { fn verify(&self) -> bool { self.verify_signature(self.pub_key()) } fn execute(&self, view: &mut Fork) { let mut schema = CurrencySchema { view }; if schema.wallet(self.pub_key()).is_none() { let wallet = Wallet::new(self.pub_key(), self.name(), INIT_BALANCE); println!("Create the wallet: {:?}", wallet); schema.wallets().put(self.pub_key(), wallet) } } } 


The money transfer transaction looks like this:



 message! { struct TxTransfer { const TYPE = SERVICE_ID; const ID = TX_TRANSFER_ID; const SIZE = 80; field from: &PublicKey [00 => 32] field to: &PublicKey [32 => 64] field amount: u64 [64 => 72] field seed: u64 [72 => 80] } } 


It contains two public keys (for both wallets) and the number of coins that are transferred. The seed field has been added so that the transaction cannot be repeated. You also need to check that the sender does not send the funds to himself:



 impl Transaction for TxTransfer { fn verify(&self) -> bool { (*self.from() != *self.to()) && self.verify_signature(self.from()) } fn execute(&self, view: &mut Fork) { let mut schema = CurrencySchema { view }; let sender = schema.wallet(self.from()); let receiver = schema.wallet(self.to()); if let (Some(mut sender), Some(mut receiver)) = (sender, receiver) { let amount = self.amount(); if sender.balance() >= amount { let sender.decrease(amount); let receiver.increase(amount); println!("Transfer between wallets: {:?} => {:?}", sender, receiver); let mut wallets = schema.wallets(); wallets.put(self.from(), sender); wallets.put(self.to(), receiver); } } } } 


In order for the transactions to be correctly displayed in the blockchain block browser, we also need to override the `info ()` method. The implementation will be the same for both types of transactions and will look like this:



 impl Transaction for TxCreateWallet { // `verify()` and `execute()` code... fn info(&self) -> serde_json::Value { serde_json::to_value(&self) .expect("Cannot serialize transaction to JSON") } } 


Implement a transaction API


To do this, create a structure with a channel and an instance of the blockchain, which will be necessary to implement read requests:



 #[derive(Clone)] struct CryptocurrencyApi { channel: ApiSender, blockchain: Blockchain, } 


To simplify the processing of processes, add a TransactionRequest enum , which combines both types of transactions: “create a wallet” and “transfer funds”.



 #[serde(untagged)] #[derive(Clone, Serialize, Deserialize)] enum TransactionRequest { CreateWallet(TxCreateWallet), Transfer(TxTransfer), } impl Into<Box<Transaction>> for TransactionRequest { fn into(self) -> Box<Transaction> { match self { TransactionRequest::CreateWallet(trans) => Box::new(trans), TransactionRequest::Transfer(trans) => Box::new(trans), } } } #[derive(Serialize, Deserialize)] struct TransactionResponse { tx_hash: Hash, } 


It remains to "make friends" with our handler with the HTTP handler of the web server. To do this, we implement the wire method. In the example below, we add a handler that converts JSON input to Transaction.



 impl Api for CryptocurrencyApi { fn wire(&self, router: &mut Router) { let self_ = self.clone(); let tx_handler = move |req: &mut Request| -> IronResult<Response> { match req.get::<bodyparser::Struct<TransactionRequest>>() { Ok(Some(tx)) => { let tx: Box<Transaction> = tx.into(); let tx_hash = tx.hash(); self_.channel.send(tx).map_err(ApiError::from)?; let json = TransactionResponse { tx_hash }; self_.ok_response(&serde_json::to_value(&json).unwrap()) } Ok(None) => Err(ApiError::IncorrectRequest( "Empty request body".into()))?, Err(e) => Err(ApiError::IncorrectRequest(Box::new(e)))?, } }; // (Read request processing skipped) // Bind the transaction handler to a specific route. router.post("/v1/wallets/transaction", transaction, "transaction"); // (Read request binding skipped) } } 


Implement the read requests API


In order to be able to verify that transactions are actually executed, we implement two types of read requests: return information about all the system wallets and return information only about a specific purse that corresponds to the public key.



To do this, we define a couple of methods in CryptocurrencyApi that will access the blockchain field to read information from the blockchain's repository.



 impl CryptocurrencyApi { fn get_wallet(&self, pub_key: &PublicKey) -> Option<Wallet> { let mut view = self.blockchain.fork(); let mut schema = CurrencySchema { view: &mut view }; schema.wallet(pub_key) } fn get_wallets(&self) -> Option<Vec<Wallet>> { let mut view = self.blockchain.fork(); let mut schema = CurrencySchema { view: &mut view }; let idx = schema.wallets(); let wallets: Vec<Wallet> = idx.values().collect(); if wallets.is_empty() { None } else { Some(wallets) } } } 


It is worth paying attention to the fact that in this case we use the fork method, despite the fact that it gives access to write and read data (so as not to overload the example). In reality, it is advisable to use the read-only access format (referring to snapshots).



Further, as well as for transactions, we add processing of requests using get_wallets () and get_wallet () methods in CryptocurrencyApi :: wire () .



 impl Api for CryptocurrencyApi { fn wire(&self, router: &mut Router) { let self_ = self.clone(); // (Transaction processing skipped) // Gets status of all wallets in the database. let self_ = self.clone(); let wallets_info = move |_: &mut Request| -> IronResult<Response> { if let Some(wallets) = self_.get_wallets() { self_.ok_response(&serde_json::to_value(wallets).unwrap()) } else { self_.not_found_response( &serde_json::to_value("Wallets database is empty") .unwrap(), ) } }; // Gets status of the wallet corresponding to the public key. let self_ = self.clone(); let wallet_info = move |req: &mut Request| -> IronResult<Response> { // Get the hex public key as the last URL component; // return an error if the public key cannot be parsed. let path = req.url.path(); let wallet_key = path.last().unwrap(); let public_key = PublicKey::from_hex(wallet_key) .map_err(ApiError::FromHex)?; if let Some(wallet) = self_.get_wallet(&public_key) { self_.ok_response(&serde_json::to_value(wallet).unwrap()) } else { self_.not_found_response( &serde_json::to_value("Wallet not found").unwrap(), ) } }; // (Transaction binding skipped) // Bind read request endpoints. router.get("/v1/wallets", wallets_info, "wallets_info"); router.get("/v1/wallet/:pub_key", wallet_info, "wallet_info"); } 


We define the service


To turn the CurrencyService structure into a blockchain service, we need to assign the Service property to it. It has two methods: service_name , which returns the name of our service, and service_id , which returns its unique ID.

The tx_from_raw method will be used to deserialize transactions, and the public_api_handler method will be used to create a REST Handler for processing web requests to the site. It will apply the logic already defined in CryptocurrencyApi .



 impl Service for CurrencyService { fn service_name(&self) -> &'static str { "cryptocurrency" } fn service_id(&self) -> u16 { SERVICE_ID } fn tx_from_raw(&self, raw: RawTransaction) -> Result<Box<Transaction>, encoding::Error> { let trans: Box<Transaction> = match raw.message_type() { TX_TRANSFER_ID => Box::new(TxTransfer::from_raw(raw)?), TX_CREATE_WALLET_ID => Box::new(TxCreateWallet::from_raw(raw)?), _ => { return Err(encoding::Error::IncorrectMessageType { message_type: raw.message_type() }); }, }; Ok(trans) } fn public_api_handler(&self, ctx: &ApiContext) -> Option<Box<Handler>> { let mut router = Router::new(); let api = CryptocurrencyApi { channel: ctx.node_channel().clone(), blockchain: ctx.blockchain().clone(), }; api.wire(&mut router); Some(Box::new(router)) } } 


We implemented all parts of our mini-blockchain. Now it remains to add CryptocyrrencyService to the list of services of the blockchain and run the demo:



 let services: Vec<Box<Service>> = vec![ Box::new(CurrencyService), ]; cargo run 


Service Testing


Exonum allows you to test the work of services. For this package is used Sandbox - it simulates the network. We can send a request to the node and get an answer, and then observe the changes taking place in the blockchain. The sandbox instance is created by the sandbox_with_services method, which allows you to specify services for testing. For example, like this:



 let s = sandbox_with_services(vec![Box::new(CurrencyService::new()), Box::new(ConfigUpdateService::new())]); 


In general, Sandbox can simulate the process of receiving a message by a node, check which node sent it, and what was in it. Also, the “sandbox” can work over time, for example, to simulate the expiration of a time period.



Sending transactions


Now let's try sending multiple transactions in our blockchain demo. First, create a wallet. This is how the create-wallet-1.json file will look like:



 { "body": { "pub_key": "03e657ae71e51be60a45b4bd20bcf79ff52f0c037ae6da0540a0e0066132b472", "name": "Johnny Doe" }, "network_id": 0, "protocol_version": 0, "service_id": 1, "message_id": 1, "signature": "ad5efdb52e48309df9aa582e67372bb3ae67828c5eaa1a7a5e387597174055d315eaa7879912d0509acf17f06a23b7f13f242017b354f682d85930fa28240402" } 


Use the curl command to send a transaction over HTTP:



 curl -H "Content-Type: application/json" -X POST -d @create-wallet-1.json \ http://127.0.0.1:8000/api/services/cryptocurrency/v1/wallets/transaction 


After that, in the console we will see that the wallet was created:



 Create the wallet: Wallet { pub_key: PublicKey(3E657AE), name: "Johnny Doe", balance: 100 } 


The second wallet is formed similarly. After its creation we can transfer funds. The transfer-funds.json file looks like this:



 { "body": { "from": "03e657ae71e51be60a45b4bd20bcf79ff52f0c037ae6da0540a0e0066132b472", "to": "d1e877472a4585d515b13f52ae7bfded1ccea511816d7772cb17e1ab20830819", "amount": "10", "seed": "12623766328194547469" }, "network_id": 0, "protocol_version": 0, "service_id": 1, "message_id": 2, "signature": "2c5e9eee1b526299770b3677ffd0d727f693ee181540e1914f5a84801dfd410967fce4c22eda621701c2b9c676ed62bc48df9c973462a8514ffb32bec202f103" } 


This transaction transfers 10 coins from the first wallet to the second. Send a command to the node using curl :



 curl -H "Content-Type: application/json" -X POST -d @transfer-funds.json \ http://127.0.0.1:8000/api/services/cryptocurrency/v1/wallets/transaction 


The node will show that the amount was successfully transferred:



 Transfer between wallets: Wallet { pub_key: PublicKey(3E657AE), name: "Johnny Doe", balance: 90 } => Wallet { pub_key: PublicKey(D1E87747), name: "Janie Roe", balance: 110 } 




Now, let's check that the endpoint for processing read requests actually works. We can request the status of both wallets in the system as follows:



 curl http://127.0.0.1:8000/api/services/cryptocurrency/v1/wallets 


This request will display information about the wallets in the following form:



 [ { "balance": "90", "name": "Johnny Doe", "pub_key": "03e657ae71e51be60a45b4bd20bcf79ff52f0c037ae6da0540a0e0066132b472" }, { "balance": "110", "name": "Janie Roe", "pub_key": "d1e877472a4585d515b13f52ae7bfded1ccea511816d7772cb17e1ab20830819" } ] 


The second endpoint also works. We can verify this by sending the following request:



 curl "http://127.0.0.1:8000/api/services/cryptocurrency/v1/wallet/\ 03e657ae71e51be60a45b4bd20bcf79ff52f0c037ae6da0540a0e0066132b472" 


We get the answer:



 { "balance": "90", "name": "Johnny Doe", "pub_key": "03e657ae71e51be60a45b4bd20bcf79ff52f0c037ae6da0540a0e0066132b472" } 


Thus, within the framework of our educational material, we figured out how a simple blockchain works with one validator. In the following posts we will talk more about linking to blockchains, managing nodes and consensus in Exonum.

Source: https://habr.com/ru/post/342208/



All Articles