107
.gitignore
vendored
Normal file
@ -0,0 +1,107 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# dotenv
|
||||
.env
|
||||
|
||||
# virtualenv
|
||||
.venv
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
|
||||
# IntelliJ Idea
|
||||
.idea
|
||||
|
||||
#macOS folder metadata
|
||||
.DS_Store
|
333
README.md
@ -1,2 +1,331 @@
|
||||
# tendermint_demo
|
||||
Research artifacts and tools for Tendermint
|
||||
# Tendermint Verifiable Computations and Storage Demo
|
||||
|
||||
This demo application shows how verifiable computations might be processed by a distributed cluster of nodes. It comes with a set of hardcoded operations that can be invoked by the client. Each requested operation is computed by every (ignoring failures or Byzantine cases) cluster node, and if any node disagrees with the computation outcome it can submit a dispute to an external Judge.
|
||||
|
||||
Results of each computation are stored on the cluster nodes and can be later on retrieved by the client. The storage of the results is secured with Merkle proofs, so malicious nodes can't substitute them with bogus data.
|
||||
|
||||
Because every computation is verified by the cluster nodes and computation outcomes are verified using Merkle proofs, the client normally doesn't have to interact with the entire cluster. Moreover, the client can interact with as little as a single node – this won't change safety properties. However, liveness might be compromised – for example, if the node the client is interacting with is silently dropping incoming requests.
|
||||
|
||||
<p align="center">
|
||||
<img src="images/cluster_nodes.png" alt="Cluster" width="722px"/>
|
||||
</p>
|
||||
|
||||
## Table of contents
|
||||
* [Motivation](#motivation)
|
||||
* [Overview](#overview)
|
||||
* [State machine](#state-machine)
|
||||
* [Computations correctness](#computations-correctness)
|
||||
* [Tendermint](#tendermint)
|
||||
* [Operations](#operations)
|
||||
* [Installation and run](#installation-and-run)
|
||||
* [Command-line interface](#command-line-interface)
|
||||
* [Effectful operations (`put`)](#effectful-operations-put)
|
||||
* [Querying operations (`get, ls`)](#querying-operations-get-ls)
|
||||
* [Compute operations (`run`)](#compute-operations-run)
|
||||
* [Verbose mode and proofs](#verbose-mode-and-proofs)
|
||||
* [Implementation details](#implementation-details)
|
||||
* [Operations processing](#operations-processing)
|
||||
* [Few notes on transactions processing](#few-notes-on-transactions-processing)
|
||||
* [Few notes on ABCI queries processing](#few-notes-on-abci-queries-processing)
|
||||
* [Client implementation details](#client-implementation-details)
|
||||
* [Transactions and Merkle hashes](#transactions-and-merkle-hashes)
|
||||
* [Problematic situations review ](#problematic-situations-review)
|
||||
|
||||
## Motivation
|
||||
The application is a proof-of-concept of a decentralized system with the following properties:
|
||||
* Support of arbitrary deterministic operations: simple reads or writes as well as complex computations.
|
||||
* Total safety of operations when more than 2/3 of the cluster is non-Byzantine.
|
||||
* Ability to dispute incorrect computations.
|
||||
* High availability: tolerance to simultaneous failures or Byzantine actions of some subset of nodes.
|
||||
* High throughput (up to 1000 transactions per second) and low latency (~1-2 seconds) of operations.
|
||||
* Reasonable blockchain finality time (few seconds).
|
||||
|
||||
## Overview
|
||||
There are following actors in the application network:
|
||||
|
||||
* Cluster **nodes** which carry certain *state* and can run computations over it following requests sent by the client. All nodes normally have an identical state – the only exception is when some node is faulty, acting maliciously or has network issues. Computations can be *effectful* – which means they can change the state.
|
||||
|
||||
When the cluster has reached *consensus* on the computation outcome, the new state can be queried by the client – any up-to-date node can serve it. Consensus requires a 2/3 majority of the nodes to agree to avoid Byzantine nodes sneaking in incorrect results.
|
||||
|
||||
* The **client** which can issue simple requests like `put` or `get` to update the cluster state as well as run complex computations. In this demo application an available computations set is limited to operations like `sum` or `factorial` but in principle can be extended by a developer.
|
||||
|
||||
The client is able to check whether the cluster has reached consensus over computation outcomes by checking cluster nodes signatures and validate responses to the queries using Merkle proofs. This means that unless the cluster is taken over by Byzantine nodes the client can be certain about results provided and in general about the cluster state.
|
||||
|
||||
* The **Judge** which is in charge of resolving disputes over computations outcomes. Different nodes in the cluster might have different opinions on how their state should change when the computation is performed. Unless there is an unanimous consensus, any disagreeing node can escalate to the **Judge** for the final determination and penalization of uncomplying nodes.
|
||||
|
||||
The **Judge** is not represented by a single machine – actually, it can be as large as Ethereum blockchain to have the desired trustworthiness properties. For this showcase, however, it is imitated as a single process stub which can only notify the user if there is any disagreement.
|
||||
|
||||
Two major logical parts can be marked out in the demo application. One is a BFT consensus engine with a replicated transaction log which is provided by the Tendermint platform (**Tendermint Core**). Another is a state machine with domain-specific state transitions induced by transactions. Each node in the cluster runs an Tendermint Core instance and a state machine instance, and Tendermint connects nodes together. We will discuss both parts in more details below.
|
||||
|
||||
<p align="center">
|
||||
<img src="images/architecture.png" alt="Architecture" width="834px"/>
|
||||
</p>
|
||||
|
||||
Demo application operates under the following set of assumptions:
|
||||
* the set of nodes in the cluster is immutable
|
||||
* public keys of cluster nodes are known to all network participants
|
||||
* the Judge is trustworthy
|
||||
* honest nodes can communicate with the client and the Judge
|
||||
|
||||
### State machine
|
||||
|
||||
Each node carries a state which is updated using transactions furnished through the consensus engine. Assuming that more than 2/3 of the cluster nodes are honest, the BFT consensus engine guarantees *correctness* of state transitions. In other words, unless 1/3 or more of the cluster nodes are Byzantine there is no way the cluster will allow an incorrect transition.
|
||||
|
||||
If every transition made since the genesis was correct, we can expect that the state itself is correct too. Results obtained by querying such a state should be correct as well (assuming a state is a verifiable data structure). However, if at any moment in time there was an incorrect transition, all subsequent states can potentially be incorrect even if all later transitions were correct.
|
||||
|
||||
<p align="center">
|
||||
<img src="images/state_machine.png" alt="State machine" width="702px"/>
|
||||
</p>
|
||||
|
||||
In this demo application the state is implemented as an hierarchical key-value tree which is combined with a Merkle tree. This allows a client that has obtained a correct Merkle root from a trusted location to query a single, potentially malicious, cluster node and validate results using Merkle proofs.
|
||||
|
||||
Such trusted location is provided by the Tendermint consensus engine. Cluster nodes reach consensus not only over the canonical order of transactions, but also over the Merkle root of the state – `app_hash` in Tendermint terminology. The client can obtain such Merkle root from any node in the cluster, verify cluster nodes signatures and check that more than 2/3 of the nodes have accepted the Merkle root change – i.e. that consensus was reached.
|
||||
|
||||
<p align="center">
|
||||
<img src="images/hierarchical_tree_basic.png" alt="Hierarchical tree" width="691px"/>
|
||||
</p>
|
||||
|
||||
### Computations correctness
|
||||
|
||||
It's possible to notice that a Byzantine node has voted for the two blocks with the same height and penalize it. The fact that a node signed these two blocks is an evidence _per se_. However, if two nodes disagree on how the state should change when transactions are applied, it's dangerous to let the cluster to resolve this for reasonable cluster sizes.
|
||||
|
||||
Let's imagine we have a case that the node `A` says the new state should be S<sub>a</sub> and node `B` – S<sub>b</sub>. We could ask nodes in the cluster to vote for one of the states, and if S<sub>a</sub> got more than 2/3 of the votes against it, penalize the node `A`.
|
||||
|
||||
Now, let's assume that `n` nodes in the cluster were independently sampled from a large enough pool of the nodes containing a fraction of `q` Byzantine nodes. In this case the number of Byzantine nodes in the cluster (denoted by `X`) approximately follows a Binomial distribution `B(n, q)`. The probability of the cluster having more than 2/3 Byzantine nodes is `Pr(X >= ceil(2/3 * n))` which for 7 cluster nodes and 20% of Byzantine nodes in the network pool is `~0.5%`.
|
||||
|
||||
This means, with a pretty high probability we might have a cluster penalizing a node that have computed the state update correctly. If we want to keep the cluster size reasonably low we can't let the cluster make decisions of that matter. Instead, we can allow any node in the cluster to escalate to the external trusted Judge if it disagrees with state transitions made by any other node. In this case, it's not possible to penalize an honest node.
|
||||
|
||||
It's important to note that in the case of 2/3+ Byzantine nodes the cluster might provide an incorrect result to the unsuspecting client. The rest of the nodes which are honest might escalate to the Judge if they are not excluded from the communication by the Byzantine nodes (actually, this situation requires further investigation). However, in this case the dispute might get resolved significant time later. To compensate, the Judge can penalize malicious nodes by forfeiting their security deposits for the benefit of the client. However, even in this case a client can't be a mission critical application where no amount of compensation would offset the damage made.
|
||||
|
||||
### Tendermint
|
||||
|
||||
[Tendermint](https://github.com/tendermint/tendermint) platform provides a Byzantine-resistant consensus engine (**Tendermint Core**) which roughly consists of the following parts:
|
||||
* distributed transaction cache (**mempool**)
|
||||
* blockchain to store transactions
|
||||
* Byzantine-resistant **сonsensus** module (to reach the agreement on the order of transactions)
|
||||
* peer-to-peer layer to communicate with other nodes
|
||||
* **RPC endpoint** to serve client requests
|
||||
* **query processor** for making requests to the state
|
||||
|
||||
To execute domain-specific logic the application state machine implements Tendermint's [ABCI interface](http://tendermint.readthedocs.io/projects/tools/en/master/abci-spec.html). It is written in `Scala 2.12`, compatible with `Tendermint v0.19.x` and uses the [`jabci`](https://mvnrepository.com/artifact/com.github.jtendermint/jabci) library providing ABCI definitions for JVM languages.
|
||||
|
||||
Tendermint orders incoming transactions, passes them to the application and stores them persistently. It also combines transactions into ordered lists – _blocks_. Besides the transaction list, a block also has some [metadata](http://tendermint.readthedocs.io/en/master/specification/block-structure.html) that helps to provide integrity and verifiability guarantees. Basically this metadata consists of two major parts:
|
||||
* metadata related to the current block
|
||||
* `height` – an index of this block in the blockchain
|
||||
* block creation time
|
||||
* hash of the transaction list in the block
|
||||
* hash of the previous block
|
||||
* metadata related to the previous block
|
||||
* `app_hash` – hash of the state machine state that was achieved at the end of the previous block
|
||||
* previous block *voting process* information
|
||||
|
||||
<p align="center">
|
||||
<img src="images/blocks.png" alt="Blocks" width="721px"/>
|
||||
</p>
|
||||
|
||||
To create a new block a single node – the block _proposer_ is chosen. The proposer composes the transaction list, prepares the metadata and initiates the [voting process](http://tendermint.readthedocs.io/en/master/introduction.html#consensus-overview). Then other nodes make votes accepting or declining the proposed block. If enough number of votes accepting the block exists (i.e. the _quorum_ was achieved – more than 2/3 of the nodes in the cluster voted positively), the block is considered committed.
|
||||
|
||||
Once this happens, every node's state machine applies newly committed block transactions to the state in the same order those transactions are present in the block. Once all block transactions are applied, the new state machine `app_hash` is memorized by the node and will be used in the next block formation.
|
||||
|
||||
If for some reason a quorum was not reached the proposer is changed and a new _round_ of a block creation for the same `height` as was in the previous attempt is started. This might happen, for example, if there was an invalid proposer, or a proposal containing incorrect hashes, a timed out voting and so on.
|
||||
|
||||
Note that the information about the voting process and the `app_hash` achieved during the block processing by the state machine are not stored in the current block. The reason is that this part of metadata is not known to the proposer at time of block creation and becomes available only after successful voting. That's why the `app_hash` and voting information for the current block are stored in the next block. That metadata can be received by external clients only upon the next block commit event.
|
||||
|
||||
Grouping transactions together significantly improves performance by reducing the storage and computational overhead per transaction. All metadata including `app_state` and nodes signatures is associated with blocks, not transactions.
|
||||
|
||||
It's important to mention that block `height` is a primary identifier of a specific state version. All non-transactional queries to the state machine (e.g. `get` operations) refer to a particular `height`. Also, some blocks might be empty – contain zero transactions. By committing empty blocks, Tendermint maintains the freshness of the state without creating dummy transactions.
|
||||
|
||||
Another critical thing to keep in mind is that once the `k` block is committed, only the presence and the order of its transactions is verified, but not the state achieved by their execution. Because the `app_hash` resulted from the block `k` execution is only available when the block `k + 1` is committed, the client has to wait for the next block to trust a result computed by the transaction in the block `k`. To avoid making the client wait if there were no transactions for a while, Tendermint makes the next block (possibly empty) in a short time after the previous block was committed.
|
||||
|
||||
### Operations
|
||||
There are few different operations that can be invoked using the bundled command-line client:
|
||||
* `put` operations which request to assign a constant value to the key: `put a/b=10`.
|
||||
* computational `put` operations which request to assign the function result to the key: `put a/c=factorial(a/b)`
|
||||
* `get` operations which read the value associated with a specified key: `get a/b`
|
||||
|
||||
`put` operations are _effectful_ and are explicitly changing the application state. To process such operation, Tendermint sends a transaction to the state machine which applies this transaction to its state, typically changing the associated value of the specified key. If requested operation is a computational `put`, state machine finds the corresponding function from a set of previously hardcoded ones, executes it and then assigns the result to the target key.
|
||||
|
||||
Correctness of `put` operations can be verified by the presence of a corresponding transaction in a correctly formed block and an undisputed `app_hash` in the next block. Basically, this would mean that a cluster has reached quorum regarding this transaction processing.
|
||||
|
||||
`get` operations do not change the application state and are implemented as Tendermint _ABCI queries_. As the result of such query the state machine returns the value associated with the requested key. Correctness of `get` operations can be verified by matching the Merkle proof of the returned result with the `app_hash` confirmed by consensus.
|
||||
|
||||
## Installation and run
|
||||
To run the application, the node machine needs Scala 2.12 with `sbt`, [Tendermint](http://tendermint.readthedocs.io/en/master/install.html) binary and GNU `screen` in the PATH.
|
||||
To execute operations the client machine needs Python 2.7 with `sha3` package installed.
|
||||
|
||||
This demo contains scripts that automate running a cluster of 4 nodes (the smallest BFT ensemble possible) on the local machine. To prepare configuration files, run `tools/local-cluster-init.sh`. This will create a directory `$HOME/.tendermint/cluster4` containing Tendermint configuration and data files.
|
||||
|
||||
To start the cluster, run `tools/local-cluster-start.sh` which starts 9 `screen` instances: `app[1-4]` – application backends, `tm[1-4]` – corresponding Tendermint instances and `judge` – Judge stub. Cluster initialization might take few seconds.
|
||||
|
||||
Other scripts allow to temporarily stop (`tools/local-cluster-stop.sh`), delete (`tools/local-cluster-delete.sh`) and reinitialize & restart (`tools/local-cluster-reset.sh`) the cluster.
|
||||
|
||||
## Command-line interface
|
||||
Examples below use `localhost:46157` to connect to the 1st local "node". To access other nodes it's possible to use other endpoints (`46257`, `46357`, `46457`). Assuming Byzantine nodes don't silently drop incoming requests, all endpoints behave the same way. To deal with such nodes client could have been sending the same request to multiple nodes at once, but this is not implemented yet.
|
||||
|
||||
### Effectful operations (`put`)
|
||||
To order the cluster to assign a value to the target key, issue:
|
||||
```bash
|
||||
> python cli/query.py localhost:46157 put a/b=10
|
||||
```
|
||||
|
||||
To order the cluster to compute a function result and assign it to the target key, run:
|
||||
```bash
|
||||
> python cli/query.py localhost:46157 put "a/c=factorial(a/b)"
|
||||
```
|
||||
|
||||
Note that `put` operations do not return any result – they merely instruct Tendermint to add a corresponding transaction to the mempool. An actual transaction execution and state update might happen a bit later, when a new block is formed, accepted by consensus and processed by state machines. It's validation happens even later when the consensus is reached on the next block.
|
||||
|
||||
There are few other operations bundled with the demo applicaton:
|
||||
|
||||
```bash
|
||||
# a trick to copy a value from one key to the target key
|
||||
> python cli/query.py localhost:46157 put "a/d=copy(a/b)"
|
||||
|
||||
# increment the value in-place and associate it's old version with the target key
|
||||
> python cli/query.py localhost:46157 put "a/e=increment(a/d)"
|
||||
|
||||
# compute the sum and assign it to the target key
|
||||
> python cli/query.py localhost:46157 put "a/f=sum(a/d,a/e)"
|
||||
|
||||
# compute a hierachical sum of values associated with the key and it's descendants
|
||||
> python cli/query.py localhost:46157 put "c/a_sum=hiersum(a)"
|
||||
```
|
||||
|
||||
### Querying operations (`get`, `ls`)
|
||||
To read the value associated with the key, run:
|
||||
```bash
|
||||
> python cli/query.py localhost:46157 get a/b
|
||||
10
|
||||
|
||||
> python cli/query.py localhost:46157 get a/c
|
||||
3628800
|
||||
|
||||
> python cli/query.py localhost:46157 get a/d
|
||||
11
|
||||
|
||||
> python cli/query.py localhost:46157 get a/e
|
||||
10
|
||||
|
||||
> python cli/query.py localhost:46157 get a/f
|
||||
21
|
||||
|
||||
> python cli/query.py localhost:46157 get c/a_sum
|
||||
3628852
|
||||
```
|
||||
|
||||
To list immediate children for the key, issue:
|
||||
```bash
|
||||
> python cli/query.py localhost:46157 ls a
|
||||
e f b c d
|
||||
```
|
||||
|
||||
### Verbose mode and proofs
|
||||
Verbose mode allows to obtain a little bit more information on how the Tendermint blockchain structure looks like and how the client performs verifications.
|
||||
|
||||
**Let's start with `put` operation:**
|
||||
```bash
|
||||
> python cli/query.py localhost:46157 put -v root/x=5
|
||||
height: 7
|
||||
|
||||
# wait for few seconds
|
||||
|
||||
> python cli/query.py localhost:46157 put -v "root/x_fact=factorial(root/x)"
|
||||
height: 9
|
||||
```
|
||||
In this example, `height` corresponds to the height of the block in which the `put` transaction eventually got included. Now, we can checkout blockchain contents using a `parse_chain.py` script:
|
||||
```bash
|
||||
> python cli/parse_chain.py localhost:46157
|
||||
height block time txs acc.txs app_hash tx1
|
||||
...
|
||||
7: 2018-06-26 14:35:02.333653 1 3 0xA981CC root/x=5
|
||||
8: 2018-06-26 14:35:03.339486 0 3 0x214F37
|
||||
9: 2018-06-26 14:35:36.698340 1 4 0x214F37 root/x_fact=factorial(root/x)
|
||||
10: 2018-06-26 14:35:37.706811 0 4 0x153A5D
|
||||
```
|
||||
This script shows latest blocks in the blockchain with a short summary on their transactions. For example, it's possible to see that provided transaction `root/x=5` was indeed included in the block with height `7`. This means that Tendermint majority (more than 2/3 of the cluster nodes) agreed on including this transaction in this block, and that was certified by their signatures.
|
||||
|
||||
It's also possible to see that `app_hash` is modified only in the block following the block with transactions. You could note that block 8 (empty) and block 9 (containing `root/x_fact=factorial(root/x)`) application hashes are the same. However, block 10 has the application hash changed which corresponds to the changes that transactions from the previous block introduced into the application state.
|
||||
|
||||
**Now let's pull results back using `get` operation:**
|
||||
```bash
|
||||
> python cli/query.py localhost:46157 get -v root/x
|
||||
height: 9
|
||||
app_hash: 0x153A5D
|
||||
5
|
||||
|
||||
> python cli/query.py localhost:46157 get -v root/x_fact
|
||||
height: 9
|
||||
app_hash: 0x153A5D
|
||||
120
|
||||
```
|
||||
Here we can see that both (correct!) results correspond to the latest `app_hash` – `0x153A5D`.
|
||||
|
||||
## Implementation details
|
||||
|
||||
### Operations processing
|
||||
To perform operations, a client normally connects to a single node of the cluster.
|
||||
|
||||
Querying requests such as `get` are processed entirely by a single node. Single node state machine has all the required data to process queries because the state is fully replicated. The blockchain is fully replicated as well and each block contains signatures of the nodes accepted it, so the client can verify that the `app_hash` return by the node is not a bogus one.
|
||||
|
||||
Processing effectful requests such as `put` requires multiple nodes to reach consensus, however the client still sends a transaction to a single node. Tendermint is responsible in spreading this transaction to other nodes and reaching consensus.
|
||||
|
||||
### Few notes on transactions processing
|
||||
A transaction is processed primarily by two Tendermint modules: mempool and consensus.
|
||||
|
||||
It appears in mempool once one of Tendermint [RPC](https://tendermint.readthedocs.io/projects/tools/en/master/specification/rpc.html) `broadcast` methods is invoked. After that the mempool invokes the application's `CheckTx` ABCI method. The application might reject the transaction if it's invalid, in which case the transaction is removed from the mempool and the node doesn't need to connect to other nodes. Otherwise Tendermint starts spreading transaction to other nodes.
|
||||
|
||||
<p align="center">
|
||||
<img src="images/mempool.png" alt="Mempool" width="823px"/>
|
||||
</p>
|
||||
|
||||
The transaction remains for some time in mempool until consensus module of the current Tendermint proposer consumes it, includes to the newly created block (possibly together with several other transactions) and initiates a voting process for this block. If the transaction rate is intensive enough or exceeds the node throughput, it is possible that the transaction might wait during few blocks formation before it is eventually consumed by proposer. Note that transaction broadcast and the block proposal are processed independently, so it is totally possible that the block proposer is not the node originally accepted and broadcaster the transaction.
|
||||
|
||||
If more than 2/3 of the cluster nodes voted for the proposal in a timely manner, the voting process ends successfully. In this case every Tendermint instance starts synchronizing the block with its local state machine. It consecutively invokes the state machine's ABCI methods: `BeginBlock`, `DeliverTx` (for each transaction), `EndBlock` and lastly, `Commit`. The state machine applies transactions in the order they are delivered, calculates the new `app_hash` and returns it to Tendermint Core. At that moment the block processing ends and the block becomes available to the outside world (via the RPC methods like `block` and `blockchain`). Tendermint keeps `app_hash` and the information about a voting process so it can include it into the next block metadata.
|
||||
|
||||
<p align="center">
|
||||
<img src="images/consensus.png" alt="Consensus" width="823px"/>
|
||||
</p>
|
||||
|
||||
### Few notes on ABCI queries processing
|
||||
ABCI queries carry the target key and the target `height`. They are initially processed by the Tendermint query processor which reroutes them to the state machine.
|
||||
|
||||
State machine processes the query by looking up for the target key in a state corresponding to the block with specified `height`. This means that the state machine has to maintain several states (corresponding to different block heights) to be able to serve different queries.
|
||||
|
||||
<p align="center">
|
||||
<img src="images/abci_queries.png" alt="ABCI queries" width="823px"/>
|
||||
</p>
|
||||
|
||||
Note that state machine handles mempool (`CheckTx`), consensus (`DeliverTx`, `Commit`, etc) and queries pipelines concurrently. Because the target `height` is explicitly requested by queries, state machine maintains separate states for those pipelines. This way so serving queries is not affected when transactions are being concurrently delivered to the application.
|
||||
|
||||
### Transactions and Merkle hashes
|
||||
State machine does not recalculate Merkle hashes during `DeliverTx` processing. Instead, the state machine modifies the tree and marks changed paths by clearing Merkle hashes until ABCI `Commit` method is invoked. This allegedly should improve performance when there are several transactions in the block.
|
||||
|
||||
<p align="center">
|
||||
<img src="images/hierarchical_tree_updated.png" alt="Hierarchical tree after DeliverTx()" width="691px"/>
|
||||
</p>
|
||||
|
||||
On `Commit` the State machine recalculates Merkle hash along changed paths only. Finally, the app returns the resulting root Merkle hash to TM Core and this hash is stored as `app_hash` for a corresponding block.
|
||||
|
||||
<p align="center">
|
||||
<img src="images/hierarchical_tree_committed.png" alt="Hierarchical tree after Commit()" width="691px"/>
|
||||
</p>
|
||||
|
||||
Note that described merkelized structure is used just for demo purposes and is not self-balanced. It remains efficient only while transactions keep it relatively balanced. Something like [Patricia tree](https://github.com/ethereum/wiki/wiki/Patricia-Tree) or Merkle B-Tree should be more appropriate to achieve self-balancing.
|
||||
|
||||
## Problematic situations review
|
||||
There is a number of situations which might potentially go wrong with this demo application. Some of them are already handled by the client, some of them could have been handled but not supported yet, some could be detected but can't be handled without the external intervention. Below we will try to list a few, just keep in mind that list is not comprehensive by any means:
|
||||
|
||||
* A node might become unavailable because of the network issues, failing hardware or simply because the malicious node decided to start dropping requests. If this happens when the client is making requests, it can get noticed using timeouts. In this case the client simply retries the request, but now sends it to a different node. For the cluster it also doesn't create too many issues, because even with a fraction of unavailable nodes the cluster is able to reach consensus.
|
||||
|
||||
* It's possible that a Byzantine node might silently drop incoming transactions so they will never get propagated over the cluster. The client could monitor the blockchain to see if transaction got finally included in an accepted block, and if it didn't – resend it to other nodes.
|
||||
|
||||
* It is also possible that a Byzantine (or simply struggling) node might use a stale state and stale blockchain to serve queries. In this case the client has no way of knowing that the cluster state has advanced. Few different approaches can be used, however – the first is to send a `no-op` transaction and wait for it to appear in the selected node blockchain. Because including a transaction into the blockchain means including and processing all transactions before it, the client will have a guarantee that the state has advanced. Another approach is to query multiple nodes at once about their last block height and compare it to the last block height of the selected node. Finally, the client might just expect some maximum time until the next block is formed and switch to another node if nothing happens.
|
||||
|
||||
* When querying Tendermint RPCs on a particular node, it might return an inconsistent information. For example, that might be incorrect block hashes, incorrect vote signatures or signatures that are not matched with known public keys. In this case the client can treat the node as possibly Byzantine and retries request to another node.
|
||||
|
||||
* When making queries to a particular node's state, a combination of `result + proof` which is inconsistent with the target `app_hash` might be returned. In this case the client also can't trust this node and should switch to another one.
|
||||
|
||||
* The cluster cannot commit the block `k+1` while the сlient waits for it to have state `k` verifiable. Not much can be done here; possible reasons might be too many failed (or Byzantine) nodes to have the quorum for consensus. A subclass of this issue is when a consensus wasn't reached because there was a disagreement on the next `app_hash` among the nodes. In this case at least one node has performed an incorrect state update, and it's probable that a dispute was escalated to the Judge.
|
||||
|
||||
* If there are too many Byzantine nodes, consensus over the incorrect state transition might be reached. If there is at least one honest node in the cluster, it might escalate a dispute to the Judge (unless it was excluded from the cluster communication by colluding Byzantine nodes).
|
||||
|
||||
|
43
bin/judge.py
Executable file
@ -0,0 +1,43 @@
|
||||
#!/usr/bin/python
|
||||
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
||||
import json
|
||||
|
||||
PORT_NUMBER = 8080
|
||||
|
||||
class JudgeHandler(BaseHTTPRequestHandler, object):
|
||||
def do_GET(self):
|
||||
if (self.path == "/status"):
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(status_map))
|
||||
else:
|
||||
self.send_response(400)
|
||||
self.end_headers()
|
||||
|
||||
def do_POST(self):
|
||||
path_components = self.path.split("/")
|
||||
if len(path_components) >= 3 and path_components[1] == "submit":
|
||||
node = path_components[2]
|
||||
status_json = self.rfile.read(int(self.headers.getheader("Content-Length")))
|
||||
status_map[node] = json.loads(status_json)
|
||||
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
else:
|
||||
self.send_response(400)
|
||||
self.end_headers()
|
||||
|
||||
def log_message(self, format, *args):
|
||||
return
|
||||
|
||||
try:
|
||||
status_map = {}
|
||||
|
||||
server = HTTPServer(("", PORT_NUMBER), JudgeHandler)
|
||||
print "the Judge server started on port", PORT_NUMBER
|
||||
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print "^C received, shutting down the Judge"
|
||||
server.socket.close()
|
||||
|
123
cli/block_report.py
Executable file
@ -0,0 +1,123 @@
|
||||
#!/usr/bin/python
|
||||
import sys, urllib, json, datetime, time
|
||||
import matplotlib.pyplot as plt
|
||||
from common_parse_utils import uvarint, parse_utc, format_bytes, read_json, get_max_height
|
||||
|
||||
def get_num_txs(json):
|
||||
return json["result"]["block"]["header"]["num_txs"]
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print "usage: python parse_block.py host:port [report_name [min_height [max_height]]]"
|
||||
sys.exit()
|
||||
|
||||
tmaddress = sys.argv[1]
|
||||
report_name = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||
if len(sys.argv) > 4:
|
||||
max_height = int(sys.argv[4])
|
||||
else:
|
||||
max_height = get_max_height(tmaddress)
|
||||
while max_height >= 3 and get_num_txs(read_json(tmaddress + "/block?height=%d" % max_height)) == 0:
|
||||
max_height -= 1
|
||||
if len(sys.argv) > 3:
|
||||
min_height = int(sys.argv[3])
|
||||
else:
|
||||
min_height = max_height
|
||||
while min_height >= 3 and get_num_txs(read_json(tmaddress + "/block?height=%d" % (min_height - 1))) > 0:
|
||||
min_height -= 1
|
||||
|
||||
accsize = 0
|
||||
acclatency = 0
|
||||
minlatency = 1e20
|
||||
maxlatency = 0
|
||||
txcount = 0
|
||||
blockcount = 0
|
||||
firsttx = 1e20
|
||||
lasttx = 0
|
||||
firstblock = 1e20
|
||||
lastblock = 0
|
||||
max_block_size = 0
|
||||
|
||||
txstat = []
|
||||
for height in range(min_height, max_height + 1):
|
||||
data = read_json(tmaddress + "/block?height=%d" % height)
|
||||
num_txs = get_num_txs(data)
|
||||
|
||||
blocktimetxt = data["result"]["block"]["header"]["time"]
|
||||
blocktime = parse_utc(blocktimetxt)
|
||||
|
||||
if num_txs > 0:
|
||||
firstblock = min(firstblock, blocktime)
|
||||
lastblock = max(lastblock, blocktime)
|
||||
blockcount += 1
|
||||
max_block_size = max(max_block_size, num_txs)
|
||||
|
||||
print height, num_txs, blocktimetxt
|
||||
txs = data["result"]["block"]["data"]["txs"]
|
||||
if txs:
|
||||
for index, txhex in enumerate(txs):
|
||||
txbytes = bytearray.fromhex(txhex)# if re.fullmatch(r"^[0-9a-fA-F]$", txhex) is not None
|
||||
key = chr(txbytes[0]) if chr(txbytes[1]) == '=' else "*"
|
||||
connindex = uvarint(txbytes[2:8])
|
||||
txnumber = uvarint(txbytes[8:16])
|
||||
hostnamehash = txhex[32:64]
|
||||
|
||||
txtime = uvarint(txbytes[32:40]) / 1e6
|
||||
if txtime < 1e9:
|
||||
txtime *= 1e6 # legacy support
|
||||
latency = blocktime - txtime
|
||||
|
||||
accsize += len(txbytes)
|
||||
acclatency += latency
|
||||
minlatency = min(minlatency, latency)
|
||||
maxlatency = max(maxlatency, latency)
|
||||
txcount += 1
|
||||
firsttx = min(firsttx, txtime)
|
||||
lasttx = max(lasttx, txtime)
|
||||
|
||||
txtimetxt = datetime.datetime.fromtimestamp(txtime)
|
||||
|
||||
txstat.append((txtime, 1))
|
||||
txstat.append((blocktime, -1))
|
||||
if index < 5:
|
||||
print txtimetxt, latency
|
||||
#print key, connindex, txnumber, hostnamehash, txtimetxt, latency
|
||||
|
||||
print "Transactions: ", txcount, "=", format_bytes(accsize)
|
||||
print " ", "%.3f s" % (lasttx - firsttx), "from", datetime.datetime.fromtimestamp(firsttx), "to", datetime.datetime.fromtimestamp(lasttx)
|
||||
print "Blocks: ", "%d: from %d to %d" % (blockcount, min_height, max_height)
|
||||
print " ", "%.3f s" % (lastblock - firstblock), "from", datetime.datetime.fromtimestamp(firstblock), "to", datetime.datetime.fromtimestamp(lastblock)
|
||||
print "Tx send rate: ", "%.3f tx/s" % (txcount / (lasttx - firsttx)), "=", format_bytes(accsize / (lasttx - firsttx)) + "/s"
|
||||
print "Tx throughput: ", "%.3f tx/s" % (txcount / (lastblock - firsttx)), "=", format_bytes(accsize / (lastblock - firsttx)) + "/s"
|
||||
print "Block throughput:", "%.3f block/s" % (blockcount / (lastblock - firsttx))
|
||||
print "Avg tx latency: ", "%.3f s" % (acclatency / txcount)
|
||||
print "Min tx latency: ", "%.3f s" % minlatency
|
||||
print "Max tx latency: ", "%.3f s" % maxlatency
|
||||
|
||||
txstat = sorted(txstat)
|
||||
cursum = 0
|
||||
curindex = 0
|
||||
steps = 1000
|
||||
stepstat = []
|
||||
for i in range(steps + 1):
|
||||
t = firsttx + (lastblock - firsttx) / steps * i
|
||||
while curindex < len(txstat) and txstat[curindex][0] <= t:
|
||||
cursum += txstat[curindex][1]
|
||||
curindex += 1
|
||||
stepstat.append(cursum)
|
||||
f = plt.figure(figsize=(15, 5))
|
||||
plt.plot([i * (lastblock - firsttx) / steps for i in range(steps + 1)], stepstat)
|
||||
long_title = "Duration: %.1f s, Tx size: %s, Tx send rate: %.3f tx/s = %s/s, Tx throughput: %.3f tx/s = %s/s" % \
|
||||
(lasttx - firsttx, format_bytes(accsize / txcount), \
|
||||
txcount / (lasttx - firsttx), format_bytes(accsize / (lasttx - firsttx)), \
|
||||
txcount / (lastblock - firsttx), format_bytes(accsize / (lastblock - firsttx)))
|
||||
#plt.title(long_title)
|
||||
plt.title(report_name)
|
||||
plt.xlabel("seconds from first tx")
|
||||
plt.ylabel("txs in backlog")
|
||||
|
||||
if report_name != "":
|
||||
long_filename = "tdmnt-stat-%d-%d-%d-%.1f-%.0f-%.0f.png" % \
|
||||
(min_height, max_height, max_block_size, lasttx - firsttx, accsize / txcount, txcount / (lasttx - firsttx))
|
||||
#f.savefig(long_filename, bbox_inches='tight')
|
||||
f.savefig(report_name + ".png", bbox_inches='tight')
|
||||
plt.show(block=True)
|
46
cli/common_parse_utils.py
Executable file
@ -0,0 +1,46 @@
|
||||
import sys, urllib, json, datetime, time
|
||||
|
||||
def uvarint(buf):
|
||||
x = long(0)
|
||||
s = 0
|
||||
for b in buf:
|
||||
if b < 0x80:
|
||||
return x | long(b) << s
|
||||
x |= long(b & 0x7f) << s
|
||||
s += 7
|
||||
return 0
|
||||
|
||||
def parse_utc(utctxt):
|
||||
#tz conversion may be wrong
|
||||
now_timestamp = time.time()
|
||||
offset = datetime.datetime.fromtimestamp(now_timestamp) - datetime.datetime.utcfromtimestamp(now_timestamp)
|
||||
dt, _, tail = utctxt.partition(".")
|
||||
if tail == "":
|
||||
dt, _, _ = utctxt.partition("Z")
|
||||
tail = "0Z"
|
||||
pure = int((datetime.datetime.strptime(dt, '%Y-%m-%dT%H:%M:%S') + offset).strftime("%s"))
|
||||
ns = int(tail.rstrip("Z").ljust(9, "0"), 10)
|
||||
return pure + ns / 1e9
|
||||
|
||||
def format_bytes(value):
|
||||
if value < 1024:
|
||||
return "%.0f B" % value
|
||||
elif value < 1024 * 1024:
|
||||
return "%.3f KiB" % (value / 1024.0)
|
||||
else:
|
||||
return "%.3f MiB" % (value / 1024.0 / 1024.0)
|
||||
|
||||
def read_json(url):
|
||||
response = urllib.urlopen("http://" + url)
|
||||
return json.loads(response.read())
|
||||
|
||||
def get_sync_info(tmaddress):
|
||||
status = read_json(tmaddress + "/status")["result"]
|
||||
if "sync_info" in status: # compatibility
|
||||
return status["sync_info"]
|
||||
else:
|
||||
return status
|
||||
|
||||
def get_max_height(tmaddress):
|
||||
return get_sync_info(tmaddress)["latest_block_height"]
|
||||
|
49
cli/parse_chain.py
Executable file
@ -0,0 +1,49 @@
|
||||
#!/usr/bin/python
|
||||
import sys, urllib, json, datetime, time
|
||||
from common_parse_utils import parse_utc, read_json, get_max_height
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print "usage: python parse_chain.py host:port [min_height]"
|
||||
sys.exit()
|
||||
|
||||
blocks_fetch = 20 # tendermint can't return more blocks
|
||||
tmaddress = sys.argv[1]
|
||||
max_height = get_max_height(tmaddress)
|
||||
min_height = int(sys.argv[2]) if len(sys.argv) > 2 else max(1, max_height - 49)
|
||||
|
||||
last_non_empty = -1
|
||||
last_fetched_height = min_height - 1
|
||||
print "%6s %26s %7s %7s %8s %30s %30s %30s %30s %30s" % ("height", "block time", "txs", "acc.txs", "app_hash", "tx1", "tx2", "tx3", "tx4", "tx5")
|
||||
for height in range(min_height, max_height + 1):
|
||||
if height > last_fetched_height:
|
||||
last_fetched_height = min(height + blocks_fetch - 1, max_height)
|
||||
bulk_data = (read_json(tmaddress + "/blockchain?minHeight=%d&maxHeight=%d" % (height, last_fetched_height)))["result"]["block_metas"]
|
||||
|
||||
data = bulk_data[last_fetched_height - height]["header"]
|
||||
|
||||
num_txs = data["num_txs"]
|
||||
total_txs = data["total_txs"]
|
||||
app_hash = data["app_hash"]
|
||||
|
||||
blocktimetxt = data["time"]
|
||||
blocktime = parse_utc(blocktimetxt)
|
||||
|
||||
if num_txs > 0 or height == max_height or height == last_non_empty + 1:
|
||||
blockdata = read_json(tmaddress + "/block?height=%d" % height)
|
||||
txs = blockdata["result"]["block"]["data"]["txs"]
|
||||
txsummary = ""
|
||||
if txs:
|
||||
last_non_empty = height
|
||||
for tx in txs[0:5]:
|
||||
txstr = tx.decode('base64')
|
||||
if len(txstr) > 30:
|
||||
txsummary += "%27s... " % txstr[0:27]
|
||||
else:
|
||||
txsummary += "%30s " % txstr
|
||||
if len(txs) > 5:
|
||||
txsummary += "..."
|
||||
app_hash_to_show = "0x" + app_hash[0:6] if app_hash != "" else "--------"
|
||||
print "%5s: %s %7d %7d" % (height, datetime.datetime.fromtimestamp(blocktime), num_txs, total_txs), app_hash_to_show, txsummary
|
||||
else:
|
||||
if height == last_non_empty + 2:
|
||||
print "..."
|
96
cli/query.py
Executable file
@ -0,0 +1,96 @@
|
||||
#!/usr/bin/python2.7
|
||||
import sys, urllib, json, datetime, time, hashlib, sha3
|
||||
from common_parse_utils import read_json, get_sync_info, get_max_height
|
||||
|
||||
|
||||
CMD_PUT_TX = "put"
|
||||
CMD_GET_QUERY = "get"
|
||||
CMD_LS_QUERY = "ls"
|
||||
ALL_COMMANDS = {CMD_PUT_TX, CMD_GET_QUERY, CMD_LS_QUERY}
|
||||
|
||||
def verify_merkle_proof(result, proof, app_hash):
|
||||
parts = proof.split(", ")
|
||||
parts_len = len(parts)
|
||||
for index in range(parts_len, -1, -1):
|
||||
low_string = parts[index] if index < parts_len else result
|
||||
low_hash = hashlib.sha3_256(low_string).hexdigest()
|
||||
high_hashes = parts[index - 1].split(" ") if index > 0 else [app_hash.lower()]
|
||||
if not any(low_hash in s for s in high_hashes):
|
||||
return False
|
||||
return True
|
||||
|
||||
def checked_abci_query(tmaddress, height, command, query, tentative_info):
|
||||
if get_max_height(tmaddress) < height + 1:
|
||||
return (height, None, None, None, False, "Cannot verify tentative '%s'! Height is not verifiable" % (info or ""))
|
||||
|
||||
app_hash = read_json('%s/block?height=%d' % (tmaddress, height + 1))["result"]["block"]["header"]["app_hash"]
|
||||
response = read_json('%s/abci_query?height=%d&data="%s:%s"' % (tmaddress, height, command, query))["result"]["response"]
|
||||
(result, proof) = (
|
||||
response["value"].decode('base64') if "value" in response else None,
|
||||
response["proof"].decode('base64') if "proof" in response else None
|
||||
)
|
||||
if result is None:
|
||||
return (height, result, proof, app_hash, False, "Result is empty")
|
||||
elif tentative_info is not None and result != tentative_info:
|
||||
return (height, result, proof, app_hash, False, "Verified result '%s' doesn't match tentative '%s'!" % (result, info))
|
||||
elif proof is None:
|
||||
return (height, result, proof, app_hash, False, "No proof")
|
||||
elif not verify_merkle_proof(result, proof, app_hash) :
|
||||
return (height, result, proof, app_hash, False, "Proof is invalid")
|
||||
else:
|
||||
return (height, result, proof, app_hash, True, "")
|
||||
|
||||
def print_response(attribute, value, always=False):
|
||||
need_print = always or "v" in flags
|
||||
if need_print:
|
||||
print attribute + ":", (8 - len(attribute)) * " ", value
|
||||
|
||||
def print_checked_abci_query(tmaddress, height, command, query, tentative_info):
|
||||
(height, result, proof, app_hash, success, message) = checked_abci_query(tmaddress, height, command, query, tentative_info)
|
||||
app_hash_to_show = "0x" + app_hash[0:6] if app_hash != "" else "--------"
|
||||
print_response("height", height)
|
||||
print_response("app_hash", app_hash_to_show or "NOT_READY")
|
||||
# print_response("proof", (proof or "NO_PROOF").upper())
|
||||
print result or "???"
|
||||
if not success:
|
||||
print_response("bad", message, True)
|
||||
|
||||
def latest_provable_height(tmaddress):
|
||||
return get_sync_info(tmaddress)["latest_block_height"] - 1
|
||||
|
||||
def wait_for_height(tmaddress, height, seconds_to_wait = 5):
|
||||
wait_step = 0.1
|
||||
for w in range(0, int(seconds_to_wait / wait_step)):
|
||||
if get_max_height(tmaddress) >= height:
|
||||
break
|
||||
time.sleep(wait_step)
|
||||
|
||||
|
||||
num_args = len(sys.argv)
|
||||
if num_args < 4 or not sys.argv[2] in ALL_COMMANDS:
|
||||
print "usage: python query.py host:port <command> [flags] arg"
|
||||
print "<command> is one of:", ", ".join(ALL_COMMANDS)
|
||||
sys.exit()
|
||||
|
||||
tmaddress = sys.argv[1]
|
||||
command = sys.argv[2]
|
||||
flags = "".join(sys.argv[3:(num_args - 1)])
|
||||
arg = sys.argv[num_args - 1]
|
||||
if command in {CMD_PUT_TX}:
|
||||
tx = arg
|
||||
query_key = tx.split("=")[0]
|
||||
response = read_json(tmaddress + '/broadcast_tx_commit?tx="' + tx + '"')
|
||||
if "error" in response:
|
||||
print_response("error", response["error"]["data"], True)
|
||||
else:
|
||||
height = response["result"]["height"]
|
||||
if response["result"].get("deliver_tx", {}).get("code", "0") != "0":
|
||||
print_response("height", height)
|
||||
print_response("bad", log or "NO_MESSAGE", True)
|
||||
else:
|
||||
info = response["result"].get("deliver_tx", {}).get("info")
|
||||
print_response("height", height)
|
||||
# print_response("info", info or "EMPTY") TODO: what is info?
|
||||
elif command in {CMD_GET_QUERY, CMD_LS_QUERY}:
|
||||
height = latest_provable_height(tmaddress)
|
||||
print_checked_abci_query(tmaddress, height, command, arg, None)
|
2
cli/report_to_file.sh
Executable file
@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
python block_report.py $1 "$2" $3 $4 | tee "$2.txt"
|
BIN
images/abci_queries.png
Normal file
After Width: | Height: | Size: 194 KiB |
BIN
images/architecture.png
Normal file
After Width: | Height: | Size: 333 KiB |
BIN
images/blocks.png
Normal file
After Width: | Height: | Size: 271 KiB |
BIN
images/cluster_nodes.png
Normal file
After Width: | Height: | Size: 150 KiB |
BIN
images/consensus.png
Normal file
After Width: | Height: | Size: 220 KiB |
1
images/drawio/abci_queries.xml
Normal file
@ -0,0 +1 @@
|
||||
<mxfile userAgent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36" version="8.8.7" editor="www.draw.io" type="github"><diagram id="4ece53e3-1642-a935-6a08-c351d2ca4690" name="Page-1">7Zrdl6I2FMD/Gh53DwmC+Djj7nT70J5tp6fdfYwQkTORuCGO2r++CSRCQlDsYMdzqi/CJeTj3t/9SNQL5uv9TwxtVr/QFBMP+uneCz55EAIw88WXlBxqSRROa0HG8lQ1agTP+d9YCdV72TZPcWk05JQSnm9MYUKLAifckCHG6M5stqTEHHWDMtwRPCeIdKV/5Slf1dI49Bv5F5xnKz0y8NWTBUpeMka3hRrPg8Gy+tSP10j3pdqXK5TSXUsUfPaCOaOU11fr/RwTqVuttvq9p56nx3kzXPAhL8D6hVdEtljPuJoXP2hdVKvBsj3wgsfdKuf4eYMS+XQnrC9kK74m6vFxPb64yQgqS3Wd0HWeqOuSM/qC55RQVg0R+NXn+ESrXHa4zAnRLQtaYNktQ2ku1meJl7TgT2idE8ncF0xeMc8TpB4oxGCo7l2DI5JnhZARvBS6e1SawYzjfa92wdFmwhcwXWPODqKJegH6Sp3KDWJl9V3DVBTMatmqxVOg/QApjrNj140txYUyp9u0gcO0EZErS/NXw8TRj63k7VEslH9QWniQ3iWWiVnzXFxl6rvqZ8FsiZhT1bmW9qLkXwulNFpEYXQWpdovYZKMCY7Sl1AwKldHjxmDowAYHAHQBQnq4NQG6Sh8C0iTS2LE1Qwbw0UQDTJsGuI4nVzfsNKkoj/yoFqt8zSVChnD4mHkv6PFw47F/8DCvmydi9lDf04ZbscARyiRGv1QViqWkQTAzb4OE51QYpEkY5CFi0GCDvbdtNAxVq99XHyaBPciagDQ5nUIawCeYK1qp/QARiAomJoEBVGXoJkj98AR+Ik6/DxzxHFVACWrvLjTc+v0TIFND/wYdvlxBCAQvp2fuLd0keus6n6tggYJrYyWaABaoUCrw9VvW1wpY8NogsuSslZ9U8/gbIHz3rVys/EYSJcjDz5VH5dzjBGfYpMwV3EcO/iKRohPsw5fcyJ3FG+zp12GIBwvE5cJoyTGi6VD/yOoFZzXKoBXUqse+9389vevYo6+KFU2tKpU7k47stPCEJ7Fa+ooKkahCzjoskwpTP8gj3/ksqVtpEV6rXeJlewzh+taCu9z/k2NLK+/yxl/lF2IFuzwTS2gumme1drAqXW0VdItS7AR+DhiGdYG9wdD0DKya+uhZQwTxPNXcxYuy6sRvtbOqhmbAF1pKMgmNj31itRr7cMtq6dZpyf7XKVWRKenCsXjwofR6TpJM2LfsJjmKnN/pSnuDWYXFdJ9I/y/CulTW/7RC+nQTMiR76ijoyvV0aD/DPC/ScgPj/Of7xn5mhk5tMtox0HRtcpo0N3n32BG/hOzFBWXxICB+RiaCbl1/121NaPZgPwcO/JzcCv5+a3p2IqEQ5OxwAcdWs2qSFL21w+Rb3nELLSgrnv815nedTrRT/2C0OTlNpg/dYDUy7yqOxX1oI088Do1qJGYhRaecqnHaqEXO4Mmv+0N8d0ZLnMGaI6jf1kcyxmCy5zhvinrS/AW+n4P+u+1KQMTiyP7Z4Kh3AsiT3c03pYs6B7zWdXvEiVmUavj5mWlLzxxhjys4L3x7daoOyxvhKLXTvHTbtE7cTjDGL9t6X7fh6of6pcJhsst4Z7jjxiy8658wyhd3mm8Co2TqXnKdAxpZ34pm1xOo7ht/htWh8TmD3jB538A</diagram></mxfile>
|
1
images/drawio/architecture.xml
Normal file
@ -0,0 +1 @@
|
||||
<mxfile userAgent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36" version="8.8.6" editor="www.draw.io" type="github"><diagram id="7037c2fa-0f9e-d8c5-e8c2-95c4c3d480c8" name="Page-1">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</diagram></mxfile>
|
1
images/drawio/blocks.xml
Normal file
@ -0,0 +1 @@
|
||||
<mxfile userAgent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36" version="8.8.6" editor="www.draw.io" type="github"><diagram id="568390c0-3384-1bbd-4c30-df2126a73d65" name="Page-1">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</diagram></mxfile>
|
1
images/drawio/cluster_nodes.xml
Normal file
@ -0,0 +1 @@
|
||||
<mxfile userAgent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36" version="8.8.6" editor="www.draw.io" type="github"><diagram id="6c333e43-4dad-7b75-0653-91ee8c6dc7ff" name="Page-1">3VlLb6MwEP41SN1DK8BA6LFJH3vYlSpV2u0eHTDgrcNkjdMk/fVrgx0gkCpVaBI1F2Ae9vibz+NHLDSZrR44nmc/ISbMcu14ZaFby3Ud59qWDyVZV5LAH1WClNNYG9WCJ/pGtFD7pQsak6JlKACYoPO2MII8J5FoyTDnsGybJcDavc5xSjqCpwizrvQ3jUVWSUN3VMu/E5pmpmcnuK40Uxy9pBwWue7PclFS/ir1DJu29ECLDMewbIjQnYUmHEBUb7PVhDCFrYGt8rvfod3EzUku9nFwK4dXzBbERFzGJdYGi3I0RNk7FhovMyrI0xxHSruU2ZeyTMyYVjM8JewRCioo5FIWyTgIl4pXwgWVAP/YMhCgWsCMpr3mN1oxBSFgJhUxLrJNMAllbAIMpMdtDrkMadwFQGOiWiSrhkgD8kBgRgRfSxOtRcirXAx5ddaXNRM8w4SswQLX10nFmn3ppuk6A/JFJ6E/IaiTkAmjaiwHpaUJlKRkjEmYRFJeCA4vpKEJopBME+UBudDz0vGHwdWzW7A6ZrI3cHWQ3cXVGwBWr4MfieWM15+aOzWkdhs+sqLiufH+R5lcKVgkGnz9rD3Kj1r3lwix1hjihQApAi4ySCGX8wAU8csWC4G5uFFVS80AhouCRkZ8T5kJYmcGCljwSI9K01K6pkRbhZVIjffdLHHCsKCv7Qp4COb+IJhrnDXqTgvzOgV/dANngrmDTgV6MBDoo3dQdw+Afave2OXvCAlxTpWQUbegA+dq0+Laudw4DVvYfRLGXh/QoTtFQfA5hR2NTljZw6+Pb2CfEN/rYQrK7pXTsc5i5TQV4jzKuMnwoIunbe1ePJ0zgv1kOxYTTKOYjNdvOBdUwv0J5SQJIxL17sOnoe/59nHKyaZMHKOcON0D5gAblC1mu+dBbXRO1O45WH61dXJ7H3JcYnsdgC03YEIPtYVt8G8BRnFZlCDcSAPHna9qpbq3wlHb4RfhMc5x0yhI1RPmhGN1pXIhyVZY/tjyb7+Z/mXoVQiV7SATsH0Udt87C7sfWdJLWuhYem6zeqZZzywb7TnLNG8uZYztDZb+3HsW6qYfgZaZNiaQJAURHRZtItiPWP4piXXBSbGQnbkTlQkOkHwOrXbsBkcf4U7/ZrNs4uO06jlE7rsb1LTaZlV4GKlMkfPQld9mK7puN1INQ/s1L3+H4Kf8rK+nK/P6PwB09x8=</diagram></mxfile>
|
1
images/drawio/consensus.xml
Normal file
@ -0,0 +1 @@
|
||||
<mxfile userAgent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36" version="8.8.7" editor="www.draw.io" type="github"><diagram id="d342b105-9953-17f9-5ea8-654ac151bcbc" name="Page-1">7VpLd6M2FP41Xs4cBAbjZexJOl20Z04zp52uemSQQY1ArpATZ359JZDMQ8IhMSSZ1t4YroQe9/vuSzDz1tnhJwZ36S80RmTmOvFh5n2auS4AS0f8ScljJQn8RSVIGI5Vp1pwi78jJVTPJXsco6LVkVNKON61hRHNcxTxlgwyRh/a3baUtGfdwQQZgtsIElP6B455WklD36nlnxFOUj0zcFTLBkZ3CaP7XM03c71t+auaM6jHUv2LFMb0oSHyrmfemlHKq6vssEZE6larrXrupqf1uG6Gcj7kAbd64B6SPdIrLtfFH7Uuyt0g2R/MvNVDijm63cFItj4I9IUs5RlRzcf9OOImIbAo1HVEMxyp64IzeofWlFBWTuE55e/YolUuB9xiQnTPnOZIDstgjMX+OuItzfkNzDCRnPuMyD3iOIKqQVHM9dW9bXJIcJILGUFbobuV0gxiHB16tQuOmAlbQDRDnD2KLuoB11HqVGYQKtQfak4F3rKSpQ0+edoOoOJxchy6xlJcKDjt0HoWaAMidxbj+xbEwT97ybeV2Cj/oLRwJa1LbBOxul1cJeq/HGfDuhKxpnJwLe2lkjMVleJgE/jBk1Sq7NKNojGJo/QlFAyL9GgxY/DIAy0eAWASydXOqUmko/AcIs2f4yMmAzZ0N14wCNjYR2E8nx5YCakYj1ypXhmOY6mQMRD3A+cNEfcNxL8igS/LsFi966wpQ00fYHElUqMfilLF0pMAd3eo3IThSjpMkj6oQ5cWE7SzN8OCAVYvPjZ+thncS9EWAZp8HcI14J7gWtlP6QGMwCBv0WaQF5gMWlpijzsCfwKDP7ccclQmQFGK8wt73jt7FqDLHvejb/LH4oCAfz5/Fr2pi9xnmfdrFdSU0MpoiAZQy++h1hckMx9HuL0dLb3eMcGplvBkhvPWyXJdeQyklyUQ3pQ/m3WMkdT4QFNKBznPdFGhhWLBCC4qfHuKrWleoLzYFxdyjU4uL/A75HItGdTSnYZcS4NcayIr1vMQ7aa5EIXbyAZiEIVos7UgMIJiO3HBVtICdyKb1XO/ltEaFvvbl/UlJrxuTLARbKqYAICFXx0wBfhX8oBRblyiIzHpxe85OHVPtabFCh0w/6Zmltd/yhV/lEOIHuzxm9pAeVO3VdpAcefwtKB7FqGW6+OQJUhD7gymQQNkW3GrZQwRyPF9exU25NUMXypz1Sybd0k277Kn2pF6rHl82hlpaYzUPbmrFGGMVFLxuPFh7LSd1ba838tTkV9pjHrd2bNKtb4ZDEP6T5dqpw6VRi/V/HZIDhxLpRZMVKmB/lPm1wnJV6v1z5eYPGVM9sNOymdJpCeLyOZJ0o8dkfuPlp8fkcuj67Y7GxCgF2aADt9LfD43HHc84dBgLOgDHxvdSk9S9OcPXmceEDgnl9Xtrw/YayOoVvDizMB2Xnaxkrrt7322U9MFL7EZzaOL0YxnNPod92Cjmbf6n2007g9R7P2OWAzzCUzGfTKy9NiFQfi2qYSmpVQ54v/FLl5CRbOyW6EE5ytCozvzY4tPiAh1sK8Hs+k6j3seWtMsw9yUG5w/vy5T389MUZX1FmJ2Ozn1xiylDH8XbVBvc3Z+qjwP591XGpa3rouJTp2HfKjR8Gibkijvwp+dwqnXnzn9KQAwU4BWPS+0cIOlHk8lAE84Ol35GjnBxdH1M9T8sATudn+lApuLZ+r3TM0TI/0KZFw3pZOr1pERMKn6go9DxG39OWvFmfqbYe/6Xw==</diagram></mxfile>
|
1
images/drawio/hierarchical_tree.xml
Normal file
@ -0,0 +1 @@
|
||||
<mxfile userAgent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36" version="8.8.6" editor="www.draw.io" type="device"><diagram id="2601f40a-c04a-d45e-4f1e-d157f2eb324b" name="Page-1">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</diagram></mxfile>
|
1
images/drawio/hierarchical_tree_basic.xml
Normal file
@ -0,0 +1 @@
|
||||
<mxfile userAgent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36" version="8.8.6" editor="www.draw.io" type="github"><diagram id="1dd8c453-06d4-b669-e7c3-1ba795d2c001" name="Page-1">7ZtLc9owEMc/jY/pWDYmcAwOaQ/tTGdyaHqUbWG7EV4qRGL66auXMeadJkUTIi5Yf61W0q4s/czDC+Np/ZnhWfENMkK9wM9qL7z1ggChoS/epLLUSj+61kLOyswYtcJ9+YcY0bTLF2VG5h1DDkB5OeuKKVQVSXlHw4zBc9dsArTb6wznZEu4TzHdVn+UGS+0Ooj8Vv9Cyrxoeka+qUlw+pgzWFSmPy8IJ+qlq6e48WXs5wXO4HlNCsdeGDMArq+mdUyojG0TNt3ubk/tatyMVPyUBoEZBl82UyeZiIQpAuMF5FBhOm7VkZoekQ58USr4lIpLJC5JXfKHteuf0uRTJEsVZ8sH00IV2roMzwvlDZnCd8w5YZVSAl+qvwjnS7NK8IKDkNqhfQWYmdYTqLgxQ9K1npycUSc8c1iw1EiRWV2Y5cREbLgdRLRKjVjyBKZEzECYMEIxL5+63rFZe/nKro2/uDAp2J2O0KVjKx1ox6I+Vz56Lh/b+fDt5cMM5wnTBWk22D4VgxhxnIjoiw0YWEaYquv/XshtdITaS2HwvNrPV/Vy8446Rm3CG9E0u5Eh1/bKsDCnwKauh3GVAqV4NifaoC2N1KFU8au5SodqHc1qVdEOo5+bdz3DBLLlzrG92BN7GzeZzCYt86rjJhWrQmTgcDhP7euRLJvuxNqQPZ5/DHq12R5FIW79g4OQInvHqW76Sl7lBa+FI9kVIivJe5u5TQAOzS55m1y/8yD5dTSOYltx2nkTSlFv3tu6PrYafQM2OKnVrd8yxJwzeCQxUJBnXAWVPEwmJaUbEjwRNqHqyULW7j38hZnoZO/Dw55z3TQIg4FuYp7yeoYM2iM2QAOjFWuPS81j1GtIoG+fzP4Vu04guhPIbLBNZpE9MLu2nw67oLwjHSiwl4+BA2UHyg6UHSgf8oKnYsMbKV/qE8dG2YzWpXG0F4fecOBQ+jhK++PozqH0GVC6wWQLJD10qOBQwaGCQ4VDn6mprf9SeUB+o+FY4BgL3N6iwFacPhQLREN7MNB8uedowNGAowFHA3toILtkGrB2yr2fGPk1uhneOBqwQAOrnw6eBQeQwwGHAw4HHA4cxIH0knEAORw4jgO9nr0PUT40DqDwnDgQOBxwOOBwwOHAXi/JBaNAgpljgeMsEMfjsWMBC7+/Df3/xwKi2P4rUdWt/fUzHP8F</diagram></mxfile>
|
1
images/drawio/hierarchical_tree_committed.xml
Normal file
@ -0,0 +1 @@
|
||||
<mxfile userAgent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36" version="8.8.7" editor="www.draw.io" type="github"><diagram id="fff4becb-f500-b149-faf0-ef2686a80655" name="Page-1">7Vvdc+I2EP9rmGkfruMPzMFj4pDew3WmM5lpr4/CErYb2eJkEUz/+uoT22Bz5M5Y5OK8IK1Wq9WurN9viZn4YVb+TsEm+YNAhCeeA8uJ/zDxPNddOPxDSPZKMgs+KkFMU6iVKsFT+h/SQj0v3qYQFQ1FRghm6aYpjEieo4g1ZIBSsmuqrQlurroBMToRPEUAn0r/TiFLlHQeOJX8E0rjxKzsOnpkBaLnmJJtrtebeP5a/qnhDBhbWr9IACS7mshfTvyQEsJUKytDhEVsTdjUvMeO0YPfFOXsoglT7Qfbm70jyEOhu4SyhMQkB3hZSe/l/pCw4PBewjLMmy5vojJlX2rtf4TKb4Ho5Yzuv+gZslONQVAk0pqrO38CxhDNpcRzhPRfxNheHxOwZYSLKtc+E7LRs9ckZ1rNFabV5sSOGvEpyJZGRqRPJgM0RjpmbXF0D9nhpx6RDPE9cBWKMGDpS9M+0McvPuhVKeANnYWOjARjRloy4lvMyGzMSEtGPIsZ0f68ALxF5qKdYe7FPQMrHn9+ERMKEZVjs69bcZ3eu1WTK+wO9/phXFziQUOpSrkR6ml3Iuha31G6iQaEliHlzIeIYAw2BVI6h57REon5UMjMSBvBpqw7M4v1p9rnisB9q4evM0N7sAFFOnEa5w0bET8WPAPnw3nRQs9ob9biJ0MsN7AD6pxZdSHhz/xZD4SQvsn0moVW328C1KKwaovM8AnrYVdrQs7ta9VDcq3HRgmcMlgGoZXNtj43Qqgu2FO5AhgjPyIGDJWsifcFo+QZhQQThUb+3Fv5s5nwPcW4JocBmsOpQO0XRNdYVgVCpROyuRpfrpP4d4CxnuB7czVFV2hTXZBUsOi5cy1LaqWOKYF+CL7n9gnV97KlC4jYBYTKlH91QmVIjRVCtbCfEbsUty0j3tReRow/I8UdKe4QDowU98bJIP8EGb/g7qUh+Q2hkRwH6edhwJPQnyzm74YEO8vgcSTBw5FgQ3AtcGDj7IjvI76P+D7iu/gKS174Px+Ii3/qvhMAf3hwPSubfacAHiwsIrh3QwjeBd8XYHcncMsD0wt2n7V0+SNx1kzPT3vbWkODeJsPw+N4mxdXh3LbqX497LVZEZgOrWP69fbXG9idc/GNx8gp3bvFna04DcsLcpKjI0qgRVbYwOFL9EHogH9DdGAs6MeCfizo+6ntfqSgj6yD/1UKercnOLv9gn46tfTtxVjQK21/SAif3hCEd+H3BeDdidxnq6qxoh8r+rGi/4aVlXVAv+LeAO0J6l5dpr6dIDllGC6XtuL0rsr541dUfWdILhCccIGQZFnKfvn1lYn22rlfb7kwjw1GawknPORpBPCdFmcphPJ1zV2SMvS0AfKNxx0F4s2h5iuchx8jik6MQVHodkSyNNJtsb1HkKVYZOUvRCHIwVH+Pd2vsUhH/hk9vW23h0Ny9AqH9/HCM+L3cUZmJ2fkiQEmeGIGoiTN1RH4ZqaPn51GiD8h/IJERs8E9QpxDFriOO0njrxb/cBVjtV+Rewv/wc=</diagram></mxfile>
|
1
images/drawio/hierarchical_tree_updated.xml
Normal file
@ -0,0 +1 @@
|
||||
<mxfile userAgent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36" version="8.8.7" editor="www.draw.io" type="github"><diagram id="8c4bcf12-4946-e326-c0b4-7564ad0a553e" name="Page-1">7VtLc9s2EP41mmkP6fBhytLRVpTmkM50xpk2PUIkRLIGCQWELCq/vniSokQyUkIRsgtfBCwWC2B3ie9bmZr4i6z8nYBN8geOIJp4TlRO/PcTz3PducM+uGQvJdPgXgpikkZKqRY8pd+gEqp58TaNYNFQpBgjmm6awhDnOQxpQwYIwbum2hqj5qobEMMTwVMI0Kn07zSiiZTOAqeWf4RpnOiVXUeNrED4HBO8zdV6E89fiz85nAFtS+kXCYjw7kDkLyf+gmBMZSsrFxBx32q3yXkfOkarfROY07Mm+GofdK/PDiPmCtXFhCY4xjlAy1r6KM4HuQWH9RKaIdZ0WROWKf1y0P6Hq/wW8F5Oyf6LmiE69VgEikRYc1XnT0ApJLmQeA6X/gsp3as0AVuKmaje2ieMN2r2GudUqbnctDwcP1HDPwXeklCLpiq/AImh8pnnnPrRraLDsh7iDLIzMBUCEaDpS9M+UOkXV3p1CFhDRaEjInc2Ii0R8QxGJLARaYlIy10zWkTUfl4A2kJ90U4R28UjBSvmf3YRYxJBIsamX7f8On106yZT2FX3ejXOL/GgoVSHXAvVtAfudKXvSN1EAULLkNzMuxAjBDYFlDpVT2vxwLwrRGSEjWBTHm5mGqtPec4VjvatO7zMDBnARsTDidI4b9gIWVqwCPS786yFnuFer8Uygy838gZknhndQsKe+d4dcCF5leHVC61+3AQ48MKqzTPjB2yAU60x7jvXaoDgGveNEiz8yXxm5LCtzw0Xygv2VC4BRsuPiAGFJW3ifUEJfoYLjLBEIz+arqbBlO89RehAzooELww5ar9AskaiKuAqnZDN1NhyncS/A4zVBN+bySmqQrtTlLeGRc+dKVlyUOroEuin4PvePKH6UbZ0BhE7h1DNTwmVJjVGCNXMfEQMU9yWiHi+wYjMLcW1FNdSXEtxaxMgYxfcozAkviHUkmMnvR0GbJAXWhL89kmwJrgGOLD+itniu8V3i+8W3/lXWOLCf3sgzv+pawHcAvgVADyYG0Rw94YQvAu+z8DuTuAWCTMIdvdaOv+R6DUz8NPettbYIN62h/FxvG0XV4dy06G+HPbarHBMj4xj+vXO53p9h7sA7Pq2+Mp95JTuw/zBlJ/G5QU5zuERJVAiI2ygKrJHoQPeDdEBW9Dbgt4W9MPUdj9T0IfGwf8qBb07EJzZgv5GgPtmC3rXHxPC/RuC8C78PgO8O5G7t6qyFb2t6G1F/x0rK+OAfsWzATIQ1F1cpr4eJznlYrFcmvLT/6qcP35F1XfG5AJ3nVxgVUXgPUQpO+Ln8pfNlvIFZT44Qp8B9q8X5oTXRhPbFgnrRdxBFhkwN/RjjOBawBtLgTQE6EGJszSKxOujuySl8GkDxBuYOwL4m0zNV0qrH0fyToxAUah2iLM0VG1+vA8gSxHPkr8giUAOjvLRU/0DVuuIP62nju0OkLRHr5R492fmrD9Ezp7+LOqJAsp5awbCJM1lCnw30sfPcsPFHyF6gTyiPU69gh+DFj/eDeNH1q1/cCvGDn7V7C//Aw==</diagram></mxfile>
|
1
images/drawio/mempool.xml
Normal file
@ -0,0 +1 @@
|
||||
<mxfile userAgent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36" version="8.8.7" editor="www.draw.io" type="github"><diagram id="36dfdee5-4105-c6d7-1df0-44aa39a8ce4e" name="Page-1">7Vpbd6M2EP41fsweEAbjx8TddPuwPTnNnnb3UQYZ0wjkyji2++srgQToQuw4YGdT+8UwErrMfPPNjGDkzbLdrxSull9JjPAIOPFu5P0yAsB1pw7745J9JQn8SSVIaBqLTo3gMf0XCaF4LtmkMVorHQtCcJGuVGFE8hxFhSKDlJKt2m1BsDrrCibIEDxGEJvSv9K4WFbS0Hca+ReUJks5s+uIljmMnhJKNrmYbwS8RfmrmjMoxxL910sYk21L5H0eeTNKSFFdZbsZwly3Um3Vc/cdrfW6KcqLYx4A1QPPEG+QXHG5rmIvdVHuBvH+7si72y7TAj2uYMRbt8z6TLYsMiya6/047CbBcL0W1xHJ0khcrwtKntCMYELLKTyn/NUtUuV8wEWKseyZkxzxYSmMU7Y/TbwgeXEPsxRzzH1B+BkVaQRFg4AY8MW9bXKI0yRnMowWTHd3QjOIFmjXqV23thnzBUQyVNA96yIeAI54RLiBdIttg6nAm1ayZQtPnvQDKHCc1EM3tmQXwpx203oW0waY7yxOnxUTB/9sON7u2EaLG6GFW+5dbJuINu3sKhH/5ThzqkvYmsrBpbQTSs5QUIqDeeAHB6FU+SWIoj6BI/TFFAzXy9pj+sARmCo4coEJJCDJqQ2kWvgWII1fwxGDGTYEcy84yrCxj8J4PLxhuUnZePhW9MrSOOYK6cPifuBc0OK+YfFviNmXZilbPXBmhKI2B1iohGv0Zl2qmDOJC1a7iiYMKtGQxDlIg4uCBEn2ZlgwjNVpHxs+VQR3QlQBQBuvx2DNBS9grewn9OD2gCBv4nzyFQwxiYGhqSX6gB4QFBgIeixggcoUKFqm+RU/7x0/E2esoQdIPLXxY6Eg1387fiadyQvfZ5n5SxU0kJDKaImOgJbfAa0HxHMfhxHfipS8V6c41RIO5jiXTpeb2uNIeFlC4X35s3lHH2mNr1OUOzYpKrRALOiBosLzQszA11eUrVhNe8VV77jyQudiqJoaqJphXqy+zZ56hgtRuIhsJgyiEM0XFv33oFa1BrEVs3WW2rdWZT18MWf942F2jQXnjQU2gA3lta5rwZdmTGb8W360yDfOrcNt0mm/19hJP88a1lZolxbfxcz8+gdf8Sc+BOtB99/FBsqbpq3SBoqNY1PNvkxjZEMjpDBGAWmCZC/HDoOWkW1lrZRRhGGRPqursFlezPBQuatE2VgH2VhHT7V88Vj74FQbaWqMpJ/ZVbs2RiqhWG/8OHR2H+UJ9js9y/2dxKiTzl5VonXNYDjShy7RXjpO6r1EG6shOQgNwgwGqs9cs8A/b0C+vZv9do3IQ0ZkX8uigeUIcrB4bKv//0/xGKgBuYnPP+oNQVrI/Qt6LGX3Kddq2UclvNNCeGgJ4cElQ7gbGOeajkZox8ZwcHiojiDOFA/3rW4lB627Fx242gsc39fcoRrx1AwBHKyPFjBSWfZPRGOYw1dyMbBx8WyJoqdvu2M5+J3H/15Dfg88PDZSTdczmXhs8bg+DvLBR6uMul/unbMympi0Gl6SVW/UDxVOLou0jHTAosj6fcMVmFXbhDf+vclWYr6gp2xAmvOK29PyAE+bx9M/zzrQ3w17zhsOlnEnpgQ52rK2j58TyCzqrBlB/T6jzlrNfMAPB8oHzBccL9HuHJPo6acmXVmLSd5ts65rpgNs760KTDlL6oWAg3dGwN5YZWA3PJGBvfDAQD1RsD/VKHWiUPCbKdW7aCkW8VKstOp6g48+FftZ2fcUR1+zJaR5wqRgsPosHKw+Y7fNx+EVOpsv8L3P/wE=</diagram></mxfile>
|
1
images/drawio/state_machine.xml
Normal file
@ -0,0 +1 @@
|
||||
<mxfile userAgent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36" version="8.8.6" editor="www.draw.io" type="device"><diagram id="817c6f60-e6d3-5cde-b963-fc17ec91567f" name="Page-1">7Zpdc6MgFIZ/TS63E0XT5LLN9uNmZzqTi+1eUiWGKZEskq/99QsKUUGnNsUm201uigc4CO/jOUgdgOly98DgavGDxogM/GG8G4DvA9/3vMlQ/JGWfWEZeaAwJAzHqlFpmOE/SBlVv2SNY5TVGnJKCcerujGiaYoiXrNBxui23mxOSX3UFUyQZZhFkNjWnzjmi8I6Doel/RHhZKFH9oaq5gVGrwmj61SNN/DBPP8V1Uuofan22QLGdFsxgbsBmDJKeVFa7qaIyLXVy1b0u2+pPdw3Qynv0sEvOmwgWaupzwb+iIi+t9n6RRQTWXzVNuGoYlZT4Hu9bPnEkXQ9FNXbBeZotoKRrN0KUIRtwZdEXHnSv566bJsQmGWqHNEljlQ544y+oikllOVDgGH+O9RodaTDOSak0jIO0TgOpJ2m/B4uMZEwPiKyQRxHUFUo9rxQXTcNBQlOUmGLxKIidph4dZXVwm8Q42hXMalVf0B0iTjbiyaq1tfEqCfE00RsS94CZVpUUNM2qAhPDp5LlUVBCd0sOugq+q13Ed618OCEwo8t4S0lURrfyAAqZy2lkYK0itcg0mjUIJLfixhoh/mzui1Z/iXv6kq6EC3Y/lndZH5R1rXB0iptRtcsQrVwySFLEK89TCiuZRRb/oq8YYO82sYQgRxv6nmoSXM1whPF4n7LsOLX6RLrV3dRzEb1qiYG09G1genYcFSsgeUoR/Aw7U5Uet2S0PASi1zHouB0ocizk9DXjEXvjjdnEknGBhum6F0Diee/4chhIAm+FlMus1g1ZflnBZo3ckUaMByF/ZEWNpBWZCcpbw250e811RXfshyEG9FAqLQrK3U2u7q6qqS5wldLnhMxnhvs1nBNaYoMUpTJIk1mDAEruVEVSxzHcpjG7FnPr50S6IefDQe5zopDoFuy810kO3vj3fbG5V92Oe53OSfc5kw6Kw/OXnnfVn4+jlAU9aJ8DLNFPk1PXTxBLmrS3CLipxs4whPCoYfqAEdwgeO9cKwQw0ISxOQC4DRR83HAzOiUzHgWM//0HtfpGU6VW7EI95joKXfaF+s0fSYbY2C+nh+9MTbOckB/r2Caxv7wnE7bw8+Z49kKoQGcQaXaQlTPIQtST3bIaIDpTybHgWmehVuOHILZ+3nTfwfm4X+YVTAnpwQzcAWmuSvsEUxwSehvgWlRaJF6KuBGrlL0tbml7C9FAztFt711hJe3juNeSa13iU7MtrHwmZ8GdEiTQsCVLEaMSu3e0vuI88IPHEUdWiYMxhiVwurjUOdQMMpFCKHSFsiOMFsVnyTN8U5O2wUSQRjWkWg4vPQbkHBxeBl02NJfkPh0JMLw2ogSk76QEJflF2lFxik/+wN3fwE=</diagram></mxfile>
|
BIN
images/hierarchical_tree_basic.png
Normal file
After Width: | Height: | Size: 154 KiB |
BIN
images/hierarchical_tree_committed.png
Normal file
After Width: | Height: | Size: 178 KiB |
BIN
images/hierarchical_tree_updated.png
Normal file
After Width: | Height: | Size: 178 KiB |
BIN
images/keys_commit.png
Normal file
After Width: | Height: | Size: 136 KiB |
BIN
images/keys_delivertx.png
Normal file
After Width: | Height: | Size: 128 KiB |
BIN
images/mempool.png
Normal file
After Width: | Height: | Size: 218 KiB |
BIN
images/state_machine.png
Normal file
After Width: | Height: | Size: 38 KiB |
11
tmdemoapp/build.sbt
Normal file
@ -0,0 +1,11 @@
|
||||
name := "tmdemoapp"
|
||||
|
||||
version := "0.1"
|
||||
|
||||
scalaVersion := "2.12.6"
|
||||
|
||||
libraryDependencies += "com.github.jtendermint" % "jabci" % "0.17.1"
|
||||
|
||||
libraryDependencies += "org.bouncycastle" % "bcpkix-jdk15on" % "1.56"
|
||||
|
||||
libraryDependencies += "com.google.code.gson" % "gson" % "2.8.5"
|
1
tmdemoapp/project/build.properties
Normal file
@ -0,0 +1 @@
|
||||
sbt.version = 1.1.5
|
128
tmdemoapp/src/main/scala/kvstore/ABCIHandler.scala
Normal file
@ -0,0 +1,128 @@
|
||||
package kvstore
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
import com.github.jtendermint.jabci.api._
|
||||
import com.github.jtendermint.jabci.types.{ResponseCheckTx, _}
|
||||
import com.google.protobuf.ByteString
|
||||
|
||||
import scala.util.Try
|
||||
import scala.util.matching.Regex
|
||||
|
||||
/**
|
||||
* Tendermint establishes 3 socket connections with the app:
|
||||
* - Mempool for CheckTx
|
||||
* - Consensus for DeliverTx and Commit
|
||||
* - Info for Query
|
||||
*
|
||||
* According to specification the app maintains separate in-memory states for every connections:
|
||||
* – [[ABCIHandler.consensusRoot]]: the latest state modified with every successful transaction on DeliverTx
|
||||
* – [[ABCIHandler.storage]]: array of committed snapshots for Info connection
|
||||
* – [[ABCIHandler.mempoolRoot]]: the latest committed snapshot for Mempool connection (not used currently)
|
||||
*/
|
||||
class ABCIHandler(val serverIndex: Int) extends IDeliverTx with ICheckTx with ICommit with IQuery {
|
||||
@volatile var state: BlockchainState = BlockchainState()
|
||||
|
||||
private val storage: CopyOnWriteArrayList[Node] = new CopyOnWriteArrayList[Node]()
|
||||
@volatile private var mempoolRoot: Node = Node.emptyNode
|
||||
|
||||
|
||||
override def requestCheckTx(req: RequestCheckTx): ResponseCheckTx = {
|
||||
// no transaction processing logic currently
|
||||
// mempoolRoot is intended to be used here as the latest committed state
|
||||
|
||||
val tx = req.getTx.toStringUtf8
|
||||
if (tx == "BAD_CHECK") {
|
||||
System.out.println(s"CheckTx BAD: $tx")
|
||||
ResponseCheckTx.newBuilder.setCode(CodeType.BAD).setLog("BAD_CHECK").build
|
||||
} else {
|
||||
System.out.println(s"CheckTx OK: $tx")
|
||||
ResponseCheckTx.newBuilder.setCode(CodeType.OK).build
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var consensusRoot: Node = Node.emptyNode
|
||||
private var currentBlockHasTransactions: Boolean = false
|
||||
|
||||
private val binaryOpPattern: Regex = "(.+)=(.+)\\((.*),(.*)\\)".r
|
||||
private val unaryOpPattern: Regex = "(.+)=(.+)\\((.*)\\)".r
|
||||
private val plainValuePattern: Regex = "(.+)=(.*)".r
|
||||
|
||||
override def receivedDeliverTx(req: RequestDeliverTx): ResponseDeliverTx = {
|
||||
currentBlockHasTransactions = true
|
||||
|
||||
val tx = req.getTx.toStringUtf8
|
||||
System.out.println(s"DeliverTx: $tx")
|
||||
val txPayload = tx.split("###")(0)
|
||||
|
||||
val result = txPayload match {
|
||||
case response@"BAD_DELIVER" => Left(response)
|
||||
case binaryOpPattern(key, op, arg1, arg2) =>
|
||||
op match {
|
||||
case "sum" => SumOperation(arg1, arg2)(consensusRoot, key)
|
||||
case _ => Left("Unknown binary op")
|
||||
}
|
||||
case unaryOpPattern(key, op, arg) =>
|
||||
op match {
|
||||
case "copy" => CopyOperation(arg)(consensusRoot, key)
|
||||
case "increment" => IncrementOperation(arg)(consensusRoot, key)
|
||||
case "factorial" => FactorialOperation(arg)(consensusRoot, key)
|
||||
case "hiersum" => HierarchicalSumOperation(arg)(consensusRoot, key)
|
||||
case _ => Left("Unknown unary op")
|
||||
}
|
||||
case plainValuePattern(key, value) =>
|
||||
if (key == "wrong" && value.contains(serverIndex.toString))
|
||||
SetValueOperation("wrong" + value)(consensusRoot, key)
|
||||
else
|
||||
SetValueOperation(value)(consensusRoot, key)
|
||||
case key => SetValueOperation(key)(consensusRoot, key)
|
||||
}
|
||||
|
||||
result match {
|
||||
case Right((newRoot, info)) =>
|
||||
consensusRoot = newRoot
|
||||
ResponseDeliverTx.newBuilder.setCode(CodeType.OK).setInfo(info).build
|
||||
case Left(message) => ResponseDeliverTx.newBuilder.setCode(CodeType.BAD).setLog(message).build
|
||||
}
|
||||
}
|
||||
|
||||
override def requestCommit(requestCommit: RequestCommit): ResponseCommit = {
|
||||
consensusRoot = consensusRoot.merkelize()
|
||||
|
||||
val buffer = ByteBuffer.wrap(consensusRoot.merkleHash.get)
|
||||
|
||||
storage.add(consensusRoot)
|
||||
mempoolRoot = consensusRoot
|
||||
|
||||
state = BlockchainState(storage.size, consensusRoot.merkleHash, state.lastAppHash, currentBlockHasTransactions, System.currentTimeMillis())
|
||||
currentBlockHasTransactions = false
|
||||
System.out.println(s"Commit: height=${state.lastCommittedHeight} hash=${state.lastAppHash.map(MerkleUtil.merkleHashToHex).getOrElse("EMPTY")}")
|
||||
|
||||
ResponseCommit.newBuilder.setData(ByteString.copyFrom(buffer)).build
|
||||
}
|
||||
|
||||
|
||||
override def requestQuery(req: RequestQuery): ResponseQuery = {
|
||||
val height = Try(req.getHeight.toInt).toOption.filter(_ > 0).getOrElse(state.lastCommittedHeight)
|
||||
val root = storage.get(height - 1) // storage is 0-indexed, but heights are 1-indexed
|
||||
val getPattern = "get:(.*)".r
|
||||
val lsPattern = "ls:(.*)".r
|
||||
|
||||
val query = req.getData.toStringUtf8
|
||||
val (resultKey, result) = query match {
|
||||
case getPattern(key) => (key, root.getValue(key))
|
||||
case lsPattern(key) => (key, root.listChildren(key).map(x => x.mkString(" ")))
|
||||
case _ =>
|
||||
return ResponseQuery.newBuilder.setCode(CodeType.BAD).setLog("Invalid query path. Got " + query).build
|
||||
}
|
||||
|
||||
val proof = if (result.isDefined && req.getProve) MerkleUtil.twoLevelMerkleListToString(root.getProof(resultKey)) else ""
|
||||
|
||||
ResponseQuery.newBuilder.setCode(CodeType.OK)
|
||||
.setValue(ByteString.copyFromUtf8(result.getOrElse("")))
|
||||
.setProof(ByteString.copyFromUtf8(proof))
|
||||
.build
|
||||
}
|
||||
}
|
9
tmdemoapp/src/main/scala/kvstore/BlockchainState.scala
Normal file
@ -0,0 +1,9 @@
|
||||
package kvstore
|
||||
|
||||
case class BlockchainState(
|
||||
lastCommittedHeight: Int = 0,
|
||||
lastAppHash: Option[MerkleHash] = None,
|
||||
lastVerifiableAppHash: Option[MerkleHash] = None,
|
||||
lastBlockHasTransactions: Boolean = false,
|
||||
lastBlockTimestamp: Long = 0
|
||||
)
|
12
tmdemoapp/src/main/scala/kvstore/ClusterUtil.scala
Normal file
@ -0,0 +1,12 @@
|
||||
package kvstore
|
||||
|
||||
object ClusterUtil {
|
||||
val defaultABCIPort: Int = 46658
|
||||
|
||||
def abciPortToServerIndex(port: Int): Int = (port - 46058) / 100
|
||||
|
||||
def localRPCAddress(localServerIndex: Int): String = s"http://localhost:46${localServerIndex}57"
|
||||
|
||||
def peerRPCAddresses(localServerIndex: Int): Stream[String] =
|
||||
(0 until 4).toStream.filter(_ != localServerIndex).map(index => s"http://localhost:46${index}57")
|
||||
}
|
25
tmdemoapp/src/main/scala/kvstore/MerkleUtil.scala
Normal file
@ -0,0 +1,25 @@
|
||||
package kvstore
|
||||
|
||||
import org.bouncycastle.jcajce.provider.digest.SHA3
|
||||
|
||||
sealed trait MerkleMergeRule
|
||||
case object BinaryBasedMerkleMerge extends MerkleMergeRule
|
||||
case object HexBasedMerkleMerge extends MerkleMergeRule
|
||||
|
||||
object MerkleUtil {
|
||||
val merkleSize: Int = 32
|
||||
|
||||
def singleHash(data: String): MerkleHash = new SHA3.Digest256().digest(data.getBytes)
|
||||
|
||||
def mergeMerkle(parts: List[MerkleHash], mergeRule: MerkleMergeRule): MerkleHash =
|
||||
mergeRule match {
|
||||
case BinaryBasedMerkleMerge => new SHA3.Digest256().digest(parts.fold(Array.emptyByteArray)(_ ++ _))
|
||||
case HexBasedMerkleMerge => new SHA3.Digest256().digest(parts.map(merkleHashToHex).mkString(" ").getBytes)
|
||||
}
|
||||
|
||||
def merkleHashToHex(merkleHash: MerkleHash): String =
|
||||
merkleHash.map("%02x".format(_)).mkString
|
||||
|
||||
def twoLevelMerkleListToString(list: List[List[MerkleHash]]): String =
|
||||
list.map(level => level.map(MerkleUtil.merkleHashToHex).mkString(" ")).mkString(", ")
|
||||
}
|
109
tmdemoapp/src/main/scala/kvstore/Node.scala
Normal file
@ -0,0 +1,109 @@
|
||||
package kvstore
|
||||
|
||||
import kvstore.MerkleUtil._
|
||||
|
||||
import scala.collection.immutable.HashMap
|
||||
import scala.util.Try
|
||||
import scala.util.matching.Regex
|
||||
|
||||
/**
|
||||
* Node
|
||||
*
|
||||
* merkleHash set to None when branch changed. Later None merkle hashes recalculated when [[Node.merkelize]] invoked
|
||||
*
|
||||
* @param children child nodes
|
||||
* @param value assigned value, if exists
|
||||
* @param merkleHash Merkle hash of a node's subtree, if calculated
|
||||
*/
|
||||
case class Node(children: NodeStorage, value: Option[String], merkleHash: Option[MerkleHash]) {
|
||||
def merkelize(): Node =
|
||||
if (merkleHash.isDefined)
|
||||
this
|
||||
else {
|
||||
val newChildren = children.mapValues(_.merkelize())
|
||||
val withNewChildren = Node(newChildren, value, None)
|
||||
Node(newChildren, value, Some(mergeMerkle(withNewChildren.merkleItems(), HexBasedMerkleMerge)))
|
||||
}
|
||||
|
||||
private def merkleItems(): List[MerkleHash] =
|
||||
singleHash(value.getOrElse("")) :: children.flatMap(x => List(singleHash(x._1), x._2.merkleHash.get)).toList
|
||||
|
||||
def getProof(key: String): List[List[MerkleHash]] =
|
||||
if (key.isEmpty)
|
||||
List(merkleItems())
|
||||
else {
|
||||
val (next, rest) = splitPath(key)
|
||||
merkleItems() :: children(next).getProof(rest)
|
||||
}
|
||||
|
||||
def longValue: Option[Long] = value.flatMap(x => Try(x.toLong).toOption)
|
||||
|
||||
val rangeKeyValuePattern: Regex = "(\\d{1,8})-(\\d{1,8}):(.+)".r
|
||||
|
||||
def add(key: String, value: String): Node =
|
||||
key match {
|
||||
case rangeKeyValuePattern(rangeStartStr, rangeEndStr, keyPattern) =>
|
||||
// range pattern allows to set multiple keys in single transaction
|
||||
// range defined by starting and ending index, key pattern may contains hexadecimal digits of current index
|
||||
|
||||
val rangeStart = rangeStartStr.toInt
|
||||
val rangeEnd = rangeEndStr.toInt
|
||||
System.out.println(s"setting range from=$rangeStart to=$rangeEnd keyPattern=$keyPattern valuePattern=$value")
|
||||
|
||||
var currentNode = this
|
||||
for (index <- rangeStart until rangeEnd) {
|
||||
var key = keyPattern
|
||||
var effectiveValue = value
|
||||
for (hexPosition <- 0 to 6) {
|
||||
val target = "@" + hexPosition
|
||||
val replacement = ((index >> hexPosition * 4) & 0xf).toHexString
|
||||
key = key.replace(target, replacement)
|
||||
effectiveValue = effectiveValue.replace(target, replacement)
|
||||
}
|
||||
System.out.println(s"setting key=$key value=$effectiveValue")
|
||||
currentNode = currentNode.addValue(key, effectiveValue)
|
||||
}
|
||||
|
||||
currentNode
|
||||
case _ =>
|
||||
System.out.println(s"setting key=$key value=$value")
|
||||
addValue(key, value)
|
||||
}
|
||||
|
||||
private def addValue(key: String, value: String): Node =
|
||||
if (key.isEmpty)
|
||||
Node(children, Some(value), None)
|
||||
else {
|
||||
val (next, rest) = splitPath(key)
|
||||
Node(children + (next -> children.getOrElse(next, Node.emptyNode).addValue(rest, value)), this.value, None)
|
||||
}
|
||||
|
||||
def getNode(key: String): Option[Node] =
|
||||
if (key.isEmpty)
|
||||
Some(this)
|
||||
else {
|
||||
val (next, rest) = splitPath(key)
|
||||
children.get(next).flatMap(_.getNode(rest))
|
||||
}
|
||||
|
||||
def getValue(key: String): Option[String] = getNode(key).flatMap(_.value)
|
||||
|
||||
def getLongValue(key: String): Option[Long] = getNode(key).flatMap(_.longValue)
|
||||
|
||||
def listChildren(key: String): Option[List[String]] =
|
||||
if (key.isEmpty)
|
||||
Some(children.keys.toList)
|
||||
else {
|
||||
val (next, rest) = splitPath(key)
|
||||
children.get(next).flatMap(_.listChildren(rest))
|
||||
}
|
||||
|
||||
private def splitPath(path: String): (String, String) = {
|
||||
val (next, rest) = path.span(_ != '/')
|
||||
(next, rest.replaceFirst("/", ""))
|
||||
}
|
||||
}
|
||||
|
||||
object Node {
|
||||
val emptyNode: Node = Node(HashMap.empty[String, Node], None, None)
|
||||
}
|
75
tmdemoapp/src/main/scala/kvstore/Operation.scala
Normal file
@ -0,0 +1,75 @@
|
||||
package kvstore
|
||||
|
||||
trait Operation {
|
||||
def apply(root: Node, targetKey: String): Either[String, (Node, String)]
|
||||
}
|
||||
|
||||
case class SetValueOperation(value: String) extends Operation {
|
||||
override def apply(root: Node, targetKey: String): Either[String, (Node, String)] = {
|
||||
System.out.println(s"process set value=$value")
|
||||
Right(root.add(targetKey, value), value)
|
||||
}
|
||||
}
|
||||
|
||||
case class CopyOperation(arg: String) extends Operation {
|
||||
override def apply(root: Node, targetKey: String): Either[String, (Node, String)] = {
|
||||
System.out.println(s"process copy arg=$arg")
|
||||
root.getValue(arg)
|
||||
.toRight("Wrong argument")
|
||||
.map(value => (root.add(targetKey, value), value))
|
||||
}
|
||||
}
|
||||
|
||||
case class IncrementOperation(arg: String) extends Operation {
|
||||
override def apply(root: Node, targetKey: String): Either[String, (Node, String)] = {
|
||||
System.out.println(s"process increment arg=$arg")
|
||||
root.getLongValue(arg)
|
||||
.map(value => (value.toString, (value + 1).toString))
|
||||
.toRight("Wrong argument")
|
||||
.map(values => (root.add(targetKey, values._1).add(arg, values._2), values._1))
|
||||
}
|
||||
}
|
||||
|
||||
case class FactorialOperation(arg: String) extends Operation {
|
||||
override def apply(root: Node, targetKey: String): Either[String, (Node, String)] = {
|
||||
System.out.println(s"process factorial arg=$arg")
|
||||
root.getLongValue(arg)
|
||||
.filter(_ >= 0)
|
||||
.map(value => (1L to value).product.toString)
|
||||
.toRight("Wrong argument")
|
||||
.map(factorial => (root.add(targetKey, factorial), factorial))
|
||||
}
|
||||
}
|
||||
|
||||
case class HierarchicalSumOperation(arg: String) extends Operation {
|
||||
override def apply(root: Node, targetKey: String): Either[String, (Node, String)] = {
|
||||
System.out.println(s"process hierarchical sum arg=$arg")
|
||||
root.getNode(arg)
|
||||
.flatMap(calculate)
|
||||
.map(_.toString)
|
||||
.toRight("Wrong argument")
|
||||
.map(sum => (root.add(targetKey, sum), sum))
|
||||
}
|
||||
|
||||
private def calculate(node: Node): Option[Long] = {
|
||||
for {
|
||||
nodeValue <- node.longValue.orElse(Some(0L))
|
||||
childrenValues <- node.children.values.foldLeft(Option(0L))((acc, node) => acc.flatMap(x => calculate(node).map(_ + x)))
|
||||
} yield nodeValue + childrenValues
|
||||
}
|
||||
}
|
||||
|
||||
case class SumOperation(arg1: String, arg2: String) extends Operation {
|
||||
override def apply(root: Node, targetKey: String): Either[String, (Node, String)] = {
|
||||
System.out.println(s"process sum arg1=$arg1 arg2=$arg2")
|
||||
val value = for {
|
||||
arg1Value <- root.getLongValue(arg1)
|
||||
arg2Value <- root.getLongValue(arg2)
|
||||
} yield arg1Value + arg2Value
|
||||
value
|
||||
.map(_.toString)
|
||||
.toRight("Wrong arguments")
|
||||
.map(sum => (root.add(targetKey, sum), sum))
|
||||
}
|
||||
}
|
||||
|
102
tmdemoapp/src/main/scala/kvstore/ServerMonitor.scala
Normal file
@ -0,0 +1,102 @@
|
||||
package kvstore
|
||||
|
||||
import java.io.{DataOutputStream, InputStreamReader}
|
||||
import java.net.{HttpURLConnection, URL}
|
||||
|
||||
import com.google.gson.{GsonBuilder, JsonParser}
|
||||
|
||||
import scala.collection.JavaConverters._
|
||||
import scala.util.Try
|
||||
|
||||
/**
|
||||
* ABCI app monitor
|
||||
* Signals 'DISAGREEMENT WITH CLUSTER QUORUM!' when local node and cluster quorum have different app hashes
|
||||
* Signals 'NO CLUSTER QUORUM!' when cluster cannot make a new block verifying latest app hash in appropriate time
|
||||
*
|
||||
* @param handler handler to monitor
|
||||
*/
|
||||
class ServerMonitor(handler: ABCIHandler) extends Runnable {
|
||||
private val judgeEndpoint = "http://localhost:8080"
|
||||
|
||||
private val monitorPeriod = 1000
|
||||
private val checkEmptyBlockThreshold = 5000
|
||||
private val checkForLocalEmptyBlockAlertThreshold = 15000
|
||||
|
||||
private val parser = new JsonParser()
|
||||
private val gson = new GsonBuilder().create()
|
||||
|
||||
override def run(): Unit = {
|
||||
while (true) {
|
||||
Thread.sleep(monitorPeriod)
|
||||
|
||||
val state = handler.state
|
||||
val clusterHash = getAppHashFromPeers(state.lastCommittedHeight).getOrElse("")
|
||||
val localHash = state.lastVerifiableAppHash.map(MerkleUtil.merkleHashToHex).getOrElse("")
|
||||
|
||||
if (clusterHash != localHash) {
|
||||
throw new IllegalStateException(s"Cluster quorum has unexpected app hash for previous block '$clusterHash' '$localHash'")
|
||||
}
|
||||
|
||||
val timeWaiting = timeWaitingForEmptyBlock(state)
|
||||
val status = if (timeWaiting <= checkEmptyBlockThreshold)
|
||||
"OK"
|
||||
else {
|
||||
val nextClusterHash = getAppHashFromPeers(state.lastCommittedHeight + 1)
|
||||
|
||||
if (nextClusterHash.isEmpty) {
|
||||
"No quorum"
|
||||
} else if (nextClusterHash != state.lastAppHash.map(MerkleUtil.merkleHashToHex)) {
|
||||
"Disagreement with quorum"
|
||||
} else if (timeWaiting > checkForLocalEmptyBlockAlertThreshold) {
|
||||
throw new IllegalStateException("Cluster quorum committed correct block without local Tendermint")
|
||||
} else {
|
||||
"OK"
|
||||
}
|
||||
}
|
||||
|
||||
submitToJudge(state, status)
|
||||
}
|
||||
}
|
||||
|
||||
def timeWaitingForEmptyBlock(state: BlockchainState): Long =
|
||||
if (state.lastBlockHasTransactions) System.currentTimeMillis() - state.lastBlockTimestamp else 0L
|
||||
|
||||
def getAppHashFromPeers(height: Int): Option[String] =
|
||||
ClusterUtil.peerRPCAddresses(handler.serverIndex)
|
||||
.map(address => getAppHashFromPeer(address, height))
|
||||
.find(_.isDefined)
|
||||
.flatten
|
||||
|
||||
def getAppHashFromPeer(peerAddress: String, height: Int): Option[String] =
|
||||
Try(new URL(peerAddress + "/block?height=" + height).openStream).toOption
|
||||
.map(input => parser.parse(new InputStreamReader(input, "UTF-8")))
|
||||
.filter(response => !response.getAsJsonObject.has("error"))
|
||||
.map(response => response.getAsJsonObject.get("result")
|
||||
.getAsJsonObject.get("block_meta")
|
||||
.getAsJsonObject.get("header")
|
||||
.getAsJsonObject.get("app_hash")
|
||||
.getAsString
|
||||
.toLowerCase)
|
||||
|
||||
def submitToJudge(state: BlockchainState, status: String): Unit = {
|
||||
val submitPath = judgeEndpoint + "/submit/" + handler.serverIndex
|
||||
|
||||
val connection = new URL(submitPath).openConnection().asInstanceOf[HttpURLConnection]
|
||||
connection.setRequestMethod("POST")
|
||||
connection.setRequestProperty("Content-Type", "application/json")
|
||||
connection.setDoOutput(true)
|
||||
|
||||
val out = new DataOutputStream(connection.getOutputStream)
|
||||
|
||||
out.writeBytes(gson.toJson(mapAsJavaMap(Map(
|
||||
"status" -> status,
|
||||
"height" -> state.lastCommittedHeight,
|
||||
"app_hash" -> state.lastAppHash.map(MerkleUtil.merkleHashToHex).getOrElse("empty").toUpperCase)
|
||||
)))
|
||||
out.flush()
|
||||
out.close()
|
||||
|
||||
connection.getResponseCode
|
||||
connection.disconnect()
|
||||
}
|
||||
}
|
28
tmdemoapp/src/main/scala/kvstore/ServerRunner.scala
Normal file
@ -0,0 +1,28 @@
|
||||
package kvstore
|
||||
|
||||
import com.github.jtendermint.jabci.socket.TSocket
|
||||
|
||||
object ServerRunner {
|
||||
|
||||
def main(args: Array[String]): Unit = {
|
||||
val port = if (args.length > 0) args(0).toInt else ClusterUtil.defaultABCIPort
|
||||
ServerRunner.start(port)
|
||||
}
|
||||
|
||||
def start(port: Int): Unit = {
|
||||
System.out.println("starting KVStore")
|
||||
val socket = new TSocket
|
||||
|
||||
val abciHandler = new ABCIHandler(ClusterUtil.abciPortToServerIndex(port))
|
||||
socket.registerListener(abciHandler)
|
||||
|
||||
val monitorThread = new Thread(new ServerMonitor(abciHandler))
|
||||
monitorThread.setName("Monitor")
|
||||
monitorThread.start()
|
||||
|
||||
val socketThread = new Thread(() => socket.start(port))
|
||||
socketThread.setName("Socket")
|
||||
socketThread.start()
|
||||
socketThread.join()
|
||||
}
|
||||
}
|
6
tmdemoapp/src/main/scala/kvstore/package.scala
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
|
||||
package object kvstore {
|
||||
type NodeStorage = Map[String, Node]
|
||||
type MerkleHash = Array[Byte]
|
||||
}
|
7
tools/local-cluster-delete.sh
Executable file
@ -0,0 +1,7 @@
|
||||
CWD=`dirname $0`
|
||||
$CWD/local-cluster-stop.sh
|
||||
|
||||
rm -rf ~/.tendermint/cluster4/1
|
||||
rm -rf ~/.tendermint/cluster4/2
|
||||
rm -rf ~/.tendermint/cluster4/3
|
||||
rm -rf ~/.tendermint/cluster4/4
|
23
tools/local-cluster-init.sh
Executable file
@ -0,0 +1,23 @@
|
||||
tendermint init --home $HOME/.tendermint/cluster4/1
|
||||
tendermint init --home $HOME/.tendermint/cluster4/2
|
||||
tendermint init --home $HOME/.tendermint/cluster4/3
|
||||
tendermint init --home $HOME/.tendermint/cluster4/4
|
||||
echo "node1: `tendermint show_node_id --home $HOME/.tendermint/cluster4/1`"
|
||||
echo "node2: `tendermint show_node_id --home $HOME/.tendermint/cluster4/2`"
|
||||
echo "node3: `tendermint show_node_id --home $HOME/.tendermint/cluster4/3`"
|
||||
echo "node4: `tendermint show_node_id --home $HOME/.tendermint/cluster4/4`"
|
||||
|
||||
TM_VALIDATOR1='{"pub_key":'$(tendermint show_validator --home $HOME/.tendermint/cluster4/1)',"power":10,"name":""}'
|
||||
TM_VALIDATOR2='{"pub_key":'$(tendermint show_validator --home $HOME/.tendermint/cluster4/2)',"power":10,"name":""}'
|
||||
TM_VALIDATOR3='{"pub_key":'$(tendermint show_validator --home $HOME/.tendermint/cluster4/3)',"power":10,"name":""}'
|
||||
TM_VALIDATOR4='{"pub_key":'$(tendermint show_validator --home $HOME/.tendermint/cluster4/4)',"power":10,"name":""}'
|
||||
TM_VALIDATORS=$TM_VALIDATOR1,$TM_VALIDATOR2,$TM_VALIDATOR3,$TM_VALIDATOR4
|
||||
sed -i -e 's#'$TM_VALIDATOR1'#'$TM_VALIDATORS'#g' $HOME/.tendermint/cluster4/1/config/genesis.json
|
||||
sed -i -e 's#'$TM_VALIDATOR1'#'$TM_VALIDATORS'#g' $HOME/.tendermint/cluster4/2/config/genesis.json
|
||||
sed -i -e 's#'$TM_VALIDATOR1'#'$TM_VALIDATORS'#g' $HOME/.tendermint/cluster4/3/config/genesis.json
|
||||
sed -i -e 's#'$TM_VALIDATOR1'#'$TM_VALIDATORS'#g' $HOME/.tendermint/cluster4/4/config/genesis.json
|
||||
|
||||
sed -i -e 's#addr_book_strict = true#addr_book_strict = false#g' $HOME/.tendermint/cluster4/1/config/config.toml
|
||||
sed -i -e 's#addr_book_strict = true#addr_book_strict = false#g' $HOME/.tendermint/cluster4/2/config/config.toml
|
||||
sed -i -e 's#addr_book_strict = true#addr_book_strict = false#g' $HOME/.tendermint/cluster4/3/config/config.toml
|
||||
sed -i -e 's#addr_book_strict = true#addr_book_strict = false#g' $HOME/.tendermint/cluster4/4/config/config.toml
|
5
tools/local-cluster-reset.sh
Executable file
@ -0,0 +1,5 @@
|
||||
CWD=`dirname $0`
|
||||
|
||||
$CWD/local-cluster-delete.sh
|
||||
$CWD/local-cluster-init.sh
|
||||
$CWD/local-cluster-start.sh
|
19
tools/local-cluster-start.sh
Executable file
@ -0,0 +1,19 @@
|
||||
CWD=`dirname $0`
|
||||
|
||||
TM_PERSISTENT_PEERS=\
|
||||
$(tendermint show_node_id --home $HOME/.tendermint/cluster4/1)"@0.0.0.0:46156,"\
|
||||
$(tendermint show_node_id --home $HOME/.tendermint/cluster4/2)"@0.0.0.0:46256,"\
|
||||
$(tendermint show_node_id --home $HOME/.tendermint/cluster4/3)"@0.0.0.0:46356,"\
|
||||
$(tendermint show_node_id --home $HOME/.tendermint/cluster4/4)"@0.0.0.0:46456"
|
||||
|
||||
screen -d -m -S app1 bash -c "cd $CWD/../tmdemoapp; sbt 'run 46158'"
|
||||
screen -d -m -S app2 bash -c "cd $CWD/../tmdemoapp; sbt 'run 46258'"
|
||||
screen -d -m -S app3 bash -c "cd $CWD/../tmdemoapp; sbt 'run 46358'"
|
||||
screen -d -m -S app4 bash -c "cd $CWD/../tmdemoapp; sbt 'run 46458'"
|
||||
|
||||
screen -d -m -S tm1 bash -c 'tendermint node --home=$HOME/.tendermint/cluster4/1 --consensus.create_empty_blocks=false --proxy_app=tcp://127.0.0.1:46158 --rpc.laddr=tcp://0.0.0.0:46157 --p2p.laddr=tcp://0.0.0.0:46156 --p2p.persistent_peers=$TM_PERSISTENT_PEERS'
|
||||
screen -d -m -S tm2 bash -c 'tendermint node --home=$HOME/.tendermint/cluster4/2 --consensus.create_empty_blocks=false --proxy_app=tcp://127.0.0.1:46258 --rpc.laddr=tcp://0.0.0.0:46257 --p2p.laddr=tcp://0.0.0.0:46256 --p2p.persistent_peers=$TM_PERSISTENT_PEERS'
|
||||
screen -d -m -S tm3 bash -c 'tendermint node --home=$HOME/.tendermint/cluster4/3 --consensus.create_empty_blocks=false --proxy_app=tcp://127.0.0.1:46358 --rpc.laddr=tcp://0.0.0.0:46357 --p2p.laddr=tcp://0.0.0.0:46356 --p2p.persistent_peers=$TM_PERSISTENT_PEERS'
|
||||
screen -d -m -S tm4 bash -c 'tendermint node --home=$HOME/.tendermint/cluster4/4 --consensus.create_empty_blocks=false --proxy_app=tcp://127.0.0.1:46458 --rpc.laddr=tcp://0.0.0.0:46457 --p2p.laddr=tcp://0.0.0.0:46456 --p2p.persistent_peers=$TM_PERSISTENT_PEERS'
|
||||
|
||||
screen -d -m -S judge bash -c "python $CWD/../bin/judge.py"
|
11
tools/local-cluster-stop.sh
Executable file
@ -0,0 +1,11 @@
|
||||
screen -S app1 -X stuff $'\003'
|
||||
screen -S app2 -X stuff $'\003'
|
||||
screen -S app3 -X stuff $'\003'
|
||||
screen -S app4 -X stuff $'\003'
|
||||
|
||||
screen -S tm1 -X stuff $'\003'
|
||||
screen -S tm2 -X stuff $'\003'
|
||||
screen -S tm3 -X stuff $'\003'
|
||||
screen -S tm4 -X stuff $'\003'
|
||||
|
||||
screen -S judge -X stuff $'\003'
|