client-side proof checking

This commit is contained in:
Dmitry Sergeev 2018-06-18 10:31:27 +05:00
parent 3da08e1e5d
commit 7171c71811
4 changed files with 104 additions and 68 deletions

5
.gitignore vendored
View File

@ -101,4 +101,7 @@ ENV/
.mypy_cache/
# IntelliJ Idea
.idea
.idea
#macOS folder metadata
.DS_Store

View File

@ -1,18 +1,64 @@
import sys, urllib, json, datetime, time
from parse_common import readjson, getsyncinfo, getmaxheight
import sys, urllib, json, datetime, time, hashlib, sha3
from common_parse_utils import readjson, getsyncinfo, getmaxheight
CMD_TX = "tx"
CMD_TX_VERIFY = "txverify"
CMD_OP = "op"
CMD_PUT = "put"
CMD_CHECKED_PUT = "chput"
CMD_RUN = "run"
CMD_GET_QUERY = "get"
CMD_LS_QUERY = "ls"
def abci_query(tmaddress, height, query):
response = readjson(tmaddress + '/abci_query?height=' + str(height) + '&data="' + query + '"')["result"]["response"]
return (
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 getmaxheight(tmaddress) < height + 1:
return (height, None, None, None, False, "Cannot verify tentative '%s'! Height is not verifiable" % (info or ""))
apphash = readjson('%s/block?height=%d' % (tmaddress, height + 1))["result"]["block"]["header"]["app_hash"]
response = readjson('%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, apphash, False, "Result is empty")
elif tentative_info is not None and result != tentative_info:
return (height, result, proof, apphash, False, "Verified result '%s' doesn't match tentative '%s'!" % (result, info))
elif proof is None:
return (height, result, proof, apphash, False, "No proof")
elif not verify_merkle_proof(result, proof, apphash) :
return (height, result, proof, apphash, False, "Proof is invalid")
else:
return (height, result, proof, apphash, True, "")
def print_checked_abci_query(tmaddress, height, command, query, tentative_info):
(height, result, proof, apphash, success, message) = checked_abci_query(tmaddress, height, command, query, tentative_info)
print "HEIGHT:", height
print "HASH :", apphash or "NOT_READY"
print "PROOF :", (proof or "NO_PROOF").upper()
print "RESULT:", result or "EMPTY"
if success:
print "OK"
else:
print "BAD :", message
def latest_provable_height(tmaddress):
return getsyncinfo(tmaddress)["latest_block_height"] - 1
def wait_for_height(tmaddress, height):
for w in range(0, 5):
if getmaxheight(tmaddress) >= height:
break
time.sleep(1)
if len(sys.argv) < 3:
print "usage: python query.py host:port command arg"
@ -21,8 +67,8 @@ if len(sys.argv) < 3:
tmaddress = sys.argv[1]
command = sys.argv[2]
arg = sys.argv[3]
if command in {CMD_TX, CMD_TX_VERIFY, CMD_OP}:
if command == CMD_OP:
if command in {CMD_PUT, CMD_CHECKED_PUT, CMD_RUN}:
if command == CMD_RUN:
query_key = "optarg"
tx = query_key + "=" + arg
else:
@ -34,38 +80,17 @@ if command in {CMD_TX, CMD_TX_VERIFY, CMD_OP}:
else:
height = response["result"]["height"]
if response["result"].get("deliver_tx", {}).get("code", "0") != "0":
log = response["result"].get("deliver_tx", {}).get("log")
print "BAD"
print "HEIGHT:", height
print "LOG: ", log or "EMPTY"
print "BAD :", log or "NO_LOG"
else:
info = response["result"].get("deliver_tx", {}).get("info")
print "HEIGHT:", height
if command in {CMD_TX_VERIFY, CMD_OP} and info is not None:
for w in range(0, 5):
if getmaxheight(tmaddress) >= height + 1:
break
time.sleep(1)
if getmaxheight(tmaddress) < height + 1:
print "BAD :", "Cannot verify tentative result '%s'!" % (info)
else:
(result, proof) = abci_query(tmaddress, height, "get:" + query_key)
if result == info:
print "OK"
else:
print "BAD :", "Verified result '%s' doesn't match tentative '%s'!" % (result, info)
print "RESULT:", result or "EMPTY"
print "PROOF :", proof or "NO_PROOF"
if command in {CMD_CHECKED_PUT, CMD_RUN} and info is not None:
wait_for_height(tmaddress, height + 1)
print_checked_abci_query(tmaddress, height, "get", query_key, info)
else:
print "HEIGHT:", height
print "INFO: ", info or "EMPTY"
print "OK"
elif command in {CMD_GET_QUERY, CMD_LS_QUERY}:
syncinfo = getsyncinfo(tmaddress)
height = syncinfo["latest_block_height"]
apphash = syncinfo["latest_app_hash"]
print "HEIGHT:", height
print "HASH :", apphash
query_response = abci_query(tmaddress, height, command + ":" + arg)
print "RESULT:", query_response[0] or "EMPTY"
print "PROOF :", query_response[1] or "NO_PROOF"
height = latest_provable_height(tmaddress)
print_checked_abci_query(tmaddress, height, command, arg, None)

View File

@ -50,19 +50,27 @@ The **State machine** maintains its state using in-memory key-value string stora
### Operations
Tendermint architecture suppose that the client typically interacts with the Application via the local **Proxy**. This application uses Python `query.py` script as client-side Proxy to request arbitrary operations to the cluster, including:
* Simple `put` requests which specify a target key and a constant as its new value: `put a/b=10`.
* Computational `put` requests which specify that a target key should be assigned to the result of some operation (with arguments) invocation: `put a/c=factorial:a/b`.
* Requests to obtain a result of running an arbitrary operation: `run factorial:a/b`.
* Computational `put` requests which specify that a target key should be assigned to the result of some operation (with arguments) invocation: `put a/c=factorial(a/b)`.
* Requests to obtain a result of running an arbitrary operation: `run factorial(a/b)`, `run sum(a/b,a/c)`.
* Requests to read the value of specified key: `get a/b`.
`get` operations do not change the state of the application. They are implemented via Tendermint ABCI queries. As the result of a such query the **State machine** returns the value of the requested key together with the Merkle proof.
`put` operations are *effectful* and change the application state explicitly. They are implemented via Tendermint **transactions** that combined into **blocks**. **TM Core** sends a transaction to the **State machine** and **State machine** applies this transaction to its state, typically changing the target key.
`get` and `put` operations use different techniques to prove to the client that the operation is actually invoked and its result is correct. `get`s take advantage of Merkelized structure of the application state and provide Merkle proof of the result correctness. Any `put` invocation leads to adding the corresponding transaction to the blockchain, so observing this transaction in a correctly signed block means that there is a quorum in the cluster regarding this transaction's invocation.
`get` and `put` operations use different techniques to prove to the client that the operation is actually invoked and its result is correct. `get`-s take advantage of Merkelized structure of the application state and provide Merkle proof of the result correctness. Any `put` invocation leads to adding the corresponding transaction to the blockchain, so observing this transaction in a correctly signed block means that there is a quorum in the cluster regarding this transaction's invocation.
`run` operations are also *effectul*. They are implemented as combinations of `put`s and `get`s: to perform operation trustfully, **Proxy** first requests `put`-ting the result of operation to some key and then queries its value. Thus the correctness is ensured by both the consensus and Merkle proof.
`run` operations are also *effectul*. They are implemented as combinations of `put` and `get` requests: to perform operation trustfully, **Proxy** first requests `put`-ting the result of operation to some key and then queries its value. Thus the correctness is ensured by both the consensus and Merkle proof.
## Installation and run
To run the App, a **Node** machine needs:
* Scala 2.12 with `sbt`
* [Tendermint](http://tendermint.readthedocs.io/en/master/install.html)
* GNU `screen` (to run single-machine cluster)
To query Nodes from client-side proxy, a client machine needs:
* Python 2.7 with `sha3` package installed
For single-node run just launch the application:
```bash
sbt run
@ -100,12 +108,12 @@ Other scripts allow to temporarily stop (`node4-stop.sh`), delete (`node4-delete
## Sending queries
### Transactions
### Writing operations
Examples below use `localhost:46157` to query TM Core, for 4-node single-machine cluster requests to other endpoints (`46257`, `46357`, `46457`) behave the same way. For single-node launch (just one TM and one App) the default port is `46657`.
To set a new key-value mapping use:
```bash
python query.py localhost:46157 tx a/b=10
python query.py localhost:46157 put a/b=10
...
OK
HEIGHT: 2
@ -122,48 +130,48 @@ This command outputs last 50 non-empty blocks in the blockchain with a short sum
curl -s 'localhost:46157/block?height=_' # replace _ with actual height number
```
`get` transaction allows to copy a value from one key to another:
`get` operation allows to copy a value from one key to another:
```bash
python query.py localhost:46157 tx a/c=get:a/b
python query.py localhost:46157 put "a/c=get(a/b)"
...
INFO: 10
```
Submitting an `increment` transaction increments the referenced key value and copies the old referenced key value to target key:
Submitting an `increment` operation increments the referenced key value and copies the old referenced key value to target key:
```bash
python query.py localhost:46157 tx a/d=increment:a/c
python query.py localhost:46157 put "a/d=increment(a/c)"
...
INFO: 10
```
To prevent Tendermint from declining transaction that repeats one of the previously applied transactions, it's possible to put any characters after `###` at the end of transaction string, this part of string ignored:
```bash
python query.py localhost:46157 tx a/d=increment:a/c###again
python query.py localhost:46157 put "a/d=increment(a/c)###again"
...
INFO: 11
```
`sum` transaction sums the values of references keys and assigns the result to the target key:
`sum` operation sums the values of references keys and assigns the result to the target key:
```bash
python query.py localhost:46157 tx a/e=sum:a/c,a/d
python query.py localhost:46157 put "a/e=sum(a/c,a/d)"
...
INFO: 23
```
`factorial` transaction calculates the factorial of the referenced key value:
`factorial` operation calculates the factorial of the referenced key value:
```bash
python query.py localhost:46157 tx a/f=factorial:a/b
python query.py localhost:46157 put "a/f=factorial(a/b)"
...
INFO: 3628800
```
`hiersum` transaction calculates the sum of non-empty values for the referenced key and its descendants by hierarchy (all non-empty values should be integer):
`hiersum` operation calculates the sum of non-empty values for the referenced key and its descendants by hierarchy (all non-empty values should be integer):
```bash
python query.py localhost:46157 tx c/asum=hiersum:a
python query.py localhost:46157 put "c/asum=hiersum(a)"
...
INFO: 3628856
```
Transactions are not applied in case of wrong arguments (non-integer values to `increment`, `sum`, `factorial` or wrong number of arguments). Transactions with a target key like `get`, `increment`, `sum`, `factorial` return the new value of the target key as `INFO`, but this value is *tentative* cannot be trusted if the serving node is not reliable. To verify the returned `INFO` one needs to `query` the target key explicitly.
Operations are not applied in case of wrong arguments (non-integer values to `increment`, `sum`, `factorial` or wrong number of arguments). Operations with a target key like `get`, `increment`, `sum`, `factorial` return the new value of the target key as `INFO`, but this value is *tentative* and cannot be trusted if the serving node is not reliable. To verify the returned `INFO` one needs to `query` the target key explicitly.
### Simple queries
`get` reads values from KVStore:
@ -179,23 +187,23 @@ python query.py localhost:46657 ls a
RESULT: e f b c d
```
### Operations
As mentioned above operation query is a combination of subsequent:
* operation processing,
* writing its result to a special key
* and Merkelized read of this key.
### Computations without target key
As mentioned above, `run` query is a combination of subsequent:
* operation processing
* `put`-ting its result to a special key
* Merkelized `get` of this key
Below is the example (note that no target key specified here):
```bash
python query.py localhost:46157 op factorial:a/b
python query.py localhost:46157 run "factorial(a/b)"
...
RESULT: 3628800
```
## Implementation details
### A. How Proxy sees operation processing
Let's observe how operation processing looks like.
1. Proxy gets `op` from the client.
Let's observe how `run` request processing looks like.
1. Proxy gets `run` from the client.
2. Proxy decomposes operation into 2 interactions with cluster: transaction submit and response query.
3. It takes some state key `opTarget` (it might use different such keys and choose one of them somehow).
4. For transaction submit Proxy:

View File

@ -46,8 +46,8 @@ class ABCIHandler(val serverIndex: Int) extends IDeliverTx with ICheckTx with IC
private var consensusRoot: Node = Node.emptyNode
private var currentBlockHasTransactions: Boolean = false
private val binaryOpPattern: Regex = "(.+)=(.+):(.*),(.*)".r
private val unaryOpPattern: Regex = "(.+)=(.+):(.*)".r
private val binaryOpPattern: Regex = "(.+)=(.+)\\((.*),(.*)\\)".r
private val unaryOpPattern: Regex = "(.+)=(.+)\\((.*)\\)".r
private val plainValuePattern: Regex = "(.+)=(.*)".r
override def receivedDeliverTx(req: RequestDeliverTx): ResponseDeliverTx = {
@ -101,7 +101,7 @@ class ABCIHandler(val serverIndex: Int) extends IDeliverTx with ICheckTx with IC
private def appHash: Option[MerkleHash] = possiblyWrongAppHash
private def possiblyWrongAppHash: Option[MerkleHash] = correctAppHash.map(_ ++ Array(if (isByzantine) 1.toByte else 0.toByte))
private def possiblyWrongAppHash: Option[MerkleHash] = correctAppHash.map(x => if (isByzantine) { val y = x.clone(); y(0) = (0xFF ^ y(0)).toByte; y } else x)
private def correctAppHash: Option[MerkleHash] = consensusRoot.merkleHash