Skip to content

Commit e690cb5

Browse files
loveshashcherbakov
authored andcommitted
generating and verifying state proofs (#364)
* generating and verifying state proofs * verify correct value during proof verification * add proof and verification to state * exploring proof generation for arbitrary roots
1 parent 774b176 commit e690cb5

File tree

4 files changed

+316
-11
lines changed

4 files changed

+316
-11
lines changed

state/pruning_state.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from state.util.fast_rlp import encode_optimized as rlp_encode, \
99
decode_optimized as rlp_decode
1010
from state.util.utils import to_string, isHex
11+
from storage.kv_in_memory import KeyValueStorageInMemory
1112
from storage.kv_store import KeyValueStorage
1213

1314

@@ -86,6 +87,15 @@ def revertToHead(self, headHash=None):
8687
head = BLANK_NODE
8788
self._trie.replace_root_hash(self._trie.root_node, head)
8889

90+
# Proofs are always generated over committed state
91+
def generate_state_proof(self, key: bytes, root=None, serialize=False):
92+
return self._trie.generate_state_proof(key, root, serialize)
93+
94+
@staticmethod
95+
def verify_state_proof(root, key, value, proof_nodes, serialized=False):
96+
return Trie.verify_spv_proof(root, key, rlp_encode([value]),
97+
proof_nodes, serialized)
98+
8999
@property
90100
def as_dict(self):
91101
d = self._trie.to_dict()
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import pytest
2+
3+
from state.pruning_state import PruningState
4+
from state.state import State
5+
from storage.kv_in_memory import KeyValueStorageInMemory
6+
from storage.kv_store_leveldb import KeyValueStorageLeveldb
7+
8+
9+
@pytest.yield_fixture(params=['memory', 'leveldb'])
10+
def state(request, tmpdir_factory) -> State:
11+
if request.param == 'memory':
12+
db = KeyValueStorageInMemory()
13+
if request.param == 'leveldb':
14+
db = KeyValueStorageLeveldb(tmpdir_factory.mktemp('').strpath,
15+
'some_db')
16+
state = PruningState(db)
17+
yield state
18+
state.close()
19+
20+
21+
def test_state_proof_and_verification(state):
22+
state.set(b'k1', b'v1')
23+
state.set(b'k2', b'v2')
24+
state.set(b'k3', b'v3')
25+
state.set(b'k4', b'v4')
26+
27+
p1 = state.generate_state_proof(b'k1')
28+
p2 = state.generate_state_proof(b'k2')
29+
p3 = state.generate_state_proof(b'k3')
30+
p4 = state.generate_state_proof(b'k4')
31+
32+
# Verify correct proofs and values
33+
assert PruningState.verify_state_proof(state.headHash, b'k1', b'v1', p1)
34+
assert PruningState.verify_state_proof(state.headHash, b'k2', b'v2', p2)
35+
assert PruningState.verify_state_proof(state.headHash, b'k3', b'v3', p3)
36+
assert PruningState.verify_state_proof(state.headHash, b'k4', b'v4', p4)
37+
38+
# Incorrect proof
39+
assert PruningState.verify_state_proof(state.headHash, b'k3', b'v3', p4)
40+
41+
# Correct proof but incorrect value
42+
assert not PruningState.verify_state_proof(state.headHash, b'k2', b'v1', p2)
43+
assert not PruningState.verify_state_proof(state.headHash, b'k4', b'v2', p4)
44+
45+
46+
def test_state_proof_and_verification_serialized(state):
47+
data = {k.encode(): v.encode() for k, v in
48+
[('k1', 'v1'), ('k2', 'v2'), ('k35', 'v55'), ('k70', 'v99')]}
49+
50+
for k, v in data.items():
51+
state.set(k, v)
52+
53+
proofs = {k: state.generate_state_proof(k, serialize=True) for k in data}
54+
55+
for k, v in data.items():
56+
assert PruningState.verify_state_proof(state.headHash, k, v,
57+
proofs[k], serialized=True)

state/test/trie/test_proof.py

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
from copy import deepcopy
2+
from random import random, randint, choice
3+
4+
import pytest
5+
6+
from plenum.common.util import randomString
7+
from state.db.persistent_db import PersistentDB
8+
from state.trie.pruning_trie import Trie, rlp_encode
9+
from storage.kv_in_memory import KeyValueStorageInMemory
10+
11+
12+
def gen_test_data(num_keys, max_key_size=64, max_val_size=256):
13+
return {randomString(max_key_size).encode():
14+
rlp_encode([randomString(max_val_size)]) for i in range(num_keys)}
15+
16+
17+
def test_verify_proof():
18+
node_trie = Trie(PersistentDB(KeyValueStorageInMemory()))
19+
client_trie = Trie(PersistentDB(KeyValueStorageInMemory()))
20+
21+
node_trie.update('k1'.encode(), rlp_encode(['v1']))
22+
node_trie.update('k2'.encode(), rlp_encode(['v2']))
23+
24+
root_hash_0 = node_trie.root_hash
25+
p0 = node_trie.produce_spv_proof('k2'.encode())
26+
p0.append(deepcopy(node_trie.root_node))
27+
p00 = deepcopy(p0)
28+
assert client_trie.verify_spv_proof(root_hash_0, 'k2'.encode(),
29+
rlp_encode(['v2']), p0)
30+
assert p00 == p0
31+
32+
node_trie.update('k3'.encode(), rlp_encode(['v3']))
33+
node_trie.update('k4'.encode(), rlp_encode(['v4']))
34+
node_trie.update('x1'.encode(), rlp_encode(['y1']))
35+
node_trie.update('x2'.encode(), rlp_encode(['y2']))
36+
root_hash_1 = node_trie.root_hash
37+
38+
# Generate 1 proof and then verify that proof
39+
p1 = node_trie.produce_spv_proof('k1'.encode())
40+
p1.append(node_trie.root_node)
41+
assert client_trie.verify_spv_proof(root_hash_1, 'k1'.encode(),
42+
rlp_encode(['v1']), p1)
43+
44+
p2 = node_trie.produce_spv_proof('x2'.encode())
45+
p2.append(node_trie.root_node)
46+
assert client_trie.verify_spv_proof(root_hash_1, 'x2'.encode(),
47+
rlp_encode(['y2']), p2)
48+
49+
# Generate more than 1 proof and then verify all proofs
50+
51+
p3 = node_trie.produce_spv_proof('k3'.encode())
52+
p3.append(node_trie.root_node)
53+
54+
p4 = node_trie.produce_spv_proof('x1'.encode())
55+
p4.append(node_trie.root_node)
56+
57+
assert client_trie.verify_spv_proof(root_hash_1, 'k3'.encode(),
58+
rlp_encode(['v3']), p3)
59+
assert client_trie.verify_spv_proof(root_hash_1, 'x1'.encode(),
60+
rlp_encode(['y1']), p4)
61+
62+
# Proof is correct but value is different
63+
assert not client_trie.verify_spv_proof(root_hash_1, 'x1'.encode(),
64+
rlp_encode(['y99']), p4)
65+
66+
# Verify same proof again
67+
assert client_trie.verify_spv_proof(root_hash_1, 'k3'.encode(),
68+
rlp_encode(['v3']), p3)
69+
70+
assert p00 == p0
71+
assert client_trie.verify_spv_proof(root_hash_0, 'k2'.encode(),
72+
rlp_encode(['v2']), p0)
73+
74+
75+
def test_verify_proof_generated_using_helper():
76+
node_trie = Trie(PersistentDB(KeyValueStorageInMemory()))
77+
client_trie = Trie(PersistentDB(KeyValueStorageInMemory()))
78+
79+
node_trie.update('k1'.encode(), rlp_encode(['v1']))
80+
node_trie.update('k2'.encode(), rlp_encode(['v2']))
81+
82+
root_hash_0 = node_trie.root_hash
83+
p0 = node_trie.generate_state_proof('k2'.encode())
84+
assert client_trie.verify_spv_proof(root_hash_0, 'k2'.encode(),
85+
rlp_encode(['v2']), p0)
86+
87+
node_trie.update('k3'.encode(), rlp_encode(['v3']))
88+
node_trie.update('k4'.encode(), rlp_encode(['v4']))
89+
node_trie.update('x1'.encode(), rlp_encode(['y1']))
90+
node_trie.update('x2'.encode(), rlp_encode(['y2']))
91+
root_hash_1 = node_trie.root_hash
92+
93+
# Generate 1 proof and then verify that proof
94+
p1 = node_trie.generate_state_proof('k1'.encode())
95+
assert client_trie.verify_spv_proof(root_hash_1, 'k1'.encode(),
96+
rlp_encode(['v1']), p1)
97+
98+
p2 = node_trie.generate_state_proof('x2'.encode())
99+
assert client_trie.verify_spv_proof(root_hash_1, 'x2'.encode(),
100+
rlp_encode(['y2']), p2)
101+
102+
# Generate more than 1 proof and then verify all proofs
103+
104+
p3 = node_trie.generate_state_proof('k3'.encode())
105+
106+
p4 = node_trie.generate_state_proof('x1'.encode())
107+
108+
assert client_trie.verify_spv_proof(root_hash_1, 'k3'.encode(),
109+
rlp_encode(['v3']), p3)
110+
assert client_trie.verify_spv_proof(root_hash_1, 'x1'.encode(),
111+
rlp_encode(['y1']), p4)
112+
113+
# Proof is correct but value is different
114+
assert not client_trie.verify_spv_proof(root_hash_1, 'x1'.encode(),
115+
rlp_encode(['y99']), p4)
116+
117+
# Verify same proof again
118+
assert client_trie.verify_spv_proof(root_hash_1, 'k3'.encode(),
119+
rlp_encode(['v3']), p3)
120+
121+
assert client_trie.verify_spv_proof(root_hash_0, 'k2'.encode(),
122+
rlp_encode(['v2']), p0)
123+
124+
# Proof generated using non-existent key fails verification
125+
p5 = node_trie.generate_state_proof('x909'.encode())
126+
assert not client_trie.verify_spv_proof(root_hash_1, 'x909'.encode(),
127+
rlp_encode(['y909']), p5)
128+
129+
130+
def test_verify_proof_random_data():
131+
"""
132+
Add some key value pairs in trie. Generate and verify proof for them.
133+
:return:
134+
"""
135+
num_keys = 100
136+
test_data = gen_test_data(num_keys)
137+
partitions = 4
138+
partition_size = num_keys // partitions
139+
keys = [list(list(test_data.keys())[i:i + partition_size])
140+
for i in range(0, len(test_data), partition_size)]
141+
142+
node_trie = Trie(PersistentDB(KeyValueStorageInMemory()))
143+
client_trie = Trie(PersistentDB(KeyValueStorageInMemory()))
144+
root_hashes = []
145+
proofs = []
146+
for i in range(0, partitions):
147+
for k in keys[i]:
148+
node_trie.update(k, test_data[k])
149+
150+
root_hashes.append(node_trie.root_hash)
151+
proofs.append({k: node_trie.generate_state_proof(k) for k in keys[i]})
152+
assert all([client_trie.verify_spv_proof(root_hashes[i],
153+
k, test_data[k],
154+
proofs[i][k]) for k in keys[i]])
155+
156+
# Pick any keys from any partition and verify the already generated proof
157+
for _ in range(400):
158+
p = randint(0, partitions - 1)
159+
key = choice(keys[p])
160+
assert client_trie.verify_spv_proof(root_hashes[p], key,
161+
test_data[key], proofs[p][key])
162+
163+
# Pick any key randomly, generate new proof corresponding to current root
164+
# and verify proof
165+
all_keys = [k for i in keys for k in i]
166+
root_hash = node_trie.root_hash
167+
for _ in range(400):
168+
key = choice(all_keys)
169+
proof = node_trie.generate_state_proof(key)
170+
assert client_trie.verify_spv_proof(root_hash, key,
171+
test_data[key], proof)
172+
173+
174+
def test_proof_serialize_deserialize():
175+
node_trie = Trie(PersistentDB(KeyValueStorageInMemory()))
176+
client_trie = Trie(PersistentDB(KeyValueStorageInMemory()))
177+
keys = {k.encode(): [rlp_encode([v]), ] for k, v in
178+
[('k1', 'v1'), ('k2', 'v2'), ('k35', 'v55'), ('k70', 'v99')]}
179+
180+
for k, v in keys.items():
181+
node_trie.update(k, v[0])
182+
183+
for k in keys:
184+
keys[k].append(node_trie.generate_state_proof(k, serialize=True))
185+
186+
for k in keys:
187+
prf = keys[k][1]
188+
assert isinstance(prf, bytes)
189+
assert client_trie.verify_spv_proof(node_trie.root_hash, k, keys[k][0],
190+
prf, serialized=True)
191+
192+
193+
@pytest.mark.skip(reason='Need to check if need/possible to build proof for an '
194+
'arbitrary old root')
195+
def test_proof_specific_root():
196+
node_trie = Trie(PersistentDB(KeyValueStorageInMemory()))
197+
client_trie = Trie(PersistentDB(KeyValueStorageInMemory()))
198+
199+
node_trie.update('k1'.encode(), rlp_encode(['v1']))
200+
node_trie.update('k2'.encode(), rlp_encode(['v2']))
201+
node_trie.update('x3'.encode(), rlp_encode(['v3']))
202+
203+
root_hash_0 = node_trie.root_hash
204+
root_node_0 = node_trie.root_node
205+
206+
node_trie.update('x4'.encode(), rlp_encode(['v5']))
207+
node_trie.update('y99'.encode(), rlp_encode(['v6']))
208+
node_trie.update('x5'.encode(), rlp_encode(['v7']))
209+
210+
# root_hash_1 = node_trie.root_hash
211+
# root_node_1 = node_trie.root_node
212+
213+
k, v = 'k1'.encode(), rlp_encode(['v1'])
214+
old_root_proof = node_trie.generate_state_proof(k, root=root_node_0)
215+
assert client_trie.verify_spv_proof(root_hash_0, k, v, old_root_proof)

state/trie/pruning_trie.py

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
import rlp
77
from rlp.utils import decode_hex, encode_hex, ascii_chr, str_to_bytes
88
from state.db.db import BaseDB
9-
from state.util.fast_rlp import encode_optimized
9+
from state.util.fast_rlp import encode_optimized, decode_optimized
1010
from state.util.utils import is_string, to_string, sha3, sha3rlp, encode_int
1111
from storage.kv_in_memory import KeyValueStorageInMemory
1212

1313
rlp_encode = encode_optimized
14+
rlp_decode = decode_optimized
1415

1516
bin_to_nibbles_cache = {}
1617

@@ -63,12 +64,13 @@ def __init__(self):
6364
self.nodes = []
6465
self.exempt = []
6566

66-
def push(self, mode, nodes=[]):
67+
def push(self, mode, nodes=None):
6768
global proving
6869
proving = True
6970
self.mode.append(mode)
7071
self.exempt.append(set())
7172
if mode == VERIFYING:
73+
nodes = nodes or []
7274
self.nodes.append(set([rlp_encode(x) for x in nodes]))
7375
else:
7476
self.nodes.append(set())
@@ -968,9 +970,10 @@ def root_hash_valid(self):
968970
return True
969971
return self.root_hash in self._db
970972

971-
def produce_spv_proof(self, key):
973+
def produce_spv_proof(self, key, root=None):
974+
root = root or self.root_node
972975
proof.push(RECORDING)
973-
self.get(key)
976+
self.get_at(root, key)
974977
o = proof.get_nodelist()
975978
proof.pop()
976979
return o
@@ -984,25 +987,45 @@ def get_at(self, root_node, key):
984987
"""
985988
return self._get(root_node, bin_to_nibbles(to_string(key)))
986989

990+
def generate_state_proof(self, key, root=None, serialize=False):
991+
# NOTE: The method `produce_spv_proof` is not deliberately modified
992+
root = root or self.root_node
993+
pf = self.produce_spv_proof(key, root)
994+
pf.append(copy.deepcopy(root))
995+
return pf if not serialize else self.serialize_proof(pf)
996+
987997
@staticmethod
988-
def verify_spv_proof(root, key, proof_nodes):
998+
def verify_spv_proof(root, key, value, proof_nodes, serialized=False):
999+
# NOTE: `root` is a derivative of the last element of `proof_nodes`
1000+
# but it's important to keep `root` as a separate as signed root
1001+
# hashes will be published.
1002+
if serialized:
1003+
proof_nodes = Trie.deserialize_proof(proof_nodes)
9891004
proof.push(VERIFYING, proof_nodes)
990-
t = Trie(KeyValueStorageInMemory())
1005+
new_trie = Trie(KeyValueStorageInMemory())
9911006

992-
for i, node in enumerate(proof_nodes):
1007+
for node in proof_nodes:
9931008
R = rlp_encode(node)
9941009
H = sha3(R)
995-
t._db.put(H, R)
1010+
new_trie._db.put(H, R)
9961011
try:
997-
t.root_hash = root
998-
t.get(key)
1012+
new_trie.root_hash = root
1013+
v = new_trie.get(key)
9991014
proof.pop()
1000-
return True
1015+
return v == value
10011016
except Exception as e:
10021017
print(e)
10031018
proof.pop()
10041019
return False
10051020

1021+
@staticmethod
1022+
def serialize_proof(proof_nodes):
1023+
return rlp_encode(proof_nodes)
1024+
1025+
@staticmethod
1026+
def deserialize_proof(ser_proof):
1027+
return rlp_decode(ser_proof)
1028+
10061029

10071030
if __name__ == "__main__":
10081031

0 commit comments

Comments
 (0)