values();
/**
- * Returns an iterator over the map entries.
- * The entries are ordered by keys in lexicographical order.
+ * Returns an iterator over the map entries. The entries are ordered by keys in lexicographical
+ * order.
*
* @throws IllegalStateException if this map is not valid
*/
diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ProofMapIndexProxy.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ProofMapIndexProxy.java
index 4a967a9e54..dad64901d4 100644
--- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ProofMapIndexProxy.java
+++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ProofMapIndexProxy.java
@@ -44,8 +44,29 @@
* that a certain key is mapped to a particular value or that there are no mapping for
* the key in the map.
*
- * This map is implemented as a Merkle-Patricia tree. It does not permit null keys and values,
- * and requires that keys are 32-byte long.
+ *
This map is implemented as a Merkle-Patricia tree. It does not permit null keys and values.
+ *
+ *
The Merkle-Patricia tree backing the proof map uses internal 32-byte keys. The tree balance
+ * relies on the internal keys being uniformly distributed.
+ *
+ *
+ *
+ * By default, when creating the proof map using methods
+ * {@link #newInstance(String, View, Serializer, Serializer)} and
+ * {@link #newInGroupUnsafe(String, byte[], View, Serializer, Serializer)}, the user keys are
+ * converted into internal keys through hashing. This allows to use keys of arbitrary size and
+ * ensures the balance of the internal tree. It is also possible to create a proof map that will
+ * not hash keys with methods {@link #newInstanceNoKeyHashing(String, View, Serializer, Serializer)}
+ * and {@link #newInGroupUnsafeNoKeyHashing(String, byte[], View, Serializer, Serializer)}. In this
+ * mode, the map will use the user keys as internal tree keys. Such mode of operation is
+ * appropriate iff all of the following conditions hold:
+ *
+ *
+ * - All keys are 32-byte long
+ * - The keys are uniformly distributed
+ * - The keys come from a trusted source that cannot influence their distribution and affect
+ * the tree balance.
+ *
*
* The "destructive" methods of the map, i.e., the one that change the map contents,
* are specified to throw {@link UnsupportedOperationException} if
@@ -58,13 +79,13 @@
*
When the view goes out of scope, this map is destroyed. Subsequent use of the closed map
* is prohibited and will result in {@link IllegalStateException}.
*
- * @param the type of keys in this map. Must be 32-byte long in the serialized form
+ * @param the type of keys in this map
* @param the type of values in this map
* @see View
*/
public final class ProofMapIndexProxy extends AbstractIndexProxy implements MapIndex {
- private final ProofMapKeyCheckingSerializerDecorator keySerializer;
+ private final Serializer keySerializer;
private final CheckingSerializerDecorator valueSerializer;
/**
@@ -74,7 +95,7 @@ public final class ProofMapIndexProxy extends AbstractIndexProxy implement
* [a-zA-Z0-9_]
* @param view a database view. Must be valid.
* If a view is read-only, "destructive" operations are not permitted.
- * @param keySerializer a serializer of keys, must always produce 32-byte long values
+ * @param keySerializer a serializer of keys
* @param valueSerializer a serializer of values
* @param the type of keys in the map
* @param the type of values in the map
@@ -86,9 +107,34 @@ public static ProofMapIndexProxy newInstance(
String name, View view, Serializer keySerializer, Serializer valueSerializer) {
IndexAddress address = IndexAddress.valueOf(name);
long viewNativeHandle = view.getViewNativeHandle();
- LongSupplier nativeMapConstructor = () -> nativeCreate(name, viewNativeHandle);
+ LongSupplier nativeMapConstructor = () -> nativeCreate(name, viewNativeHandle, true);
+
+ return getOrCreate(address, view, keySerializer, valueSerializer, nativeMapConstructor, true);
+ }
+
+ /**
+ * Creates a ProofMapIndexProxy that uses non-hashed keys.
+ * Requires that keys are 32-byte long.
+ *
+ * @param name a unique alphanumeric non-empty identifier of this map in the underlying storage:
+ * [a-zA-Z0-9_]
+ * @param view a database view. Must be valid.
+ * If a view is read-only, "destructive" operations are not permitted.
+ * @param keySerializer a serializer of keys, must always produce 32-byte long values
+ * @param valueSerializer a serializer of values
+ * @param the type of keys in the map
+ * @param the type of values in the map
+ * @throws IllegalStateException if the view is not valid
+ * @throws IllegalArgumentException if the name is empty
+ * @see StandardSerializers
+ */
+ public static ProofMapIndexProxy newInstanceNoKeyHashing(
+ String name, View view, Serializer keySerializer, Serializer valueSerializer) {
+ IndexAddress address = IndexAddress.valueOf(name);
+ long viewNativeHandle = view.getViewNativeHandle();
+ LongSupplier nativeMapConstructor = () -> nativeCreate(name, viewNativeHandle, false);
- return getOrCreate(address, view, keySerializer, valueSerializer, nativeMapConstructor);
+ return getOrCreate(address, view, keySerializer, valueSerializer, nativeMapConstructor, false);
}
/**
@@ -100,6 +146,36 @@ public static ProofMapIndexProxy newInstance(
* @param groupName a name of the collection group
* @param mapId an identifier of this collection in the group, see the caveats
* @param view a database view
+ * @param keySerializer a serializer of keys
+ * @param valueSerializer a serializer of values
+ * @param the type of keys in the map
+ * @param the type of values in the map
+ * @return a new map proxy
+ * @throws IllegalStateException if the view is not valid
+ * @throws IllegalArgumentException if the name or index id is empty
+ * @see StandardSerializers
+ */
+ public static ProofMapIndexProxy newInGroupUnsafe(
+ String groupName, byte[] mapId, View view, Serializer keySerializer,
+ Serializer valueSerializer) {
+ IndexAddress address = IndexAddress.valueOf(groupName, mapId);
+ long viewNativeHandle = view.getViewNativeHandle();
+ LongSupplier nativeMapConstructor =
+ () -> nativeCreateInGroup(groupName, mapId, viewNativeHandle, true);
+
+ return getOrCreate(address, view, keySerializer, valueSerializer, nativeMapConstructor, true);
+ }
+
+ /**
+ * Creates a new proof map that uses non-hashed keys
+ * in a collection group with the given name.
+ * Requires that keys are 32-byte long.
+ *
+ * See a caveat on index identifiers.
+ *
+ * @param groupName a name of the collection group
+ * @param mapId an identifier of this collection in the group, see the caveats
+ * @param view a database view
* @param keySerializer a serializer of keys, must always produce 32-byte long values
* @param valueSerializer a serializer of values
* @param the type of keys in the map
@@ -109,26 +185,24 @@ public static ProofMapIndexProxy newInstance(
* @throws IllegalArgumentException if the name or index id is empty
* @see StandardSerializers
*/
- public static ProofMapIndexProxy newInGroupUnsafe(String groupName,
- byte[] mapId,
- View view,
- Serializer keySerializer,
- Serializer valueSerializer) {
+ public static ProofMapIndexProxy newInGroupUnsafeNoKeyHashing(
+ String groupName, byte[] mapId, View view, Serializer keySerializer,
+ Serializer valueSerializer) {
IndexAddress address = IndexAddress.valueOf(groupName, mapId);
long viewNativeHandle = view.getViewNativeHandle();
LongSupplier nativeMapConstructor =
- () -> nativeCreateInGroup(groupName, mapId, viewNativeHandle);
+ () -> nativeCreateInGroup(groupName, mapId, viewNativeHandle, false);
- return getOrCreate(address, view, keySerializer, valueSerializer, nativeMapConstructor);
+ return getOrCreate(address, view, keySerializer, valueSerializer, nativeMapConstructor, false);
}
private static ProofMapIndexProxy getOrCreate(IndexAddress address, View view,
Serializer keySerializer, Serializer valueSerializer,
- LongSupplier nativeMapConstructor) {
+ LongSupplier nativeMapConstructor, boolean keyHashing) {
return view.findOpenIndex(address)
.map(ProofMapIndexProxy::checkCachedInstance)
.orElseGet(() -> newMapIndexProxy(address, view, keySerializer, valueSerializer,
- nativeMapConstructor));
+ nativeMapConstructor, keyHashing));
}
@SuppressWarnings("unchecked") // The compiler is correct: the cache is not type-safe: ECR-3387
@@ -139,9 +213,8 @@ private static ProofMapIndexProxy checkCachedInstance(StorageIndex
private static ProofMapIndexProxy newMapIndexProxy(IndexAddress address, View view,
Serializer keySerializer, Serializer valueSerializer,
- LongSupplier nativeMapConstructor) {
- ProofMapKeyCheckingSerializerDecorator ks =
- ProofMapKeyCheckingSerializerDecorator.from(keySerializer);
+ LongSupplier nativeMapConstructor, boolean keyHashing) {
+ Serializer ks = decorateKeySerializer(keySerializer, keyHashing);
CheckingSerializerDecorator vs = CheckingSerializerDecorator.from(valueSerializer);
NativeHandle mapNativeHandle = createNativeMap(view, nativeMapConstructor);
@@ -151,6 +224,15 @@ private static ProofMapIndexProxy newMapIndexProxy(IndexAddress add
return map;
}
+ private static Serializer decorateKeySerializer(
+ Serializer keySerializer, boolean keyHashing) {
+ if (!keyHashing) {
+ return ProofMapKeyCheckingSerializerDecorator.from(keySerializer);
+ } else {
+ return CheckingSerializerDecorator.from(keySerializer);
+ }
+ }
+
private static NativeHandle createNativeMap(View view, LongSupplier nativeMapConstructor) {
NativeHandle mapNativeHandle = new NativeHandle(nativeMapConstructor.getAsLong());
@@ -160,13 +242,13 @@ private static NativeHandle createNativeMap(View view, LongSupplier nativeMapCon
return mapNativeHandle;
}
- private static native long nativeCreate(String name, long viewNativeHandle);
+ private static native long nativeCreate(String name, long viewNativeHandle, boolean keyHashing);
private static native long nativeCreateInGroup(String groupName, byte[] mapId,
- long viewNativeHandle);
+ long viewNativeHandle, boolean keyHashing);
private ProofMapIndexProxy(NativeHandle nativeHandle, IndexAddress address, View view,
- ProofMapKeyCheckingSerializerDecorator keySerializer,
+ Serializer keySerializer,
CheckingSerializerDecorator valueSerializer) {
super(nativeHandle, address, view);
this.keySerializer = keySerializer;
@@ -184,10 +266,11 @@ public boolean containsKey(K key) {
/**
* {@inheritDoc}
*
- * @param key a proof map key, must be 32-byte long when serialized
+ * @param key a proof map key
* @param value a storage value to associate with the key
* @throws IllegalStateException if this map is not valid
- * @throws IllegalArgumentException if the size of the key is not 32 bytes
+ * @throws IllegalArgumentException if the size of the key is not 32 bytes (in case of a
+ * proof map that uses non-hashed keys)
* @throws UnsupportedOperationException if this map is read-only
*/
@Override
@@ -227,11 +310,11 @@ public V get(K key) {
* Returns a proof that there are values mapped to the specified keys or that there are no such
* mappings.
*
- * @param key a proof map key which might be mapped to some value, must be 32-byte long
- * @param otherKeys other proof map keys which might be mapped to some values, each must be
- * 32-byte long
+ * @param key a proof map key which might be mapped to some value
+ * @param otherKeys other proof map keys which might be mapped to some values
* @throws IllegalStateException if this map is not valid
- * @throws IllegalArgumentException if the size of any of the keys is not 32 bytes
+ * @throws IllegalArgumentException if the size of any of the keys is not 32 bytes (in case of a
+ * proof map that uses non-hashed keys)
*/
public UncheckedMapProof getProof(K key, K... otherKeys) {
if (otherKeys.length == 0) {
@@ -246,10 +329,11 @@ public UncheckedMapProof getProof(K key, K... otherKeys) {
* Returns a proof that there are values mapped to the specified keys or that there are no such
* mappings.
*
- * @param keys proof map keys which might be mapped to some values, each must be 32-byte long
+ * @param keys proof map keys which might be mapped to some values
* @throws IllegalStateException if this map is not valid
- * @throws IllegalArgumentException if the size of any of the keys is not 32 bytes
- * or keys collection is empty
+ * @throws IllegalArgumentException if the size of any of the keys is not 32 bytes (in case of a
+ * proof map that uses non-hashed keys) or
+ * keys collection is empty
*/
public UncheckedMapProof getProof(Collection extends K> keys) {
checkArgument(!keys.isEmpty(), "Keys collection should not be empty");
diff --git a/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/BaseProofMapIndexProxyIntegrationTestable.java b/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/BaseProofMapIndexProxyIntegrationTestable.java
new file mode 100644
index 0000000000..f7f2e7f357
--- /dev/null
+++ b/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/BaseProofMapIndexProxyIntegrationTestable.java
@@ -0,0 +1,482 @@
+/*
+ * Copyright 2019 The Exonum Team
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.exonum.binding.core.storage.indices;
+
+import static com.exonum.binding.core.storage.indices.CheckedMapProofMatcher.isValid;
+import static com.exonum.binding.core.storage.indices.MapEntries.putAll;
+import static com.exonum.binding.core.storage.indices.MapTestEntry.absentEntry;
+import static com.exonum.binding.core.storage.indices.MapTestEntry.presentEntry;
+import static com.exonum.binding.core.storage.indices.ProofMapContainsMatcher.provesThatAbsent;
+import static com.exonum.binding.core.storage.indices.ProofMapContainsMatcher.provesThatCorrect;
+import static com.exonum.binding.core.storage.indices.ProofMapContainsMatcher.provesThatPresent;
+import static com.exonum.binding.core.storage.indices.StoragePreconditions.PROOF_MAP_KEY_SIZE;
+import static com.exonum.binding.core.storage.indices.StoragePreconditions.checkProofKey;
+import static com.exonum.binding.core.storage.indices.TestStorageItems.V1;
+import static com.exonum.binding.core.storage.indices.TestStorageItems.V2;
+import static com.exonum.binding.core.storage.indices.TestStorageItems.V3;
+import static com.exonum.binding.core.storage.indices.TestStorageItems.V4;
+import static com.exonum.binding.core.storage.indices.TestStorageItems.values;
+import static com.exonum.binding.test.Bytes.bytes;
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Collections.singletonList;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.IsNot.not;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.exonum.binding.common.collect.MapEntry;
+import com.exonum.binding.common.hash.HashCode;
+import com.exonum.binding.common.hash.Hashing;
+import com.exonum.binding.common.proofs.map.CheckedMapProof;
+import com.exonum.binding.common.proofs.map.UncheckedMapProof;
+import com.exonum.binding.common.serialization.StandardSerializers;
+import com.exonum.binding.core.proxy.Cleaner;
+import com.exonum.binding.core.proxy.CloseFailuresException;
+import com.exonum.binding.core.storage.database.View;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Streams;
+import com.google.common.primitives.UnsignedBytes;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Test;
+
+abstract class BaseProofMapIndexProxyIntegrationTestable
+ extends BaseIndexProxyTestable> {
+
+ private static final String MAP_NAME = "test_proof_map";
+
+ private final HashCode key1 = getTestKeys().get(0);
+ private final HashCode key2 = getTestKeys().get(1);
+ private final HashCode key3 = getTestKeys().get(2);
+
+ private static final HashCode EMPTY_MAP_INDEX_HASH = HashCode.fromString(
+ "7324b5c72b51bb5d4c180f1109cfd347b60473882145841c39f3e584576296f9");
+
+ abstract List getTestKeys();
+
+ @Test
+ void containsKey() {
+ runTestWithView(database::createFork, (map) -> {
+ map.put(key1, V1);
+ assertTrue(map.containsKey(key1));
+ });
+ }
+
+ @Test
+ void doesNotContainAbsentKey() {
+ runTestWithView(database::createFork, (map) -> {
+ map.put(key1, V1);
+ assertFalse(map.containsKey(key2));
+ });
+ }
+
+ @Test
+ void emptyMapDoesNotContainAbsentKey() {
+ runTestWithView(database::createSnapshot, (map) -> assertFalse(map.containsKey(key2)));
+ }
+
+ @Test
+ void containsThrowsIfNullKey() {
+ runTestWithView(database::createSnapshot,
+ (map) -> assertThrows(NullPointerException.class, () -> map.containsKey(null)));
+ }
+
+ @Test
+ void putFailsIfSnapshot() {
+ runTestWithView(database::createSnapshot,
+ (map) -> assertThrows(UnsupportedOperationException.class, () -> map.put(key1, V1)));
+ }
+
+ @Test
+ void putAllInEmptyMap() {
+ runTestWithView(database::createFork, (map) -> {
+ ImmutableMap source = ImmutableMap.of(
+ key1, V1,
+ key2, V2
+ );
+
+ map.putAll(source);
+
+ // Check that the map contains all items
+ for (Map.Entry entry : source.entrySet()) {
+ HashCode key = entry.getKey();
+ assertTrue(map.containsKey(key));
+ assertThat(map.get(key), equalTo(entry.getValue()));
+ }
+ });
+ }
+
+ @Test
+ void putAllOverwritingEntries() {
+ runTestWithView(database::createFork, (map) -> {
+ map.putAll(ImmutableMap.of(
+ key1, V1,
+ key2, V2
+ ));
+
+ ImmutableMap replacements = ImmutableMap.of(
+ key1, V3,
+ key2, V4
+ );
+
+ map.putAll(replacements);
+
+ // Check that the map contains the recently put entries
+ for (Map.Entry entry : replacements.entrySet()) {
+ HashCode key = entry.getKey();
+ assertTrue(map.containsKey(key));
+ assertThat(map.get(key), equalTo(entry.getValue()));
+ }
+ });
+ }
+
+ @Test
+ void get() {
+ runTestWithView(database::createFork, (map) -> {
+ map.put(key1, V1);
+
+ assertThat(map.get(key1), equalTo(V1));
+ });
+ }
+
+ @Test
+ void getIndexHash_EmptyMap() {
+ runTestWithView(database::createSnapshot,
+ (map) -> assertThat(map.getIndexHash(), equalTo(EMPTY_MAP_INDEX_HASH)));
+ }
+
+ @Test
+ void getIndexHash_NonEmptyMap() {
+ runTestWithView(database::createFork, (map) -> {
+ map.put(key1, V1);
+
+ HashCode indexHash = map.getIndexHash();
+ assertThat(indexHash, notNullValue());
+ assertThat(indexHash.bits(), equalTo(Hashing.DEFAULT_HASH_SIZE_BITS));
+ assertThat(indexHash, not(equalTo(EMPTY_MAP_INDEX_HASH)));
+ });
+ }
+
+ @Test
+ void getProof_EmptyMapDoesNotContainSingleKey() {
+ runTestWithView(database::createSnapshot,
+ (map) -> assertThat(map, provesThatAbsent(key1))
+ );
+ }
+
+ @Test
+ void getProof_SingletonMapContains() {
+ runTestWithView(database::createFork, (map) -> {
+ HashCode key = key1;
+ String value = V1;
+ map.put(key, value);
+
+ assertThat(map, provesThatPresent(key, value));
+ });
+ }
+
+ @Test
+ void getProof_SingletonMapDoesNotContain() {
+ runTestWithView(database::createFork, (map) -> {
+ map.put(key1, V1);
+
+ assertThat(map, provesThatAbsent(key2));
+ });
+ }
+
+ @Test
+ void getProof_MultiEntryMapContains() {
+ runTestWithView(database::createFork, (map) -> {
+ List> entries = createMapEntries();
+ putAll(map, entries);
+
+ for (MapEntry e : entries) {
+ assertThat(map, provesThatPresent(e.getKey(), e.getValue()));
+ }
+ });
+ }
+
+ @Test
+ void getMultiProof_MultiEntryMapContains() {
+ runTestWithView(database::createFork, (map) -> {
+ List> entries = createMapEntries();
+ putAll(map, entries);
+
+ assertThat(map, provesThatPresent(entries));
+ });
+ }
+
+ @Test
+ void getProof_MultiEntryMapDoesNotContain() {
+ runTestWithView(database::createFork, (map) -> {
+ List> entries = createMapEntries();
+ putAll(map, entries);
+
+ byte[] allOnes = new byte[PROOF_MAP_KEY_SIZE];
+ Arrays.fill(allOnes, UnsignedBytes.checkedCast(0xFF));
+
+ List otherKeys = ImmutableList.of(
+ HashCode.fromBytes(allOnes), // [11…1]
+ createProofKey("PK1001"),
+ createProofKey("PK1002"),
+ createProofKey("PK100500")
+ );
+
+ for (HashCode key : otherKeys) {
+ assertThat(map, provesThatAbsent(key));
+ }
+ });
+ }
+
+ @Test
+ void getMultiProof_EmptyMapDoesNotContainSeveralKeys() {
+ runTestWithView(database::createSnapshot, (map) ->
+ assertThat(map, provesThatAbsent(key1, key2)));
+ }
+
+ @Test
+ void getMultiProof_SingletonMapDoesNotContainSeveralKeys() {
+ runTestWithView(database::createFork, (map) -> {
+ map.put(key1, V1);
+
+ assertThat(map, provesThatAbsent(key2, key3));
+ });
+ }
+
+ @Test
+ void getMultiProof_SingletonMapBothContainsAndDoesNot() {
+ runTestWithView(database::createFork, (map) -> {
+ ImmutableMap source = ImmutableMap.of(
+ key1, V1
+ );
+
+ map.putAll(source);
+
+ assertThat(map, provesThatCorrect(presentEntry(key1, V1), absentEntry(key2)));
+ });
+ }
+
+ @Test
+ void getMultiProof_TwoElementMapContains() {
+ runTestWithView(database::createFork, (map) -> {
+ ImmutableMap source = ImmutableMap.of(
+ key1, V1,
+ key2, V2
+ );
+
+ map.putAll(source);
+
+ assertThat(map, provesThatCorrect(presentEntry(key1, V1), presentEntry(key2, V2)));
+ });
+ }
+
+ @Test
+ void remove() {
+ runTestWithView(database::createFork, (map) -> {
+ map.put(key1, V1);
+ map.remove(key1);
+
+ assertNull(map.get(key1));
+ assertFalse(map.containsKey(key1));
+ });
+ }
+
+ @Test
+ void removeFailsIfSnapshot() {
+ runTestWithView(database::createSnapshot,
+ (map) -> assertThrows(UnsupportedOperationException.class, () -> map.remove(key1)));
+ }
+
+ @Test
+ void clearEmptyHasNoEffect() {
+ runTestWithView(database::createFork, ProofMapIndexProxy::clear);
+ }
+
+ @Test
+ void clearNonEmptyRemovesAllValues() {
+ runTestWithView(database::createFork, (map) -> {
+ List> entries = createMapEntries();
+
+ putAll(map, entries);
+
+ map.clear();
+
+ for (MapEntry e : entries) {
+ assertFalse(map.containsKey(e.getKey()));
+ }
+ });
+ }
+
+ @Test
+ void clearFailsIfSnapshot() {
+ runTestWithView(database::createSnapshot, (map) -> {
+ assertThrows(UnsupportedOperationException.class, () -> map.clear());
+ });
+ }
+
+ @Test
+ void isEmptyShouldReturnTrueForEmptyMap() {
+ runTestWithView(database::createSnapshot, (map) -> assertTrue(map.isEmpty()));
+ }
+
+ @Test
+ void isEmptyShouldReturnFalseForNonEmptyMap() {
+ runTestWithView(database::createFork, (map) -> {
+ map.put(key1, V1);
+
+ assertFalse(map.isEmpty());
+ });
+ }
+
+ @Test
+ void getProofFromSingleKey() {
+ runTestWithView(database::createFork, (map) -> {
+ map.put(key1, V1);
+
+ UncheckedMapProof proof = map.getProof(key1);
+ CheckedMapProof checkedProof = proof.check();
+
+ assertThat(checkedProof, isValid(singletonList(presentEntry(key1, V1))));
+ });
+ }
+
+ @Test
+ void getProofFromVarargs() {
+ runTestWithView(database::createFork, (map) -> {
+ map.put(key1, V1);
+ map.put(key2, V2);
+
+ UncheckedMapProof proof = map.getProof(key1, key2);
+ CheckedMapProof checkedProof = proof.check();
+
+ assertThat(
+ checkedProof, isValid(Arrays.asList(presentEntry(key1, V1), presentEntry(key2, V2))));
+ });
+ }
+
+ @Test
+ void getProofFromEmptyCollection() {
+ runTestWithView(database::createFork, (map) -> {
+ map.put(key1, V1);
+
+ IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
+ UncheckedMapProof proof = map.getProof(Collections.emptyList());
+ });
+ assertThat(thrown.getLocalizedMessage(),
+ containsString("Keys collection should not be empty"));
+ });
+ }
+
+ @Test
+ void getProofFromCollection() {
+ runTestWithView(database::createFork, (map) -> {
+ map.put(key1, V1);
+
+ UncheckedMapProof proof = map.getProof(singletonList(key1));
+ CheckedMapProof checkedProof = proof.check();
+
+ assertThat(checkedProof, isValid(singletonList(presentEntry(key1, V1))));
+ });
+ }
+
+ /**
+ * Create a proof key of length 32 with the specified suffix.
+ *
+ * @param suffix a key suffix. Must be shorter than or equal to 32 bytes in UTF-8.
+ * @return a key, starting with zeroes and followed by the specified suffix encoded in UTF-8
+ */
+ private static HashCode createProofKey(String suffix) {
+ byte[] suffixBytes = bytes(suffix);
+ return createProofKey(suffixBytes);
+ }
+
+ static HashCode createProofKey(byte... suffixBytes) {
+ byte[] proofKey = createRawProofKey(suffixBytes);
+ return HashCode.fromBytes(proofKey);
+ }
+
+ static byte[] createRawProofKey(byte... suffixBytes) {
+ checkArgument(suffixBytes.length <= PROOF_MAP_KEY_SIZE);
+ byte[] proofKey = new byte[PROOF_MAP_KEY_SIZE];
+ System.arraycopy(suffixBytes, 0, proofKey, PROOF_MAP_KEY_SIZE - suffixBytes.length,
+ suffixBytes.length);
+ return checkProofKey(proofKey);
+ }
+
+ void runTestWithView(Function viewFactory,
+ Consumer> mapTest) {
+ runTestWithView(viewFactory, (ignoredView, map) -> mapTest.accept(map));
+ }
+
+ private void runTestWithView(
+ Function viewFactory,
+ BiConsumer> mapTest) {
+ try (Cleaner cleaner = new Cleaner()) {
+ View view = viewFactory.apply(cleaner);
+ ProofMapIndexProxy map = this.create(MAP_NAME, view);
+
+ mapTest.accept(view, map);
+ } catch (CloseFailuresException e) {
+ throw new AssertionError("Unexpected exception", e);
+ }
+ }
+
+ @Override
+ StorageIndex createOfOtherType(String name, View view) {
+ return ListIndexProxy.newInstance(name, view, StandardSerializers.string());
+ }
+
+ @Override
+ Object getAnyElement(ProofMapIndexProxy index) {
+ return index.get(key1);
+ }
+
+ @Override
+ void update(ProofMapIndexProxy index) {
+ index.put(key1, V1);
+ }
+
+ List> createMapEntries() {
+ return createMapEntries(getTestKeys().stream());
+ }
+
+ /**
+ * Creates map entries with the given keys. Uses values
+ * from {@linkplain TestStorageItems#values} in a round-robin fashion.
+ */
+ List> createMapEntries(Stream proofKeys) {
+ Stream keys = proofKeys.distinct();
+ Stream roundRobinValues = IntStream.range(0, Integer.MAX_VALUE)
+ .mapToObj(i -> values.get(i % values.size()));
+ return Streams.zip(keys, roundRobinValues, MapEntry::valueOf)
+ .collect(Collectors.toList());
+ }
+}
diff --git a/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/ProofMapIndexProxyGroupIntegrationTest.java b/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/ProofMapIndexProxyGroupIntegrationTest.java
index 6cd951e810..9e22890c78 100644
--- a/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/ProofMapIndexProxyGroupIntegrationTest.java
+++ b/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/ProofMapIndexProxyGroupIntegrationTest.java
@@ -16,33 +16,34 @@
package com.exonum.binding.core.storage.indices;
-import static com.exonum.binding.core.storage.indices.ProofMapIndexProxyIntegrationTest.PK1;
-import static com.exonum.binding.core.storage.indices.ProofMapIndexProxyIntegrationTest.PK2;
-import static com.exonum.binding.core.storage.indices.ProofMapIndexProxyIntegrationTest.PK3;
+import static com.exonum.binding.core.storage.indices.TestStorageItems.K1;
+import static com.exonum.binding.core.storage.indices.TestStorageItems.K2;
+import static com.exonum.binding.core.storage.indices.TestStorageItems.K3;
-import com.exonum.binding.common.hash.HashCode;
import com.exonum.binding.common.serialization.StandardSerializers;
import com.exonum.binding.core.storage.database.View;
import com.google.common.collect.ImmutableMap;
+import org.junit.jupiter.api.Disabled;
-class ProofMapIndexProxyGroupIntegrationTest extends BaseMapIndexGroupTestable {
+@Disabled("Disabled until native support is implemented - ECR-3765")
+class ProofMapIndexProxyGroupIntegrationTest extends BaseMapIndexGroupTestable {
private static final String GROUP_NAME = "proof_map_group_IT";
@Override
- ImmutableMap> getTestEntriesById() {
- return ImmutableMap.>builder()
+ ImmutableMap> getTestEntriesById() {
+ return ImmutableMap.>builder()
.put("1", ImmutableMap.of())
- .put("2", ImmutableMap.of(PK1, "V1"))
- .put("3", ImmutableMap.of(PK2, "V2", PK3, "V3"))
- .put("4", ImmutableMap.of(PK3, "V3", PK2, "V2"))
- .put("5", ImmutableMap.of(PK1, "V5", PK2, "V6", PK3, "V7"))
+ .put("2", ImmutableMap.of(K1, "V1"))
+ .put("3", ImmutableMap.of(K2, "V2", K3, "V3"))
+ .put("4", ImmutableMap.of(K3, "V3", K2, "V2"))
+ .put("5", ImmutableMap.of(K1, "V5", K2, "V6", K3, "V7"))
.build();
}
@Override
- ProofMapIndexProxy createInGroup(byte[] mapId, View view) {
+ ProofMapIndexProxy createInGroup(byte[] mapId, View view) {
return ProofMapIndexProxy.newInGroupUnsafe(GROUP_NAME, mapId, view,
- StandardSerializers.hash(), StandardSerializers.string());
+ StandardSerializers.string(), StandardSerializers.string());
}
}
diff --git a/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/ProofMapIndexProxyGroupNoKeyHashingIntegrationTest.java b/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/ProofMapIndexProxyGroupNoKeyHashingIntegrationTest.java
new file mode 100644
index 0000000000..2fdcdde499
--- /dev/null
+++ b/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/ProofMapIndexProxyGroupNoKeyHashingIntegrationTest.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2019 The Exonum Team
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.exonum.binding.core.storage.indices;
+
+import static com.exonum.binding.core.storage.indices.ProofMapIndexProxyNoKeyHashingIntegrationTest.SORTED_TEST_KEYS;
+
+import com.exonum.binding.common.hash.HashCode;
+import com.exonum.binding.common.serialization.StandardSerializers;
+import com.exonum.binding.core.storage.database.View;
+import com.google.common.collect.ImmutableMap;
+
+class ProofMapIndexProxyGroupNoKeyHashingIntegrationTest
+ extends BaseMapIndexGroupTestable {
+
+ private static final HashCode PK1 = SORTED_TEST_KEYS.get(0);
+ private static final HashCode PK2 = SORTED_TEST_KEYS.get(1);
+ private static final HashCode PK3 = SORTED_TEST_KEYS.get(2);
+
+ private static final String GROUP_NAME = "proof_map_group_IT";
+
+ @Override
+ ImmutableMap> getTestEntriesById() {
+ return ImmutableMap.>builder()
+ .put("1", ImmutableMap.of())
+ .put("2", ImmutableMap.of(PK1, "V1"))
+ .put("3", ImmutableMap.of(PK2, "V2", PK3, "V3"))
+ .put("4", ImmutableMap.of(PK3, "V3", PK2, "V2"))
+ .put("5", ImmutableMap.of(PK1, "V5", PK2, "V6", PK3, "V7"))
+ .build();
+ }
+
+ @Override
+ ProofMapIndexProxy createInGroup(byte[] mapId, View view) {
+ return ProofMapIndexProxy.newInGroupUnsafeNoKeyHashing(GROUP_NAME, mapId, view,
+ StandardSerializers.hash(), StandardSerializers.string());
+ }
+}
diff --git a/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/ProofMapIndexProxyIntegrationTest.java b/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/ProofMapIndexProxyIntegrationTest.java
index cd528b6ac7..76205496b6 100644
--- a/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/ProofMapIndexProxyIntegrationTest.java
+++ b/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/ProofMapIndexProxyIntegrationTest.java
@@ -16,70 +16,21 @@
package com.exonum.binding.core.storage.indices;
-import static com.exonum.binding.core.storage.indices.CheckedMapProofMatcher.isValid;
-import static com.exonum.binding.core.storage.indices.MapEntries.putAll;
-import static com.exonum.binding.core.storage.indices.MapTestEntry.absentEntry;
-import static com.exonum.binding.core.storage.indices.MapTestEntry.presentEntry;
-import static com.exonum.binding.core.storage.indices.ProofMapContainsMatcher.provesThatAbsent;
-import static com.exonum.binding.core.storage.indices.ProofMapContainsMatcher.provesThatCorrect;
-import static com.exonum.binding.core.storage.indices.ProofMapContainsMatcher.provesThatPresent;
-import static com.exonum.binding.core.storage.indices.StoragePreconditions.PROOF_MAP_KEY_SIZE;
-import static com.exonum.binding.core.storage.indices.StoragePreconditions.checkProofKey;
-import static com.exonum.binding.core.storage.indices.TestStorageItems.V1;
-import static com.exonum.binding.core.storage.indices.TestStorageItems.V2;
-import static com.exonum.binding.core.storage.indices.TestStorageItems.V3;
-import static com.exonum.binding.core.storage.indices.TestStorageItems.V4;
-import static com.exonum.binding.core.storage.indices.TestStorageItems.values;
-import static com.exonum.binding.test.Bytes.bytes;
-import static com.exonum.binding.test.Bytes.createPrefixed;
-import static com.google.common.base.Preconditions.checkArgument;
-import static java.util.Collections.singletonList;
-import static org.hamcrest.CoreMatchers.containsString;
-import static org.hamcrest.CoreMatchers.equalTo;
-import static org.hamcrest.CoreMatchers.notNullValue;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.core.IsNot.not;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static java.util.stream.Collectors.toList;
-import com.exonum.binding.common.collect.MapEntry;
import com.exonum.binding.common.hash.HashCode;
-import com.exonum.binding.common.hash.Hashing;
-import com.exonum.binding.common.proofs.map.CheckedMapProof;
-import com.exonum.binding.common.proofs.map.UncheckedMapProof;
import com.exonum.binding.common.serialization.StandardSerializers;
-import com.exonum.binding.core.proxy.Cleaner;
-import com.exonum.binding.core.proxy.CloseFailuresException;
import com.exonum.binding.core.storage.database.View;
import com.exonum.binding.test.Bytes;
-import com.exonum.binding.test.CiOnly;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Streams;
-import com.google.common.primitives.UnsignedBytes;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.BitSet;
-import java.util.Collections;
-import java.util.Iterator;
import java.util.List;
-import java.util.Map;
-import java.util.function.BiConsumer;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
import java.util.stream.Stream;
-import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Disabled;
+@Disabled("Disabled until native support is implemented - ECR-3765")
class ProofMapIndexProxyIntegrationTest
- extends BaseIndexProxyTestable> {
+ extends BaseProofMapIndexProxyIntegrationTestable {
- private static final String MAP_NAME = "test_proof_map";
-
- private static final List PROOF_KEYS = Stream.of(
+ private static final List TEST_KEYS = Stream.of(
Bytes.bytes(0x00),
Bytes.bytes(0x01),
Bytes.bytes(0x02),
@@ -95,717 +46,18 @@ class ProofMapIndexProxyIntegrationTest
Bytes.bytes(0x10, 0x01),
Bytes.bytes(0x10, 0x10)
)
- .map(ProofMapIndexProxyIntegrationTest::createRawProofKey)
- .sorted(UnsignedBytes.lexicographicalComparator())
.map(HashCode::fromBytes)
- .collect(Collectors.toList());
-
- static final HashCode PK1 = PROOF_KEYS.get(0);
- static final HashCode PK2 = PROOF_KEYS.get(1);
- static final HashCode PK3 = PROOF_KEYS.get(2);
-
- private static final HashCode INVALID_PROOF_KEY = HashCode.fromString("1234");
-
- private static final HashCode EMPTY_MAP_INDEX_HASH = HashCode.fromString(
- "7324b5c72b51bb5d4c180f1109cfd347b60473882145841c39f3e584576296f9");
-
- @Test
- void containsKey() {
- runTestWithView(database::createFork, (map) -> {
- map.put(PK1, V1);
- assertTrue(map.containsKey(PK1));
- });
- }
-
- @Test
- void doesNotContainAbsentKey() {
- runTestWithView(database::createFork, (map) -> {
- map.put(PK1, V1);
- assertFalse(map.containsKey(PK2));
- });
- }
-
- @Test
- void emptyMapDoesNotContainAbsentKey() {
- runTestWithView(database::createSnapshot, (map) -> assertFalse(map.containsKey(PK2)));
- }
-
- @Test
- void containsThrowsIfNullKey() {
- runTestWithView(database::createSnapshot,
- (map) -> assertThrows(NullPointerException.class, () -> map.containsKey(null)));
- }
-
- @Test
- void containsThrowsIfInvalidKey() {
- runTestWithView(database::createSnapshot, (map) -> assertThrows(IllegalArgumentException.class,
- () -> map.containsKey(INVALID_PROOF_KEY)));
- }
-
- @Test
- void putFailsIfSnapshot() {
- runTestWithView(database::createSnapshot,
- (map) -> assertThrows(UnsupportedOperationException.class, () -> map.put(PK1, V1)));
- }
-
- @Test
- void putFailsIfInvalidKey() {
- runTestWithView(database::createFork, (map) -> assertThrows(IllegalArgumentException.class,
- () -> map.put(INVALID_PROOF_KEY, V1)));
- }
-
- @Test
- void putAllInEmptyMap() {
- runTestWithView(database::createFork, (map) -> {
- ImmutableMap source = ImmutableMap.of(
- PK1, V1,
- PK2, V2
- );
-
- map.putAll(source);
-
- // Check that the map contains all items
- for (Map.Entry entry : source.entrySet()) {
- HashCode key = entry.getKey();
- assertTrue(map.containsKey(key));
- assertThat(map.get(key), equalTo(entry.getValue()));
- }
- });
- }
-
- @Test
- void putAllOverwritingEntries() {
- runTestWithView(database::createFork, (map) -> {
- map.putAll(ImmutableMap.of(
- PK1, V1,
- PK2, V2
- ));
-
- ImmutableMap replacements = ImmutableMap.of(
- PK1, V3,
- PK2, V4
- );
-
- map.putAll(replacements);
-
- // Check that the map contains the recently put entries
- for (Map.Entry entry : replacements.entrySet()) {
- HashCode key = entry.getKey();
- assertTrue(map.containsKey(key));
- assertThat(map.get(key), equalTo(entry.getValue()));
- }
- });
- }
-
- @Test
- void get() {
- runTestWithView(database::createFork, (map) -> {
- map.put(PK1, V1);
-
- assertThat(map.get(PK1), equalTo(V1));
- });
- }
-
- @Test
- void getIndexHash_EmptyMap() {
- runTestWithView(database::createSnapshot,
- (map) -> assertThat(map.getIndexHash(), equalTo(EMPTY_MAP_INDEX_HASH)));
- }
-
- @Test
- void getIndexHash_NonEmptyMap() {
- runTestWithView(database::createFork, (map) -> {
- map.put(PK1, V1);
-
- HashCode indexHash = map.getIndexHash();
- assertThat(indexHash, notNullValue());
- assertThat(indexHash.bits(), equalTo(Hashing.DEFAULT_HASH_SIZE_BITS));
- assertThat(indexHash, not(equalTo(EMPTY_MAP_INDEX_HASH)));
- });
- }
-
- @Test
- void getProof_EmptyMapDoesNotContainSingleKey() {
- runTestWithView(database::createSnapshot,
- (map) -> assertThat(map, provesThatAbsent(PK1))
- );
- }
-
- @Test
- void getProof_SingletonMapContains() {
- runTestWithView(database::createFork, (map) -> {
- HashCode key = PK1;
- String value = V1;
- map.put(key, value);
-
- assertThat(map, provesThatPresent(key, value));
- });
- }
-
- @Test
- void getProof_SingletonMapDoesNotContain() {
- runTestWithView(database::createFork, (map) -> {
- map.put(PK1, V1);
-
- assertThat(map, provesThatAbsent(PK2));
- });
- }
-
- @Test
- void getProof_FourEntryMap_LastByte_Contains1() {
- runTestWithView(database::createFork, (map) -> {
-
- Stream proofKeys = Stream.of(
- (byte) 0b0000_0000,
- (byte) 0b0000_0001,
- (byte) 0b1000_0000,
- (byte) 0b1000_0001
- ).map(ProofMapIndexProxyIntegrationTest::createProofKey);
-
- List> entries = createMapEntries(proofKeys);
-
- putAll(map, entries);
-
- for (MapEntry e : entries) {
- assertThat(map, provesThatPresent(e.getKey(), e.getValue()));
- }
- });
- }
-
- @Test
- void getProof_FourEntryMap_LastByte_Contains2() {
- runTestWithView(database::createFork, (map) -> {
- Stream proofKeys = Stream.of(
- (byte) 0b00,
- (byte) 0b01,
- (byte) 0b10,
- (byte) 0b11
- ).map(ProofMapIndexProxyIntegrationTest::createProofKey);
-
- List> entries = createMapEntries(proofKeys);
-
- putAll(map, entries);
-
- for (MapEntry e : entries) {
- assertThat(map, provesThatPresent(e.getKey(), e.getValue()));
- }
- });
- }
-
- @Test
- void getProof_FourEntryMap_FirstByte_Contains() {
- runTestWithView(database::createFork, (map) -> {
- byte[] key1 = createRawProofKey();
- byte[] key2 = createRawProofKey();
- key2[0] = (byte) 0b0000_0001;
- byte[] key3 = createRawProofKey();
- key3[0] = (byte) 0b1000_0000;
- byte[] key4 = createRawProofKey();
- key4[0] = (byte) 0b1000_0001;
-
- List> entries = createMapEntries(
- Stream.of(key1, key2, key3, key4)
- .map(HashCode::fromBytes)
- );
-
- putAll(map, entries);
-
- for (MapEntry e : entries) {
- assertThat(map, provesThatPresent(e.getKey(), e.getValue()));
- }
- });
- }
-
- @Test
- void getProof_FourEntryMap_FirstAndLastByte_Contains() {
- runTestWithView(database::createFork, (map) -> {
- byte[] key1 = createRawProofKey(); // 000…0
- byte[] key2 = createRawProofKey(); // 100…0
- key2[0] = (byte) 0x01;
- byte[] key3 = createRawProofKey((byte) 0x80); // 000…01
- byte[] key4 = createRawProofKey((byte) 0x80); // 100…01
- key4[0] = (byte) 0x01;
-
- List> entries = createMapEntries(
- Stream.of(key1, key2, key3, key4)
- .map(HashCode::fromBytes)
- );
-
- putAll(map, entries);
-
- for (MapEntry e : entries) {
- assertThat(map, provesThatPresent(e.getKey(), e.getValue()));
- }
- });
- }
-
- @Test
- void getProof_MultiEntryMapContains() {
- runTestWithView(database::createFork, (map) -> {
- List> entries = createSortedMapEntries();
- putAll(map, entries);
-
- for (MapEntry e : entries) {
- assertThat(map, provesThatPresent(e.getKey(), e.getValue()));
- }
- });
- }
-
- @Test
- void getProof_MultiEntryMapDoesNotContain() {
- runTestWithView(database::createFork, (map) -> {
- List> entries = createSortedMapEntries();
- putAll(map, entries);
-
- byte[] allOnes = new byte[PROOF_MAP_KEY_SIZE];
- Arrays.fill(allOnes, UnsignedBytes.checkedCast(0xFF));
-
- List otherKeys = ImmutableList.of(
- HashCode.fromBytes(allOnes), // [11…1]
- createProofKey("PK1001"),
- createProofKey("PK1002"),
- createProofKey("PK100500")
- );
-
- for (HashCode key : otherKeys) {
- assertThat(map, provesThatAbsent(key));
- }
- });
- }
-
- @CiOnly
- @Test
- /*
- Takes quite a lot of time (validating 257 proofs), but it's an integration test, isn't it? :-)
- Consider adding a similar test for left-leaning MPT
- */
- void getProof_MapContainsRightLeaningMaxHeightMpt() {
- runTestWithView(database::createFork, (map) -> {
- List> entries = createEntriesForRightLeaningMpt();
- putAll(map, entries);
-
- for (MapEntry e : entries) {
- assertThat(map, provesThatPresent(e.getKey(), e.getValue()));
- }
- });
- }
-
- @Test
- void getMultiProof_EmptyMapDoesNotContainSeveralKeys() {
- runTestWithView(database::createSnapshot, (map) ->
- assertThat(map, provesThatAbsent(PK1, PK2)));
- }
-
- @Test
- void getMultiProof_SingletonMapDoesNotContainSeveralKeys() {
- runTestWithView(database::createFork, (map) -> {
- map.put(PK1, V1);
-
- assertThat(map, provesThatAbsent(PK2, PK3));
- });
- }
-
- @Test
- void getMultiProof_SingletonMapBothContainsAndDoesNot() {
- runTestWithView(database::createFork, (map) -> {
- ImmutableMap source = ImmutableMap.of(
- PK1, V1
- );
-
- map.putAll(source);
-
- assertThat(map, provesThatCorrect(presentEntry(PK1, V1), absentEntry(PK2)));
- });
- }
-
- @Test
- void getMultiProof_TwoElementMapContains() {
- runTestWithView(database::createFork, (map) -> {
- ImmutableMap source = ImmutableMap.of(
- PK1, V1,
- PK2, V2
- );
-
- map.putAll(source);
-
- assertThat(map, provesThatCorrect(presentEntry(PK1, V1), presentEntry(PK2, V2)));
- });
- }
-
- @Test
- void getMultiProof_FourEntryMap_LastByte_Contains1() {
- runTestWithView(database::createFork, (map) -> {
-
- Stream proofKeys = Stream.of(
- (byte) 0b0000_0000,
- (byte) 0b0000_0001,
- (byte) 0b1000_0000,
- (byte) 0b1000_0001
- ).map(ProofMapIndexProxyIntegrationTest::createProofKey);
-
- List> entries = createMapEntries(proofKeys);
-
- putAll(map, entries);
-
- assertThat(map, provesThatPresent(entries));
- });
- }
-
- @Test
- void getMultiProof_FourEntryMap_LastByte_Contains2() {
- runTestWithView(database::createFork, (map) -> {
- Stream proofKeys = Stream.of(
- (byte) 0b00,
- (byte) 0b01,
- (byte) 0b10,
- (byte) 0b11
- ).map(ProofMapIndexProxyIntegrationTest::createProofKey);
-
- List> entries = createMapEntries(proofKeys);
-
- putAll(map, entries);
-
- assertThat(map, provesThatPresent(entries));
- });
- }
-
- @Test
- void getMultiProof_FourEntryMap_FirstByte_Contains() {
- runTestWithView(database::createFork, (map) -> {
- byte[] key1 = createRawProofKey();
- byte[] key2 = createRawProofKey();
- key2[0] = (byte) 0b0000_0001;
- byte[] key3 = createRawProofKey();
- key3[0] = (byte) 0b1000_0000;
- byte[] key4 = createRawProofKey();
- key4[0] = (byte) 0b1000_0001;
-
- List> entries = createMapEntries(
- Stream.of(key1, key2, key3, key4)
- .map(HashCode::fromBytes)
- );
-
- putAll(map, entries);
-
- assertThat(map, provesThatPresent(entries));
- });
- }
-
- @Test
- void getMultiProof_FourEntryMap_FirstAndLastByte_Contains() {
- runTestWithView(database::createFork, (map) -> {
- byte[] key1 = createRawProofKey(); // 000…0
- byte[] key2 = createRawProofKey(); // 100…0
- key2[0] = (byte) 0x01;
- byte[] key3 = createRawProofKey((byte) 0x80); // 000…01
- byte[] key4 = createRawProofKey((byte) 0x80); // 100…01
- key4[0] = (byte) 0x01;
-
- List> entries = createMapEntries(
- Stream.of(key1, key2, key3, key4)
- .map(HashCode::fromBytes)
- );
-
- putAll(map, entries);
-
- assertThat(map, provesThatPresent(entries));
- });
- }
-
- @Test
- void getMultiProof_SortedMultiEntryMapContains() {
- runTestWithView(database::createFork, (map) -> {
- List> entries = createSortedMapEntries();
- putAll(map, entries);
-
- assertThat(map, provesThatPresent(entries));
- });
- }
-
- @Test
- void getMultiProof_FourEntryMap_DoesNotContain() {
- runTestWithView(database::createFork, (map) -> {
- /*
- This map will have the following structure:
- <00xxxx>
- / \
- <00|00xx> <00|10xx>
- / \ / \
- <0000|01> <0000|11> <0010|00> <0010|10>
- */
- List> entries = createMapEntries(
- Stream.of(
- proofKeyFromPrefix("0000|01"),
- proofKeyFromPrefix("0000|11"),
- proofKeyFromPrefix("0010|00"),
- proofKeyFromPrefix("0010|10")
- )
- );
-
- putAll(map, entries);
-
- List proofKeys = Arrays.asList(
- // Should be rejected on root level
- proofKeyFromPrefix("01|0000"),
- // Should be rejected on intermediate level
- proofKeyFromPrefix("00|01"),
- proofKeyFromPrefix("00|11"),
- // Should be rejected on leaf level
- proofKeyFromPrefix("0000|00"),
- proofKeyFromPrefix("0000|10"),
- proofKeyFromPrefix("0010|01"),
- proofKeyFromPrefix("0010|11")
- );
-
- assertThat(map, provesThatAbsent(proofKeys));
- });
- }
-
- @Test
- void remove() {
- runTestWithView(database::createFork, (map) -> {
- map.put(PK1, V1);
- map.remove(PK1);
-
- assertNull(map.get(PK1));
- assertFalse(map.containsKey(PK1));
- });
- }
-
- @Test
- void removeFailsIfSnapshot() {
- runTestWithView(database::createSnapshot,
- (map) -> assertThrows(UnsupportedOperationException.class, () -> map.remove(PK1)));
- }
-
- @Test
- void removeFailsIfInvalidKey() {
- runTestWithView(database::createFork,
- (map) -> assertThrows(IllegalArgumentException.class, () -> map.remove(INVALID_PROOF_KEY)));
- }
-
- @Test
- void keysTest() {
- runTestWithView(database::createFork, (map) -> {
- List> entries = createSortedMapEntries();
-
- putAll(map, entries);
-
- Iterator keysIterator = map.keys();
- List keysFromIter = ImmutableList.copyOf(keysIterator);
- List keysInMap = MapEntries.extractKeys(entries);
-
- // Keys must appear in a lexicographical order.
- assertThat(keysFromIter, equalTo(keysInMap));
- });
- }
+ .collect(toList());
- @Test
- void valuesTest() {
- runTestWithView(database::createFork, (map) -> {
- List> entries = createSortedMapEntries();
-
- putAll(map, entries);
-
- Iterator valuesIterator = map.values();
- List valuesFromIter = ImmutableList.copyOf(valuesIterator);
- List valuesInMap = MapEntries.extractValues(entries);
-
- // Values must appear in a lexicographical order of keys.
- assertThat(valuesFromIter, equalTo(valuesInMap));
- });
- }
-
- @Test
- void entriesTest() {
- runTestWithView(database::createFork, (map) -> {
- List> entries = createSortedMapEntries();
-
- putAll(map, entries);
-
- Iterator> entriesIterator = map.entries();
- List entriesFromIter = ImmutableList.copyOf(entriesIterator);
- // Entries must appear in a lexicographical order of keys.
- assertThat(entriesFromIter, equalTo(entries));
- });
- }
-
- @Test
- void clearEmptyHasNoEffect() {
- runTestWithView(database::createFork, ProofMapIndexProxy::clear);
- }
-
- @Test
- void clearNonEmptyRemovesAllValues() {
- runTestWithView(database::createFork, (map) -> {
- List> entries = createSortedMapEntries();
-
- putAll(map, entries);
-
- map.clear();
-
- for (MapEntry e : entries) {
- assertFalse(map.containsKey(e.getKey()));
- }
- });
- }
-
- @Test
- void clearFailsIfSnapshot() {
- runTestWithView(database::createSnapshot, (map) -> {
- assertThrows(UnsupportedOperationException.class, () -> map.clear());
- });
- }
-
- @Test
- void isEmptyShouldReturnTrueForEmptyMap() {
- runTestWithView(database::createSnapshot, (map) -> assertTrue(map.isEmpty()));
- }
-
- @Test
- void isEmptyShouldReturnFalseForNonEmptyMap() {
- runTestWithView(database::createFork, (map) -> {
- map.put(PK1, V1);
-
- assertFalse(map.isEmpty());
- });
- }
-
- @Test
- void getProofFromSingleKey() {
- runTestWithView(database::createFork, (map) -> {
- map.put(PK1, V1);
-
- UncheckedMapProof proof = map.getProof(PK1);
- CheckedMapProof checkedProof = proof.check();
-
- assertThat(checkedProof, isValid(singletonList(presentEntry(PK1, V1))));
- });
- }
-
- @Test
- void getProofFromVarargs() {
- runTestWithView(database::createFork, (map) -> {
- map.put(PK1, V1);
- map.put(PK2, V2);
-
- UncheckedMapProof proof = map.getProof(PK1, PK2);
- CheckedMapProof checkedProof = proof.check();
-
- assertThat(
- checkedProof, isValid(Arrays.asList(presentEntry(PK1, V1), presentEntry(PK2, V2))));
- });
- }
-
- @Test
- void getProofFromEmptyCollection() {
- runTestWithView(database::createFork, (map) -> {
- map.put(PK1, V1);
-
- IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
- UncheckedMapProof proof = map.getProof(Collections.emptyList());
- });
- assertThat(thrown.getLocalizedMessage(),
- containsString("Keys collection should not be empty"));
- });
- }
-
- @Test
- void getProofFromCollection() {
- runTestWithView(database::createFork, (map) -> {
- map.put(PK1, V1);
-
- UncheckedMapProof proof = map.getProof(singletonList(PK1));
- CheckedMapProof checkedProof = proof.check();
-
- assertThat(checkedProof, isValid(singletonList(presentEntry(PK1, V1))));
- });
- }
-
- /**
- * Returns a new key with the given prefix.
- *
- * @param prefix a key prefix — from the least significant bit to the most significant,
- * i.e., "00 01" is 8, "10 00" is 1.
- * May contain spaces, underscores or bars (e.g., "00 01|01 11" and "11_10"
- * are valid strings).
- */
- private static HashCode proofKeyFromPrefix(String prefix) {
- prefix = filterBitPrefix(prefix);
- byte[] key = keyFromString(prefix);
- return HashCode.fromBytes(key);
- }
-
- /**
- * Replaces spaces that may be used to separate groups of binary digits.
- */
- private static String filterBitPrefix(String prefix) {
- String filtered = prefix.replaceAll("[ _|]", "");
- // Check that the string is correct
- assert filtered.matches("[01]*");
- assert filtered.length() <= PROOF_MAP_KEY_SIZE;
- return filtered;
- }
-
- /**
- * Creates a 32-byte key from the bit prefix.
- */
- private static byte[] keyFromString(String prefix) {
- BitSet keyPrefixBits = new BitSet(prefix.length());
- for (int i = 0; i < prefix.length(); i++) {
- char bit = prefix.charAt(i);
- if (bit == '1') {
- keyPrefixBits.set(i);
- }
- }
- return createPrefixed(keyPrefixBits.toByteArray(), PROOF_MAP_KEY_SIZE);
- }
-
- /**
- * Create a proof key of length 32 with the specified suffix.
- *
- * @param suffix a key suffix. Must be shorter than or equal to 32 bytes in UTF-8.
- * @return a key, starting with zeroes and followed by the specified suffix encoded in UTF-8
- */
- private static HashCode createProofKey(String suffix) {
- byte[] suffixBytes = bytes(suffix);
- return createProofKey(suffixBytes);
- }
-
- private static HashCode createProofKey(byte... suffixBytes) {
- byte[] proofKey = createRawProofKey(suffixBytes);
- return HashCode.fromBytes(proofKey);
- }
-
- private static byte[] createRawProofKey(byte... suffixBytes) {
- checkArgument(suffixBytes.length <= PROOF_MAP_KEY_SIZE);
- byte[] proofKey = new byte[PROOF_MAP_KEY_SIZE];
- System.arraycopy(suffixBytes, 0, proofKey, PROOF_MAP_KEY_SIZE - suffixBytes.length,
- suffixBytes.length);
- return checkProofKey(proofKey);
- }
-
- private static void runTestWithView(Function viewFactory,
- Consumer> mapTest) {
- runTestWithView(viewFactory, (ignoredView, map) -> mapTest.accept(map));
- }
-
- private static void runTestWithView(
- Function viewFactory,
- BiConsumer> mapTest) {
- try (Cleaner cleaner = new Cleaner()) {
- View view = viewFactory.apply(cleaner);
- ProofMapIndexProxy map = createProofMap(MAP_NAME, view);
-
- mapTest.accept(view, map);
- } catch (CloseFailuresException e) {
- throw new AssertionError("Unexpected exception", e);
- }
+ @Override
+ List getTestKeys() {
+ return TEST_KEYS;
}
@Override
ProofMapIndexProxy create(String name, View view) {
- return createProofMap(name, view);
+ return ProofMapIndexProxy.newInstance(name, view, StandardSerializers.hash(),
+ StandardSerializers.string());
}
@Override
@@ -814,97 +66,4 @@ ProofMapIndexProxy createInGroup(String groupName, byte[] idIn
return ProofMapIndexProxy.newInGroupUnsafe(groupName, idInGroup, view,
StandardSerializers.hash(), StandardSerializers.string());
}
-
- @Override
- StorageIndex createOfOtherType(String name, View view) {
- return ListIndexProxy.newInstance(name, view, StandardSerializers.string());
- }
-
- @Override
- Object getAnyElement(ProofMapIndexProxy index) {
- return index.get(PK1);
- }
-
- @Override
- void update(ProofMapIndexProxy index) {
- index.put(PK1, V1);
- }
-
- private static ProofMapIndexProxy createProofMap(String name, View view) {
- return ProofMapIndexProxy.newInstance(name, view, StandardSerializers.hash(),
- StandardSerializers.string());
- }
-
- /**
- * Creates `numOfEntries` map entries, sorted by key:
- * [(00…0PK1, V1), (00…0PK2, V2), … (00…0PKi, Vi)].
- */
- private List> createSortedMapEntries() {
- // Use PROOF_KEYS which are already sorted.
- return createMapEntries(PROOF_KEYS.stream());
- }
-
- /**
- * Creates map entries with the given keys. Uses values
- * from {@linkplain TestStorageItems#values} in a round-robin fashion.
- */
- private List> createMapEntries(Stream proofKeys) {
- Stream keys = proofKeys.distinct();
- Stream roundRobinValues = IntStream.range(0, Integer.MAX_VALUE)
- .mapToObj(i -> values.get(i % values.size()));
- return Streams.zip(keys, roundRobinValues, MapEntry::valueOf)
- .collect(Collectors.toList());
- }
-
- /**
- * Creates 257 entries for a ProofMap that, when added to it, will make the underlying
- * Merkle-Patricia tree of the maximum height (256). Leaf nodes will be at depths
- * ranging from 1 to 256.
- *
- * Bits of 32-byte keys:
- * 00…0000
- * 100…000
- * 0100…00
- * 00100…0
- * …
- * 00…0100
- * 00…0010
- * 00…0001.
- *
- * When all the keys above are added to the ProofMap, the underlying Merkle-Patricia tree
- * has the following structure (only key bits in leaf nodes are shown; the intermediate
- * nodes are shown as 'o' character):
- *
- * o — the root node
- * / \
- * o 100…000 — a leaf node
- * / \
- * o 0100…00
- * / \
- * o 00100…0
- * / \
- * … 00010…0
- * /
- * o — an intermediate node with key prefix 00…0 of size 255 bits.
- * / \
- * 00…0000 00…0001 — leaf nodes at depth 256 with a common prefix of 255 bits.
- */
- private static List> createEntriesForRightLeaningMpt() {
- int numKeyBits = Byte.SIZE * PROOF_MAP_KEY_SIZE;
- BitSet keyBits = new BitSet(numKeyBits);
- int numEntries = numKeyBits + 1;
- List> entries = new ArrayList<>(numEntries);
- entries.add(MapEntry.valueOf(HashCode.fromBytes(new byte[PROOF_MAP_KEY_SIZE]), V1));
-
- for (int i = 0; i < numKeyBits; i++) {
- keyBits.set(i);
- byte[] key = createPrefixed(keyBits.toByteArray(), PROOF_MAP_KEY_SIZE);
- String value = values.get(i % values.size());
- entries.add(MapEntry.valueOf(HashCode.fromBytes(key), value));
- keyBits.clear(i);
- assert keyBits.length() == 0;
- }
-
- return entries;
- }
}
diff --git a/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/ProofMapIndexProxyNoKeyHashingIntegrationTest.java b/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/ProofMapIndexProxyNoKeyHashingIntegrationTest.java
new file mode 100644
index 0000000000..e211f23c78
--- /dev/null
+++ b/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/ProofMapIndexProxyNoKeyHashingIntegrationTest.java
@@ -0,0 +1,481 @@
+/*
+ * Copyright 2019 The Exonum Team
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.exonum.binding.core.storage.indices;
+
+import static com.exonum.binding.core.storage.indices.MapEntries.putAll;
+import static com.exonum.binding.core.storage.indices.ProofMapContainsMatcher.provesThatAbsent;
+import static com.exonum.binding.core.storage.indices.ProofMapContainsMatcher.provesThatPresent;
+import static com.exonum.binding.core.storage.indices.StoragePreconditions.PROOF_MAP_KEY_SIZE;
+import static com.exonum.binding.core.storage.indices.TestStorageItems.V1;
+import static com.exonum.binding.core.storage.indices.TestStorageItems.values;
+import static com.exonum.binding.test.Bytes.createPrefixed;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.exonum.binding.common.collect.MapEntry;
+import com.exonum.binding.common.hash.HashCode;
+import com.exonum.binding.common.serialization.StandardSerializers;
+import com.exonum.binding.core.storage.database.View;
+import com.exonum.binding.test.Bytes;
+import com.exonum.binding.test.CiOnly;
+import com.google.common.collect.ImmutableList;
+import com.google.common.primitives.UnsignedBytes;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Test;
+
+class ProofMapIndexProxyNoKeyHashingIntegrationTest
+ extends BaseProofMapIndexProxyIntegrationTestable {
+
+ static final List SORTED_TEST_KEYS = Stream.of(
+ Bytes.bytes(0x00),
+ Bytes.bytes(0x01),
+ Bytes.bytes(0x02),
+ Bytes.bytes(0x08),
+ Bytes.bytes(0x0f),
+ Bytes.bytes(0x10),
+ Bytes.bytes(0x20),
+ Bytes.bytes(0x80),
+ Bytes.bytes(0xf0),
+ Bytes.bytes(0xff),
+ Bytes.bytes(0x01, 0x01),
+ Bytes.bytes(0x01, 0x10),
+ Bytes.bytes(0x10, 0x01),
+ Bytes.bytes(0x10, 0x10)
+ )
+ .map(BaseProofMapIndexProxyIntegrationTestable::createRawProofKey)
+ .sorted(UnsignedBytes.lexicographicalComparator())
+ .map(HashCode::fromBytes)
+ .collect(Collectors.toList());
+
+ private static final HashCode INVALID_PROOF_KEY = HashCode.fromString("1234");
+
+ @Override
+ List getTestKeys() {
+ return SORTED_TEST_KEYS;
+ }
+
+ @Override
+ ProofMapIndexProxy create(String name, View view) {
+ return createProofMap(name, view);
+ }
+
+ @Override
+ ProofMapIndexProxy createInGroup(String groupName, byte[] idInGroup,
+ View view) {
+ return ProofMapIndexProxy.newInGroupUnsafeNoKeyHashing(groupName, idInGroup, view,
+ StandardSerializers.hash(), StandardSerializers.string());
+ }
+
+ private static ProofMapIndexProxy createProofMap(String name, View view) {
+ return ProofMapIndexProxy.newInstanceNoKeyHashing(name, view, StandardSerializers.hash(),
+ StandardSerializers.string());
+ }
+
+ @Test
+ void containsThrowsIfInvalidKey() {
+ runTestWithView(database::createSnapshot, (map) -> assertThrows(IllegalArgumentException.class,
+ () -> map.containsKey(INVALID_PROOF_KEY)));
+ }
+
+ @Test
+ void putFailsIfInvalidKey() {
+ runTestWithView(database::createFork, (map) -> assertThrows(IllegalArgumentException.class,
+ () -> map.put(INVALID_PROOF_KEY, V1)));
+ }
+
+ @Test
+ void removeFailsIfInvalidKey() {
+ runTestWithView(database::createFork,
+ (map) -> assertThrows(IllegalArgumentException.class, () -> map.remove(INVALID_PROOF_KEY)));
+ }
+
+ @Test
+ void keysTest() {
+ runTestWithView(database::createFork, (map) -> {
+ List> entries = createSortedMapEntries();
+
+ putAll(map, entries);
+
+ Iterator keysIterator = map.keys();
+ List keysFromIter = ImmutableList.copyOf(keysIterator);
+ List keysInMap = MapEntries.extractKeys(entries);
+
+ // Keys must appear in a lexicographical order.
+ assertThat(keysFromIter, equalTo(keysInMap));
+ });
+ }
+
+ @Test
+ void valuesTest() {
+ runTestWithView(database::createFork, (map) -> {
+ List> entries = createSortedMapEntries();
+
+ putAll(map, entries);
+
+ Iterator valuesIterator = map.values();
+ List valuesFromIter = ImmutableList.copyOf(valuesIterator);
+ List valuesInMap = MapEntries.extractValues(entries);
+
+ // Values must appear in a lexicographical order of keys.
+ assertThat(valuesFromIter, equalTo(valuesInMap));
+ });
+ }
+
+ @Test
+ void entriesTest() {
+ runTestWithView(database::createFork, (map) -> {
+ List> entries = createSortedMapEntries();
+
+ putAll(map, entries);
+
+ Iterator> entriesIterator = map.entries();
+ List entriesFromIter = ImmutableList.copyOf(entriesIterator);
+ // Entries must appear in a lexicographical order of keys.
+ assertThat(entriesFromIter, equalTo(entries));
+ });
+ }
+
+ @Test
+ void getProof_FourEntryMap_LastByte_Contains1() {
+ runTestWithView(database::createFork, (map) -> {
+
+ Stream proofKeys = Stream.of(
+ (byte) 0b0000_0000,
+ (byte) 0b0000_0001,
+ (byte) 0b1000_0000,
+ (byte) 0b1000_0001
+ ).map(BaseProofMapIndexProxyIntegrationTestable::createProofKey);
+
+ List> entries = createMapEntries(proofKeys);
+
+ putAll(map, entries);
+
+ for (MapEntry e : entries) {
+ assertThat(map, provesThatPresent(e.getKey(), e.getValue()));
+ }
+ });
+ }
+
+ @Test
+ void getProof_FourEntryMap_LastByte_Contains2() {
+ runTestWithView(database::createFork, (map) -> {
+ Stream proofKeys = Stream.of(
+ (byte) 0b00,
+ (byte) 0b01,
+ (byte) 0b10,
+ (byte) 0b11
+ ).map(BaseProofMapIndexProxyIntegrationTestable::createProofKey);
+
+ List> entries = createMapEntries(proofKeys);
+
+ putAll(map, entries);
+
+ for (MapEntry e : entries) {
+ assertThat(map, provesThatPresent(e.getKey(), e.getValue()));
+ }
+ });
+ }
+
+ @Test
+ void getProof_FourEntryMap_FirstByte_Contains() {
+ runTestWithView(database::createFork, (map) -> {
+ byte[] key1 = createRawProofKey();
+ byte[] key2 = createRawProofKey();
+ key2[0] = (byte) 0b0000_0001;
+ byte[] key3 = createRawProofKey();
+ key3[0] = (byte) 0b1000_0000;
+ byte[] key4 = createRawProofKey();
+ key4[0] = (byte) 0b1000_0001;
+
+ List> entries = createMapEntries(
+ Stream.of(key1, key2, key3, key4)
+ .map(HashCode::fromBytes)
+ );
+
+ putAll(map, entries);
+
+ for (MapEntry e : entries) {
+ assertThat(map, provesThatPresent(e.getKey(), e.getValue()));
+ }
+ });
+ }
+
+ @Test
+ void getProof_FourEntryMap_FirstAndLastByte_Contains() {
+ runTestWithView(database::createFork, (map) -> {
+ byte[] key1 = createRawProofKey(); // 000…0
+ byte[] key2 = createRawProofKey(); // 100…0
+ key2[0] = (byte) 0x01;
+ byte[] key3 = createRawProofKey((byte) 0x80); // 000…01
+ byte[] key4 = createRawProofKey((byte) 0x80); // 100…01
+ key4[0] = (byte) 0x01;
+
+ List> entries = createMapEntries(
+ Stream.of(key1, key2, key3, key4)
+ .map(HashCode::fromBytes)
+ );
+
+ putAll(map, entries);
+
+ for (MapEntry e : entries) {
+ assertThat(map, provesThatPresent(e.getKey(), e.getValue()));
+ }
+ });
+ }
+
+ @Test
+ void getMultiProof_FourEntryMap_LastByte_Contains1() {
+ runTestWithView(database::createFork, (map) -> {
+
+ Stream proofKeys = Stream.of(
+ (byte) 0b0000_0000,
+ (byte) 0b0000_0001,
+ (byte) 0b1000_0000,
+ (byte) 0b1000_0001
+ ).map(BaseProofMapIndexProxyIntegrationTestable::createProofKey);
+
+ List> entries = createMapEntries(proofKeys);
+
+ putAll(map, entries);
+
+ assertThat(map, provesThatPresent(entries));
+ });
+ }
+
+ @Test
+ void getMultiProof_FourEntryMap_LastByte_Contains2() {
+ runTestWithView(database::createFork, (map) -> {
+ Stream proofKeys = Stream.of(
+ (byte) 0b00,
+ (byte) 0b01,
+ (byte) 0b10,
+ (byte) 0b11
+ ).map(BaseProofMapIndexProxyIntegrationTestable::createProofKey);
+
+ List> entries = createMapEntries(proofKeys);
+
+ putAll(map, entries);
+
+ assertThat(map, provesThatPresent(entries));
+ });
+ }
+
+ @Test
+ void getMultiProof_FourEntryMap_FirstByte_Contains() {
+ runTestWithView(database::createFork, (map) -> {
+ byte[] key1 = createRawProofKey();
+ byte[] key2 = createRawProofKey();
+ key2[0] = (byte) 0b0000_0001;
+ byte[] key3 = createRawProofKey();
+ key3[0] = (byte) 0b1000_0000;
+ byte[] key4 = createRawProofKey();
+ key4[0] = (byte) 0b1000_0001;
+
+ List> entries = createMapEntries(
+ Stream.of(key1, key2, key3, key4)
+ .map(HashCode::fromBytes)
+ );
+
+ putAll(map, entries);
+
+ assertThat(map, provesThatPresent(entries));
+ });
+ }
+
+ @Test
+ void getMultiProof_FourEntryMap_FirstAndLastByte_Contains() {
+ runTestWithView(database::createFork, (map) -> {
+ byte[] key1 = createRawProofKey(); // 000…0
+ byte[] key2 = createRawProofKey(); // 100…0
+ key2[0] = (byte) 0x01;
+ byte[] key3 = createRawProofKey((byte) 0x80); // 000…01
+ byte[] key4 = createRawProofKey((byte) 0x80); // 100…01
+ key4[0] = (byte) 0x01;
+
+ List> entries = createMapEntries(
+ Stream.of(key1, key2, key3, key4)
+ .map(HashCode::fromBytes)
+ );
+
+ putAll(map, entries);
+
+ assertThat(map, provesThatPresent(entries));
+ });
+ }
+
+ @Test
+ void getMultiProof_FourEntryMap_DoesNotContain() {
+ runTestWithView(database::createFork, (map) -> {
+ /*
+ This map will have the following structure:
+ <00xxxx>
+ / \
+ <00|00xx> <00|10xx>
+ / \ / \
+ <0000|01> <0000|11> <0010|00> <0010|10>
+ */
+ List> entries = createMapEntries(
+ Stream.of(
+ proofKeyFromPrefix("0000|01"),
+ proofKeyFromPrefix("0000|11"),
+ proofKeyFromPrefix("0010|00"),
+ proofKeyFromPrefix("0010|10")
+ )
+ );
+
+ putAll(map, entries);
+
+ List proofKeys = Arrays.asList(
+ // Should be rejected on root level
+ proofKeyFromPrefix("01|0000"),
+ // Should be rejected on intermediate level
+ proofKeyFromPrefix("00|01"),
+ proofKeyFromPrefix("00|11"),
+ // Should be rejected on leaf level
+ proofKeyFromPrefix("0000|00"),
+ proofKeyFromPrefix("0000|10"),
+ proofKeyFromPrefix("0010|01"),
+ proofKeyFromPrefix("0010|11")
+ );
+
+ assertThat(map, provesThatAbsent(proofKeys));
+ });
+ }
+
+ @CiOnly
+ @Test
+ /*
+ Takes quite a lot of time (validating 257 proofs), but it's an integration test, isn't it? :-)
+ Consider adding a similar test for left-leaning MPT
+ */
+ void getProof_MapContainsRightLeaningMaxHeightMpt() {
+ runTestWithView(database::createFork, (map) -> {
+ List> entries = createEntriesForRightLeaningMpt();
+ putAll(map, entries);
+
+ for (MapEntry e : entries) {
+ assertThat(map, provesThatPresent(e.getKey(), e.getValue()));
+ }
+ });
+ }
+
+ /**
+ * Returns a new key with the given prefix.
+ *
+ * @param prefix a key prefix — from the least significant bit to the most significant,
+ * i.e., "00 01" is 8, "10 00" is 1.
+ * May contain spaces, underscores or bars (e.g., "00 01|01 11" and "11_10"
+ * are valid strings).
+ */
+ private static HashCode proofKeyFromPrefix(String prefix) {
+ prefix = filterBitPrefix(prefix);
+ byte[] key = keyFromString(prefix);
+ return HashCode.fromBytes(key);
+ }
+
+ /**
+ * Replaces spaces that may be used to separate groups of binary digits.
+ */
+ private static String filterBitPrefix(String prefix) {
+ String filtered = prefix.replaceAll("[ _|]", "");
+ // Check that the string is correct
+ assert filtered.matches("[01]*");
+ assert filtered.length() <= PROOF_MAP_KEY_SIZE;
+ return filtered;
+ }
+
+ /**
+ * Creates a 32-byte key from the bit prefix.
+ */
+ private static byte[] keyFromString(String prefix) {
+ BitSet keyPrefixBits = new BitSet(prefix.length());
+ for (int i = 0; i < prefix.length(); i++) {
+ char bit = prefix.charAt(i);
+ if (bit == '1') {
+ keyPrefixBits.set(i);
+ }
+ }
+ return createPrefixed(keyPrefixBits.toByteArray(), PROOF_MAP_KEY_SIZE);
+ }
+
+ /**
+ * Creates `numOfEntries` map entries, sorted by key:
+ * [(00…0PK1, V1), (00…0PK2, V2), … (00…0PKi, Vi)].
+ */
+ List> createSortedMapEntries() {
+ return createMapEntries(SORTED_TEST_KEYS.stream());
+ }
+
+ /**
+ * Creates 257 entries for a ProofMap that, when added to it, will make the underlying
+ * Merkle-Patricia tree of the maximum height (256). Leaf nodes will be at depths
+ * ranging from 1 to 256.
+ *
+ * Bits of 32-byte keys:
+ * 00…0000
+ * 100…000
+ * 0100…00
+ * 00100…0
+ * …
+ * 00…0100
+ * 00…0010
+ * 00…0001.
+ *
+ * When all the keys above are added to the ProofMap, the underlying Merkle-Patricia tree
+ * has the following structure (only key bits in leaf nodes are shown; the intermediate
+ * nodes are shown as 'o' character):
+ *
+ * o — the root node
+ * / \
+ * o 100…000 — a leaf node
+ * / \
+ * o 0100…00
+ * / \
+ * o 00100…0
+ * / \
+ * … 00010…0
+ * /
+ * o — an intermediate node with key prefix 00…0 of size 255 bits.
+ * / \
+ * 00…0000 00…0001 — leaf nodes at depth 256 with a common prefix of 255 bits.
+ */
+ private static List> createEntriesForRightLeaningMpt() {
+ int numKeyBits = Byte.SIZE * PROOF_MAP_KEY_SIZE;
+ BitSet keyBits = new BitSet(numKeyBits);
+ int numEntries = numKeyBits + 1;
+ List> entries = new ArrayList<>(numEntries);
+ entries.add(MapEntry.valueOf(HashCode.fromBytes(new byte[PROOF_MAP_KEY_SIZE]), V1));
+
+ for (int i = 0; i < numKeyBits; i++) {
+ keyBits.set(i);
+ byte[] key = createPrefixed(keyBits.toByteArray(), PROOF_MAP_KEY_SIZE);
+ String value = values.get(i % values.size());
+ entries.add(MapEntry.valueOf(HashCode.fromBytes(key), value));
+ keyBits.clear(i);
+ assert keyBits.length() == 0;
+ }
+
+ return entries;
+ }
+}
diff --git a/exonum-java-binding/cryptocurrency-demo/src/main/java/com/exonum/binding/cryptocurrency/CryptocurrencySchema.java b/exonum-java-binding/cryptocurrency-demo/src/main/java/com/exonum/binding/cryptocurrency/CryptocurrencySchema.java
index 304adcbaa6..1791e691a7 100644
--- a/exonum-java-binding/cryptocurrency-demo/src/main/java/com/exonum/binding/cryptocurrency/CryptocurrencySchema.java
+++ b/exonum-java-binding/cryptocurrency-demo/src/main/java/com/exonum/binding/cryptocurrency/CryptocurrencySchema.java
@@ -49,11 +49,12 @@ public List getStateHashes() {
}
/**
- * Returns a proof map of wallets.
+ * Returns a proof map of wallets. Note that this is a
+ * proof map that uses non-hashed keys.
*/
public ProofMapIndexProxy wallets() {
String name = fullIndexName("wallets");
- return ProofMapIndexProxy.newInstance(name, view, StandardSerializers.publicKey(),
+ return ProofMapIndexProxy.newInstanceNoKeyHashing(name, view, StandardSerializers.publicKey(),
WalletSerializer.INSTANCE);
}
diff --git a/exonum-java-binding/fakes/src/main/java/com/exonum/binding/fakes/services/service/TestSchema.java b/exonum-java-binding/fakes/src/main/java/com/exonum/binding/fakes/services/service/TestSchema.java
index 1409eb5cf2..c8aedb582f 100644
--- a/exonum-java-binding/fakes/src/main/java/com/exonum/binding/fakes/services/service/TestSchema.java
+++ b/exonum-java-binding/fakes/src/main/java/com/exonum/binding/fakes/services/service/TestSchema.java
@@ -35,7 +35,7 @@ public TestSchema(View view) {
}
public ProofMapIndexProxy testMap() {
- return ProofMapIndexProxy.newInstance(TEST_MAP_NAME, view, StandardSerializers.hash(),
+ return ProofMapIndexProxy.newInstanceNoKeyHashing(TEST_MAP_NAME, view, StandardSerializers.hash(),
StandardSerializers.string());
}
diff --git a/exonum-java-binding/qa-service/src/main/java/com/exonum/binding/qaservice/QaSchema.java b/exonum-java-binding/qa-service/src/main/java/com/exonum/binding/qaservice/QaSchema.java
index 028d41ffd3..8704c68820 100644
--- a/exonum-java-binding/qa-service/src/main/java/com/exonum/binding/qaservice/QaSchema.java
+++ b/exonum-java-binding/qa-service/src/main/java/com/exonum/binding/qaservice/QaSchema.java
@@ -70,11 +70,12 @@ public TimeSchema timeSchema() {
}
/**
- * Returns a proof map of counter values.
+ * Returns a proof map of counter values. Note that this is a
+ * proof map that uses non-hashed keys.
*/
public ProofMapIndexProxy counters() {
String name = fullIndexName("counters");
- return ProofMapIndexProxy.newInstance(name, view, StandardSerializers.hash(),
+ return ProofMapIndexProxy.newInstanceNoKeyHashing(name, view, StandardSerializers.hash(),
StandardSerializers.uint64());
}
diff --git a/exonum-java-binding/time-oracle/src/main/java/com/exonum/binding/time/TimeSchema.java b/exonum-java-binding/time-oracle/src/main/java/com/exonum/binding/time/TimeSchema.java
index 3d6582fbae..521080f43e 100644
--- a/exonum-java-binding/time-oracle/src/main/java/com/exonum/binding/time/TimeSchema.java
+++ b/exonum-java-binding/time-oracle/src/main/java/com/exonum/binding/time/TimeSchema.java
@@ -53,7 +53,8 @@ static TimeSchema newInstance(View dbView, String name) {
EntryIndexProxy getTime();
/**
- * Returns the table that stores time for every validator.
+ * Returns the table that stores time for every validator. Note that this is a
+ * proof map that uses non-hashed keys.
*/
ProofMapIndexProxy getValidatorsTimes();
}
diff --git a/exonum-java-binding/time-oracle/src/main/java/com/exonum/binding/time/TimeSchemaProxy.java b/exonum-java-binding/time-oracle/src/main/java/com/exonum/binding/time/TimeSchemaProxy.java
index 8152ee3834..f7293cc6fe 100644
--- a/exonum-java-binding/time-oracle/src/main/java/com/exonum/binding/time/TimeSchemaProxy.java
+++ b/exonum-java-binding/time-oracle/src/main/java/com/exonum/binding/time/TimeSchemaProxy.java
@@ -73,7 +73,7 @@ public EntryIndexProxy getTime() {
@Override
public ProofMapIndexProxy getValidatorsTimes() {
- return ProofMapIndexProxy.newInstance(indexName(TimeIndex.VALIDATORS_TIMES), view,
+ return ProofMapIndexProxy.newInstanceNoKeyHashing(indexName(TimeIndex.VALIDATORS_TIMES), view,
PUBLIC_KEY_SERIALIZER, ZONED_DATE_TIME_SERIALIZER);
}