Skip to content

Commit e64c544

Browse files
author
Ilya Bogdanov
authored
Merge pull request #1081 from exonum/ECR-3413
Add proofs of absence for ListProof [ECR-3413]
2 parents c7b12af + d6bf195 commit e64c544

File tree

15 files changed

+313
-46
lines changed

15 files changed

+313
-46
lines changed

exonum-java-binding/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
2626
`CheckedProof#getIndexHash`, `ProofListIndexProxy#getIndexHash` and
2727
`ProofMapIndexProxy#getIndexHash` accordingly.
2828

29+
### Added
30+
- Proofs of absence of an element with the specified index for `ProofListIndexProxy`. (#1081)
31+
2932
## [0.7.0] - 2019-07-17
3033

3134
### Overview

exonum-java-binding/common/src/main/java/com/exonum/binding/common/proofs/list/CheckedListProof.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@
3636
*/
3737
public interface CheckedListProof<E> extends CheckedProof {
3838
/**
39-
* Get all list elements. There might be several consecutive ranges.
39+
* Get all list proof elements. There might be several consecutive ranges.
40+
*
41+
* @return list proof elements. Empty if the proof is a proof of absence
4042
* @throws IllegalStateException if the proof is not valid
4143
*/
4244
NavigableMap<Long, E> getElements();

exonum-java-binding/common/src/main/java/com/exonum/binding/common/proofs/list/CheckedListProofImpl.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@
3030
* <li>a calculated index hash of corresponding collection
3131
* <li>proof elements
3232
* </ul>
33-
* If the proof is not valid, you may get the verification status using
33+
*
34+
* <p>If the proof is the proof of absence, then empty collection of elements is returned.
35+
*
36+
* <p>If the proof is not valid, you may get the verification status using
3437
* {@link #getProofStatus()} with description of why the proof is not
3538
* valid.
3639
*/
@@ -45,7 +48,7 @@ public class CheckedListProofImpl<E> implements CheckedListProof {
4548
/**
4649
* Creates checked list proof.
4750
* @param calculatedIndexHash calculated index hash of the proof
48-
* @param elements proof elements collection
51+
* @param elements proof elements collection (empty in case of a proof of absence)
4952
* @param proofStatus a status of proof verification
5053
*/
5154
public CheckedListProofImpl(HashCode calculatedIndexHash,

exonum-java-binding/common/src/main/java/com/exonum/binding/common/proofs/list/ListProofHashCalculator.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@ public void visit(ListProofElement value) {
115115
.hash();
116116
}
117117

118+
@Override
119+
public void visit(ListProofOfAbsence listProofOfAbsence) {
120+
hash = listProofOfAbsence.getMerkleRoot();
121+
}
122+
118123
private HashCode visitLeft(ListProofBranch branch, long parentIndex) {
119124
index = 2 * parentIndex;
120125
branch.getLeft().accept(this);
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2019 The Exonum Team
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.exonum.binding.common.proofs.list;
18+
19+
import com.exonum.binding.common.hash.HashCode;
20+
21+
/**
22+
* Represents the proof of absence of a requested element or a range of elements
23+
* by providing Merkle root hash of a corresponding proof list.
24+
*/
25+
public final class ListProofOfAbsence implements ListProofNode {
26+
27+
private final HashCode merkleRoot;
28+
29+
@SuppressWarnings("unused") // Native API
30+
ListProofOfAbsence(byte[] merkleRoot) {
31+
this(HashCode.fromBytes(merkleRoot));
32+
}
33+
34+
/**
35+
* Creates ListProof absence node with given Merkle root.
36+
*/
37+
public ListProofOfAbsence(HashCode merkleRoot) {
38+
this.merkleRoot = merkleRoot;
39+
}
40+
41+
@Override
42+
public void accept(ListProofVisitor visitor) {
43+
visitor.visit(this);
44+
}
45+
46+
/**
47+
* Returns the Merkle root of a corresponding proof list.
48+
*/
49+
public HashCode getMerkleRoot() {
50+
return merkleRoot;
51+
}
52+
}

exonum-java-binding/common/src/main/java/com/exonum/binding/common/proofs/list/ListProofStatus.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ public enum ListProofStatus implements ProofStatus {
2727
INVALID_HASH_NODE_DEPTH("Hash node appears below the maximum allowed depth"),
2828
INVALID_TREE_NO_ELEMENTS("Tree does not contain any element nodes"),
2929
INVALID_NODE_DEPTH("Value node appears at the wrong level"),
30-
INVALID_HASH_NODES_COUNT("A branch node in a proof tree has only hash nodes as children");
30+
INVALID_HASH_NODES_COUNT("A branch node in a proof tree has only hash nodes as children"),
31+
INVALID_PROOF_OF_ABSENCE("Proof of absence is not a root node of a proof tree");
3132

3233
private final String description;
3334

exonum-java-binding/common/src/main/java/com/exonum/binding/common/proofs/list/ListProofStructureValidator.java

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@
1616

1717
package com.exonum.binding.common.proofs.list;
1818

19+
import com.google.common.collect.Multimap;
20+
import com.google.common.collect.Multimaps;
1921
import java.util.ArrayList;
2022
import java.util.Arrays;
23+
import java.util.Collection;
2124
import java.util.Collections;
25+
import java.util.EnumMap;
2226
import java.util.List;
2327

2428
/**
@@ -29,11 +33,7 @@ final class ListProofStructureValidator implements ListProofVisitor {
2933
static final int MAX_NODE_DEPTH = 63;
3034

3135
// TODO: optimize data storage and algorithms of validity checks, https://jira.bf.local/browse/ECR-2443
32-
private final List<NodeInfo> listProofBranchesInfo;
33-
34-
private final List<NodeInfo> listProofElementsInfo;
35-
36-
private final List<NodeInfo> listProofHashNodesInfo;
36+
private final Multimap<NodeType, NodeInfo> listProofInfo;
3737

3838
private int depth;
3939

@@ -45,9 +45,7 @@ final class ListProofStructureValidator implements ListProofVisitor {
4545
ListProofStructureValidator(ListProofNode listProof) {
4646
depth = 0;
4747
proofStatus = ListProofStatus.VALID;
48-
listProofBranchesInfo = new ArrayList<>();
49-
listProofElementsInfo = new ArrayList<>();
50-
listProofHashNodesInfo = new ArrayList<>();
48+
listProofInfo = Multimaps.newListMultimap(new EnumMap<>(NodeType.class), ArrayList::new);
5149

5250
listProof.accept(this);
5351
this.check();
@@ -59,7 +57,7 @@ public void visit(ListProofBranch branch) {
5957

6058
NodeType leftElementType = getNodeType(branch.getLeft());
6159
NodeType rightElementType = branch.getRight().map(this::getNodeType).orElse(NodeType.NONE);
62-
listProofBranchesInfo.add(
60+
listProofInfo.get(NodeType.BRANCH).add(
6361
new NodeInfo(branch, depth, Arrays.asList(leftElementType, rightElementType))
6462
);
6563

@@ -69,12 +67,17 @@ public void visit(ListProofBranch branch) {
6967

7068
@Override
7169
public void visit(ListProofHashNode hashNode) {
72-
listProofHashNodesInfo.add(new NodeInfo(hashNode, depth));
70+
listProofInfo.get(NodeType.HASHNODE).add(new NodeInfo(hashNode, depth));
7371
}
7472

7573
@Override
7674
public void visit(ListProofElement element) {
77-
listProofElementsInfo.add(new NodeInfo(element, depth));
75+
listProofInfo.get(NodeType.ELEMENT).add(new NodeInfo(element, depth));
76+
}
77+
78+
@Override
79+
public void visit(ListProofOfAbsence listProofOfAbsence) {
80+
listProofInfo.get(NodeType.ABSENCE).add(new NodeInfo(listProofOfAbsence, depth));
7881
}
7982

8083
private void visitLeft(ListProofBranch branch, int branchDepth) {
@@ -95,16 +98,19 @@ private int getChildDepth(int branchDepth) {
9598
* Performs list proof structure checks and assigns proofStatus based on results of these checks.
9699
*/
97100
private void check() {
98-
if (exceedsMaxDepth(listProofElementsInfo)) {
101+
if (exceedsMaxDepth(listProofInfo.get(NodeType.ELEMENT))) {
99102
proofStatus = ListProofStatus.INVALID_ELEMENT_NODE_DEPTH;
100-
} else if (exceedsMaxDepth(listProofHashNodesInfo)) {
103+
} else if (exceedsMaxDepth(listProofInfo.get(NodeType.HASHNODE))) {
101104
proofStatus = ListProofStatus.INVALID_HASH_NODE_DEPTH;
102-
} else if (hasInvalidNodesDepth(listProofElementsInfo)) {
105+
} else if (hasInvalidNodesDepth(listProofInfo.get(NodeType.ELEMENT))) {
103106
proofStatus = ListProofStatus.INVALID_NODE_DEPTH;
104-
} else if (hasNoElementNodes(listProofBranchesInfo, listProofElementsInfo)) {
107+
} else if (hasNoElementNodes(listProofInfo.get(NodeType.BRANCH),
108+
listProofInfo.get(NodeType.ELEMENT))) {
105109
proofStatus = ListProofStatus.INVALID_TREE_NO_ELEMENTS;
106-
} else if (hashNodesLimitExceeded(listProofBranchesInfo)) {
110+
} else if (hashNodesLimitExceeded(listProofInfo.get(NodeType.BRANCH))) {
107111
proofStatus = ListProofStatus.INVALID_HASH_NODES_COUNT;
112+
} else if (hasInvalidAbsentNodes()) {
113+
proofStatus = ListProofStatus.INVALID_PROOF_OF_ABSENCE;
108114
}
109115
}
110116

@@ -120,6 +126,8 @@ private NodeType getNodeType(ListProofNode node) {
120126
return NodeType.ELEMENT;
121127
} else if (node instanceof ListProofHashNode) {
122128
return NodeType.HASHNODE;
129+
} else if (node instanceof ListProofOfAbsence) {
130+
return NodeType.ABSENCE;
123131
} else {
124132
throw new RuntimeException("Unknown tree node type: " + node);
125133
}
@@ -131,7 +139,7 @@ private NodeType getNodeType(ListProofNode node) {
131139
* @param nodes collection of node info
132140
* @return true if node depth is invalid
133141
*/
134-
private boolean exceedsMaxDepth(List<NodeInfo> nodes) {
142+
private boolean exceedsMaxDepth(Collection<NodeInfo> nodes) {
135143
return nodes.stream()
136144
.anyMatch(nodeInfo -> nodeInfo.getDepth() > MAX_NODE_DEPTH);
137145
}
@@ -142,7 +150,7 @@ private boolean exceedsMaxDepth(List<NodeInfo> nodes) {
142150
* @param nodes collection of node info
143151
* @return true if node depths vary
144152
*/
145-
private boolean hasInvalidNodesDepth(List<NodeInfo> nodes) {
153+
private boolean hasInvalidNodesDepth(Collection<NodeInfo> nodes) {
146154
long depthsCount = nodes.stream()
147155
.map(NodeInfo::getDepth)
148156
.distinct()
@@ -157,7 +165,7 @@ private boolean hasInvalidNodesDepth(List<NodeInfo> nodes) {
157165
* @param nodes collection of node info
158166
* @return true if node depths vary
159167
*/
160-
private boolean hasNoElementNodes(List<NodeInfo> branches, List<NodeInfo> nodes) {
168+
private boolean hasNoElementNodes(Collection<NodeInfo> branches, Collection<NodeInfo> nodes) {
161169
return branches.size() > 0 && nodes.size() == 0;
162170
}
163171

@@ -167,7 +175,7 @@ private boolean hasNoElementNodes(List<NodeInfo> branches, List<NodeInfo> nodes)
167175
* @param branches collection of branches info
168176
* @return true if branch contains only hash nodes.
169177
*/
170-
private boolean hashNodesLimitExceeded(List<NodeInfo> branches) {
178+
private boolean hashNodesLimitExceeded(Collection<NodeInfo> branches) {
171179
return branches.stream()
172180
.anyMatch(this::invalidBranch);
173181
}
@@ -181,6 +189,26 @@ private boolean invalidBranch(NodeInfo branch) {
181189
.allMatch(nodeType -> (nodeType == NodeType.HASHNODE) || (nodeType == NodeType.NONE));
182190
}
183191

192+
/**
193+
* Returns true if this proof is a proof of absence and doesn't have a valid structure:
194+
* contains more than one node of absence, or contains any nodes other than a node of absence,
195+
* or the node of absence has non-zero depth.
196+
*/
197+
private boolean hasInvalidAbsentNodes() {
198+
Collection<NodeInfo> absentNodes = listProofInfo.get(NodeType.ABSENCE);
199+
int absentNodesSize = absentNodes.size();
200+
if (absentNodesSize > 1) {
201+
return true;
202+
} else if (absentNodesSize == 1) {
203+
boolean singleNodeTree = listProofInfo.get(NodeType.BRANCH).isEmpty()
204+
&& listProofInfo.get(NodeType.HASHNODE).isEmpty()
205+
&& listProofInfo.get(NodeType.ELEMENT).isEmpty();
206+
return !singleNodeTree || absentNodes.stream().allMatch(node -> node.getDepth() != 0);
207+
} else {
208+
return false;
209+
}
210+
}
211+
184212
/**
185213
* Returns proof status.
186214
*/
@@ -238,6 +266,6 @@ List<NodeType> getChildElementsTypes() {
238266
* Enum used to identify tree node type.
239267
*/
240268
private enum NodeType {
241-
BRANCH, ELEMENT, HASHNODE, NONE
269+
BRANCH, ELEMENT, HASHNODE, ABSENCE, NONE
242270
}
243271
}

exonum-java-binding/common/src/main/java/com/exonum/binding/common/proofs/list/ListProofVisitor.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,6 @@ public interface ListProofVisitor {
2323
void visit(ListProofHashNode listProofHashNode);
2424

2525
void visit(ListProofElement value);
26+
27+
void visit(ListProofOfAbsence listProofOfAbsence);
2628
}

exonum-java-binding/common/src/test/java/com/exonum/binding/common/proofs/list/ListProofStructureValidatorTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,15 @@ void visit_ProofRightValue() {
100100
assertTrue(validator.isValid());
101101
}
102102

103+
@Test
104+
void visit_ProofOfAbsence() {
105+
ListProofNode root = new ListProofOfAbsence(H1);
106+
107+
validator = createListProofStructureValidator(root);
108+
109+
assertTrue(validator.isValid());
110+
}
111+
103112
@Test
104113
void visit_InvalidTreeHasNoElements() {
105114
ListProofNode left = new ListProofHashNode(H1);
@@ -218,6 +227,22 @@ void visit_InvalidBranchHasMissingRightElement() {
218227
assertFalse(validator.isValid());
219228
}
220229

230+
@Test
231+
void visit_InvalidProofOfAbsence() {
232+
ListProofBranch root = new ListProofBranch(
233+
new ListProofBranch(
234+
leafOf(V1),
235+
// Having proof of absence not as a proof tree root is not allowed
236+
new ListProofOfAbsence(H1)
237+
),
238+
new ListProofHashNode(H2)
239+
);
240+
241+
validator = createListProofStructureValidator(root);
242+
243+
assertThat(validator.getProofStatus(), is(ListProofStatus.INVALID_PROOF_OF_ABSENCE));
244+
}
245+
221246
private ListProofStructureValidator createListProofStructureValidator(ListProofNode listProof) {
222247
return new ListProofStructureValidator(listProof);
223248
}

exonum-java-binding/core/rust/src/storage/proof_list_index.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
use exonum::crypto::Hash;
1616
use exonum_merkledb::{
17-
proof_list_index::{ListProof, ProofListIndexIter},
17+
proof_list_index::{ListProof, ProofListIndexIter, ProofOfAbsence},
1818
Fork, ObjectHash, ProofListIndex, Snapshot,
1919
};
2020
use jni::{
@@ -402,8 +402,8 @@ fn make_java_proof<'a>(env: &JNIEnv<'a>, proof: &ListProof<Value>) -> Result<JOb
402402
make_java_proof_branch(env, left, right)
403403
}
404404
ListProof::Leaf(ref value) => make_java_proof_element(env, value),
405-
ListProof::Absent(_) => {
406-
unimplemented!("The ProofOfAbsence structure is not public for the moment")
405+
ListProof::Absent(ref proof_of_absence) => {
406+
make_java_proof_of_absence(env, proof_of_absence)
407407
}
408408
}
409409
}
@@ -442,3 +442,15 @@ fn make_java_hash_node<'a>(env: &JNIEnv<'a>, hash: &Hash) -> Result<JObject<'a>>
442442
&[hash.as_obj().into()],
443443
)
444444
}
445+
446+
fn make_java_proof_of_absence<'a>(
447+
env: &JNIEnv<'a>,
448+
proof_of_absence: &ProofOfAbsence,
449+
) -> Result<JObject<'a>> {
450+
let hash = env.auto_local(utils::convert_hash(env, &proof_of_absence.merkle_root())?.into());
451+
env.new_object(
452+
"com/exonum/binding/common/proofs/list/ListProofOfAbsence",
453+
"([B)V",
454+
&[hash.as_obj().into()],
455+
)
456+
}

0 commit comments

Comments
 (0)