mirror of
https://github.com/fluencelabs/tendermint-demo
synced 2025-03-14 15:20:50 +00:00
client-side proof checking
This commit is contained in:
parent
3da08e1e5d
commit
7171c71811
5
.gitignore
vendored
5
.gitignore
vendored
@ -101,4 +101,7 @@ ENV/
|
||||
.mypy_cache/
|
||||
|
||||
# IntelliJ Idea
|
||||
.idea
|
||||
.idea
|
||||
|
||||
#macOS folder metadata
|
||||
.DS_Store
|
101
parse/query.py
101
parse/query.py
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user