2.2. Implementation of Smart Contract System Based on Consortium Blockchains
2.2.1. Address/Account
In order to ensure the security and consistency of data, the write permission of blockchain is restricted by account and consensus system. Account and address need to have user identity authentication, uniqueness confirmation, digital signature, and other functions.
The account consists of three elements:
Account information: such as account name, email, and so on;
Public key: used for signature verification [
34];
Private key: maintained by users themselves.
The most important part of an account system is the asymmetric encryption algorithm. Good asymmetric encryption algorithms have to satisfy the following features:
Confidentiality: ensure the privacy of private data.
Authentication: ensure the identity of all parties attempted to access.
Authorization: ensure that a party attempting to perform a function has the right to do so.
Data integrity: ensure that objects are not illegally changed.
Non-repudiation: ensure that one party rejects the data or communication they initiate.
RSA and ECC are the most popular asymmetric encryption algorithms. In order to implement the account system in this paper, considering the guarantee of identity authentication and transaction tamper-proof in the distributed system, the account system should design one or more asymmetric key signatures. When choosing an asymmetric key algorithm, the key generation, performance, network, and storage resource overhead should be considered synthetically.
We compare all aspects of the two asymmetric key signature algorithms under the same difficulty. The comparative results are shown in
Table 1.
We can see from the comparison that ECC has some advantages over RSA in key length and key generation time. Because the key length of ECC is relatively short, the network bandwidth and storage space required by ECC are also relatively small. However, the RSA algorithm is faster than ECC in signature verification time, which has a certain advantage in scenarios where a large number of transactions need to be verified, and the RSA algorithm is simple and relatively mature.
Therefore, the account system in this paper adopts a combination of RSA and ECC in the implementation, so as to be flexible. Users can specify whether the public key type is RSA or ECC by setting different public key header identification fields. When the system receives the transaction signed by the user, the verification algorithm is identified by the identification field.
2.2.2. Network Structure
Since the transaction discovery and block proposal of blockchain are all realized through broadcasting, it would lead to a large number of redundant packets in the network, resulting in great pressure for the network layer system. In this paper, message frames are used to implement the blockchain network system protocol.
The network layer in this paper uses a P2P structure to reduce the risk of data loss and data tampering by reducing nodes in network transmission. Different from the network structure with a central server, the nodes in the peer-to-peer network are not only the clients but also perform some functions of the server, such as service discovery, message forwarding, and so on. Any node can not directly find other nodes beside its adjacent nodes after connecting to the network. It must rely on the node group in the network for information exchange.
The blockchain in this paper uses fully distributed unstructured topological networks. The network structure adopts the way of a random graph, while the degree of node generally obeys the power-law rule [
35,
36]. Fully distributed unstructured topological networks [
37] have high fault tolerance and adaptability to the dynamic changes of nodes in the network. Therefore, the flexibility is high.
In the network, each node is responsible for processing the node discovery request of the adjacent node. Every node pushes its own known node to the adjacent node and obtains the new node through the node discovery service provided by the adjacent node.
In the transmission layer, the TCP/IP protocol is used for data transmission. A flooding protocol is adopted based on the TCP/IP protocol. The basic idea of flooding protocol is to deliver data packets to all possible paths except the path where data comes when each node receives new data, and to discard the packet if redundant data is received. The data transmission process is shown in
Figure 1. When a piece of new data arrives, node A broadcasts to neighboring nodes B and C through flooding. When B and C receive the data, they continue to broadcast to neighboring nodes D, E, F, and G (because the data is lazily sourced, it is no longer broadcast to A). Then on the way to continue broadcasting, since nodes E and G are adjacent nodes, E and G broadcast the data to each other. However, since E and G have previously received the data from B and C respectively, the broadcast data between the two nodes this time will be discarded and the propagation will be terminated, ensuring that it will not continue to propagate to the B and C nodes.
We have divided nodes in our system into three kinds:
Central node: After initialization, all nodes would be connected to the central node, which would also partially be in charge of transaction verification.
Miner node: These nodes will store new transactions and generate new blocks when the transaction reaches a certain amount.
User node: These nodes are specifically responsible for generating transactions and proposing them to the miner node. These nodes serve as the interface layer between the user and the blockchain system.
The packages used in our network system are presented below in detail.
Version Package
Version package is mainly used for node discovery and data synchronization. If a new node is added to the network, it will forward its version information to the node of its initial connection, which is the central node, according to our design. In this system, the structure of the Version package is shown in
Table 2:
When the receiver receives a Version package, it records the sender’s address and returns a Version package regardless of the content of that Version package. The returned Version package contains its own blockchain length and its own address. The Version package is equivalent to a two-time handshake protocol, which guarantees the synchronization of mutual discovery and information consistency between nodes. The process is shown in
Figure 2:
When the node receives the Version package, it will not only return its own Version package. Instead, according to the sender’s Height field in the Version package, it will check the status of its own blockchain data and determine whether there is data to be synchronized. If the sender’s Height is higher than its own, it will enter the data synchronization process, and download the missing block. In particular, with the exception of the central node, all nodes need to have a node address as the receiver of the initial Version package when they are initially connected to the system. In our work, we set this initial node as the central node of the whole system.
Addr Package
In our system, each node is also responsible for the node push function, which means to inform the new node of the information of the known node. The Addr package is used to inform the new node of the information of the node known to the current node. The Addr package is sent in a very flexible way, usually sent along with the returned version package.
The structure of the Addr package is shown in
Table 3.
GetBlocks Package
The GetBlocks package serves to inform the receiver of this package to send information about the block it currently owns back to the sender of the GetBlocks package. The package is mainly used for block data synchronization between nodes, and the structure is relatively simple.
However, let’s assume a scenario where the receiver of the GetBlocks package has 10,000 blocks and each block’s information is 2 KB, then the amount of data returned by a single GetBlocks request might reach 10 MB. This will cause huge network overhead and network congestion, which is obviously unacceptable. Therefore, we design the GetBlocks package to request only returning hash tables for all blocks.
The structure of the GetBlocks package is shown in
Table 4.
Inv Package
The Inv package (Inventory package) is a special kind of package that contains all blocks and transactions held by the sender, but it does not directly contain all those data, just the hash value.
The structure of the Inv package is shown in
Table 5.
Let us review the scenario we assumed previously. By using the Inv package (as shown in
Figure 3), the data transferred can be compressed as the size of a block header, which could greatly reduce the network overhead and reduce the network burden. Using the Inv package allows the receiver to choose the data to be pulled (through the GetData package) on its own, avoiding a large number of unnecessary data transfers. It also has the advantage that the receiver can know the information of which data it needs to pull from the sender and then pull the real data from different nodes. Therefore, it can improve node utilization and system scalability. However, using Inv also increases the difficulty of the receiver’s data management strategy.
GetData
When a node finds that it needs to synchronize data, it sends the GetData package to its adjacent nodes to obtain specific data. The structure of the GetData package is shown in
Table 6.
The processing flow of the GetData package is simple: if the block is requested, the block data is returned according to the block ID (the Hash value of the block). Similarly, if the transaction is requested, the transaction data is returned according to the transaction ID.
Block and Action Package
The Block package and Action package are the data when the GetData package requests a block or request transaction, respectively. The data structure of the two packages is similar (
Table 7).
According to the design of the system, the Data field here corresponds to the serialized data of the specific data. The sender needs to serialize the specific data before sending, and the receiver deserializes it after receiving it to obtain the real data. The serialization of data transfer helps extend and modify the data structure of the system, and improve the scalability and maintainability of the system.
2.2.3. Consensus Algorithm
Public blockchains, such as Bitcoin and Ethereum, build their consensus system based on the PoW (Proof of Work) mechanism. Although PoW has key characteristics such as anti-forking and anti-tampering, in this work, using PoW is not a good choice. In this paper, we propose to construct a smart contract system based on consortium blockchains, where the nodes involved in the block production will be limited, that is, not all the nodes in the system have the authority to write in the ledger. Additionally, in consortium blockchains systems the number of nodes and the computational power of the whole system will not reach the level of the public blockchain at present. Thus building a consensus system based on PoW might lead to many issues, such as 51% computational power problems and resource consumption problems.
Although the proposal of proof of interest alleviates the defect of PoW to some extent, proof of interest is highly dependent on virtual currency. The blockchain system in this paper is designed to be a non-virtual-currency chain, so it is not applicable here.
The PoA (Proof of Authority) consensus system is a kind of consensus system that allows users to create new nodes and make transactions and produce blocks by using the node identity guarantee as “authentication”.
The “verifier” node is the core of the PoA consensus system. When the transaction is broadcast to the whole network, the ordinary node will no longer verify the correctness of the transaction, but only as of the intermediate route of the verifier node. The verifier node will finally be responsible for verification and packaging. The management nodes select the verifier node by voting and polling.
The verifier does not need a large amount of computation overhead similar to the PoW, nor does it need the virtual currency guarantee of the PoI (Proof of Identity). The only requirement of a verifier is that it must be a known node that already exists in the system and has been authenticated. The verifier acquires block-producing rights by identification of other nodes.
If the verifier has malicious behavior, or if there is a malfunction, it can be eliminated by voting of other management nodes on the blockchain. In addition to the initial node group, any node that wants to be a new management node must be agreed upon by all management nodes. When a management node performs malicious behavior or actively exits and needs to be removed from the management node group, it is also subject to restrictions similar to when added. Although PoA may also have a similar 51% computational power problem like PoW, it is different in nature and can be fixed by flexible joint decision design.
The DPoS consensus system maintains the node order of the system by means of election voting, and each user has the same authority to decide on the addition and elimination of management nodes. Nodes are ranked by the votes of users. The top n nodes with more votes become management nodes. The authority of each management node to confirm the blockchain transaction and write the distributed account book is equal.
Typically, management nodes select the “verifier” for each round by polling. Each verifier has the right to verify and produce a block for a certain period of time, and the produced block of a timed-out verifier will not be accepted by other management nodes. The fact that a verifier fails to produce a block within the last round block-producing time window will not affect the normal block production of the current round of verifiers.
In view of the advantages and disadvantages of the PoA and DPoS, the design of the blockchain system in this paper adopts a hybrid approach. In the dynamic expansion and reduction of management nodes, the PoA mechanism is adopted. While in the selection of management nodes, the DPoS mechanism is adopted.
In our system, the “block producer” role is played by the block producer node group, and the “verifier” is the responsibility of the central node. The “verifier” does not need to produce a block, but only needs to verify the block production of the block producer and sign the confirmation. The separation of “block producer” and “verifier” increases the risk of centralization, but it can simplify the complexity of the system and improve the system performance.
DPoS in our system is implemented using smart contracts. As the user initiates the DPoS transaction, the system runs and updates the block-producing node group through a smart contract system. When the block-producing node group accumulates a certain amount of transactions, a new block is produced and sent to the “verifier” node. After receiving the request, the “verifier” node will verify the validity of the new block and its source node according to the block data, the block producer node group table, and the current chain length. If the verification passed, the new block would be signed and broadcast to the whole blockchain.
PoA is also implemented using smart contracts. The PoA system authorization is improved to coordinate with the DPoS. When the block producing node group votes, it only determines the addition or deletion behavior but does not directly specify which node to add or eliminate. If the block-producing node group decides to add a node, the node to add is the node with the highest vote from the non-block-producing node group through DPoS voting. If the block-producing node group decides to delete a node, the node to delete is the node with the lowest vote from the block-producing node group through DPoS voting. After a valid block containing PoA transaction is verified and signed, all nodes will execute the block transaction and dynamically perform addition or deletion on the block node group table. This addition or deletion will affect the subsequent block-producing validation and will not take effect immediately.
2.2.4. Data Storage
In order to improve system performance and scalability, the database in our blockchain system is implemented using the NoSQL database. For smart contract table data, block node group table, and other hot data, we use a memory database for storage.
For each smart contract, we need to support the transaction level operation of the smart contract. When the transaction is wrong, we need to be able to roll back the data in time. This requires that for each smart contract we have a separate transaction-enabled memory database to manage its data. Traditional memory databases, such as Redis or Memcache, can not support transactions. Because of the separation of storage and services, it can not flexibly store database table objects. As a result, in this paper, we have implemented a lightweight KV memory database based on the radix tree index.
The radix tree is spatially optimized on the basis of the prefix tree. In a radix tree, when a node is the only child node of its parent node, it merges with the parent node. As a result, the number of child nodes per node in the radix tree is at most the radix r. Unlike the common prefix tree structure, the edge of the radix tree can represent either a single element (such as a character) or an ordered set of elements (such as a string), which makes the radix tree more efficient in processing small data-set (especially if strings are long) or data-set of strings that share long prefixes.
In this work, the radix tree is used as the base index of our database. The insert, delete, and lookup operation based on the radix tree index is realized [
38]. The time complexity of all these operations is
, where the value of the length
k is determined by the maximum length of the keyword in the index tree, and the keyword length unit is determined by the number of radix digits of the radix tree [
39].
The index tree structure in this work consists of the following two elements:
Edge. The edges of the radix index tree consist of its pointing nodes and its corresponding prefix labels.
Node. The radix index tree node consists of a value corresponding to its keyword and a set of values pointing to its child nodes. In particular, when the node is not a leaf node, its corresponding value is null.
When performing an insert operation, a new keyword is added to the index tree and attempts to minimize the storage. When inserting a keyword, we will first search with the keyword prefix until the search can not continue. There are two situations:
The remainder of the keyword has no common prefix with all edges with the current node as the root node. For this case, we just need to add an edge, as shown in
Figure 4a.
The remainder of the keyword has common prefix with one of the edges with the current node as the root node. In this case, we split the edge into a common edge and two edges connecting the common edge, which ensures that the number of child nodes of any node does not exceed the radix of the index tree, as shown in
Figure 4b.
The process of delete operation is similar to insert operation. When performing a delete operation, the keyword is deleted, and it will also try to merge subtree indexes to minimize storage.
To delete a keyword x, you need to first find the leaf node representing the x. If you find it, delete the corresponding leaf node. If the parent node of that leaf node has only one other child node, the edge label of that child node will be attached to the edge label of the parent node and the child node will be deleted.
The lookup operation will query the data corresponding to the keyword.
In the smart contract system, sometimes the contract will fail or need to be rolled back. For example, in a multi-vote scenario, a user votes for multiple users at the same time, which involves a multi-table write operation. However, if one of the voting operations fails, the previous voting operation needs to be rolled back to ensure data consistency, which is the active rollback operation of the contract. There is also a situation where the contract is executed with irreparable errors (such as array crossing). The contract can not be executed in the normal process and the database needs to be rolled back. This is the passive rollback operation of the contract. In this work, we implement the transaction level structure of our database based on read–write lock and write cache, as shown in
Figure 5.
For read-only operations, the read handle can be created directly because it is not destructive to the underlying data. The read handle contains database references, radix index tree references, read-only logos, etc. For the read-only handle, insert, modify, and delete operations are illegal operations. When looking up a read-only handle, it returns the result of the query directly through the underlying database index.
The read/write operation should be designed to avoid the destruction of the consistency of the underlying data. There are two typical scenarios:
Multiple read/write operations modify the same underlying data at the same time, destroying the consistency, isolation and persistence of transactions;
One write operation fails and needs to be rolled back after modifying multiple data, destroying the atomicity, consistency, and isolation of transaction, resulting in dirty data.
For scenario 1, our database adopts the way of write lock, that is, for the same database object, at the same time can only have one read/write handle. This write lock differs from a write lock in a traditional database, such as MySQL, in that the write lock in a traditional database is an exclusive lock. Before a transaction is turned on, it keeps trying to get the write lock until it succeeds. When a transaction occupies a write lock, only the transaction can write to the object. The write lock only exclusive write operation, so other read handles can read the underlying data at the same time. Write locks are released when a transaction is completed or an error or exception causes rollback.
For scenario 2, our system adopts the way of a write cache. The two-level write cache is used to ensure that the write operation meets the four elements of the transaction while improving performance.
Level 1 cache is a transaction cache. In the write handle, a write cache based on the radix tree is set for each table that needs to be modified. When adding or deleting data, add or delete operations will be temporarily written to the cache, while the underlying data will not be overwritten. When the data is queried, the data written to the cache is first queried and the cache hit data is returned first. When the cache is not hit, it is queried from the underlying data. When the transaction is complete, the write handle writes the data in the cache to the underlying data. When the transaction fails or is abnormal, the write handle directly discards the changes in the cache to avoid dirty data.
Level 2 cache is a memory cache. Level 1 cache uses the radix tree as the cache data index, which will frequently request and free node memory when adding and deleting nodes. When the number of write operations is big, a large number of node memory requests and releases will cause unnecessary system overhead. Therefore, the memory pool is used to manage the cache temporary index nodes which are used frequently and have a relatively short living period. At the end of the transaction, the node released from the level 1 cache will enter the memory pool to wait for the next use, instead of directly releasing the memory, thus reducing the time overhead of the operating system memory allocation.
2.2.5. Smart Contract
In this paper, a user-level smart contract system is implemented in the consortium blockchain system. Each user account can set up a smart contract system and several associated user-level tables.
Some important fields in the user-level table are shown in
Table 8.
Both the definition and implementation of the user-level table are in the radix tree-based memory database of this system, and the Name field is used as the unique index. At the same time, for each user-level table, there is a database handle of the user and a database schema corresponding to the handle, which together form the basic data storage unit of the user account to store the data generated by the smart contract and provide the data for the smart contract running environment. The database handle and schema of different users are different, which ensures the data isolation of different contracts so that each smart contract can run independently.
For each smart contract, we specify three important fields:
API: The API field is used to define the interface type of the smart contract, which is a mapping table from string to array. The structure of the API field is shown in
Figure 6. The API interface part specifies the name of its interface. Each interface name corresponds to a set of input parameters to provide the running environment of the interface. In particular, we can also support a zero-parameter interface;
Table: The Table field is used to define the database table structure for each smart contract, which is a tableName to tableHeader mapping table. The structure of the table field is shown in
Figure 7. We specify three basic data types for each user-level data table: string, float, and int. Users can add, delete, and modify their data through a smart contract. In order to ensure the fairness of the contract, the database table read permission of the contract is open to all users. The Table field and the Schema field both define the header field of the contract database table, but they are different: the former represents the table structure of the contract and is used for the verification of the smart contract and the fast query of the database table structure, is open to the users; while the latter plays a key role in the index query of the database data and is not visible to other users.
Jscript: The Jscript field is the script field of the smart contract, which specifies the running process of the smart contract. We use Javascript as the scripting language of smart contract, otto as its interpreter.
When deploying a smart contract, a user needs to clearly define the deployed account name, contract interface, contract table structure, and contract script. After the contract is deployed, the original contract script can be modified and upgraded without affecting the table structure and data.
The process of the user calling a smart contract is a process of transaction. The transaction contains the account name, caller, interface name, and interface parameter of the deployment contract. By providing built-in APIs, it provides an interface for contracts to interact with databases and transactions. Those built-in APIs are automatically loaded into the Javascript virtual machine environment before the contract runs, so the contract script can use those built-in APIs directly by a function call.