diff --git a/exonum-java-binding/core/checkstyle-suppressions.xml b/exonum-java-binding/core/checkstyle-suppressions.xml index d84f1b0f2f..d3928238ec 100644 --- a/exonum-java-binding/core/checkstyle-suppressions.xml +++ b/exonum-java-binding/core/checkstyle-suppressions.xml @@ -15,7 +15,7 @@ - diff --git a/exonum-java-binding/core/rust/src/storage/proof_map_index.rs b/exonum-java-binding/core/rust/src/storage/proof_map_index.rs index 81a4746808..f269bb262a 100644 --- a/exonum-java-binding/core/rust/src/storage/proof_map_index.rs +++ b/exonum-java-binding/core/rust/src/storage/proof_map_index.rs @@ -63,6 +63,8 @@ pub extern "system" fn Java_com_exonum_binding_core_storage_indices_ProofMapInde _: JClass, name: JString, view_handle: Handle, + // TODO: to be used in ECR-3765 + _key_hashing: jboolean, ) -> Handle { let res = panic::catch_unwind(|| { let name = utils::convert_to_string(&env, name)?; @@ -88,6 +90,8 @@ pub extern "system" fn Java_com_exonum_binding_core_storage_indices_ProofMapInde group_name: JString, map_id: jbyteArray, view_handle: Handle, + // TODO: to be used in ECR-3765 + _key_hashing: jboolean, ) -> Handle { let res = panic::catch_unwind(|| { let group_name = utils::convert_to_string(&env, group_name)?; diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/Blockchain.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/Blockchain.java index 107d8f89e8..fc869d62e7 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/Blockchain.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/Blockchain.java @@ -139,7 +139,8 @@ public MapIndex getTxMessages() { } /** - * Returns a map with a key-value pair of a transaction hash and execution result. + * Returns a map with a key-value pair of a transaction hash and execution result. Note that this + * is a proof map that uses non-hashed keys. */ public ProofMapIndexProxy getTxResults() { return schema.getTxResults(); diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/CoreSchemaProxy.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/CoreSchemaProxy.java index f7a131b884..88f5eafc3c 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/CoreSchemaProxy.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/CoreSchemaProxy.java @@ -149,10 +149,11 @@ MapIndex getTxMessages() { } /** - * Returns a map with a key-value pair of a transaction hash and execution result. + * Returns a map with a key-value pair of a transaction hash and execution result. Note that this + * is a proof map that uses non-hashed keys. */ ProofMapIndexProxy getTxResults() { - return ProofMapIndexProxy.newInstance(CoreIndex.TRANSACTIONS_RESULTS, dbView, + return ProofMapIndexProxy.newInstanceNoKeyHashing(CoreIndex.TRANSACTIONS_RESULTS, dbView, StandardSerializers.hash(), EXECUTION_STATUS_SERIALIZER); } diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/MapIndex.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/MapIndex.java index e0406f677a..837881075c 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/MapIndex.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/MapIndex.java @@ -94,22 +94,23 @@ default void putAll(Map sourceMap) { void remove(K key); /** - * Returns an iterator over the map keys in lexicographical order. + * Returns an iterator over the map keys. The keys are ordered in lexicographical order. * * @throws IllegalStateException if this map is not valid */ Iterator keys(); /** - * Returns an iterator over the map values in lexicographical order of keys. + * Returns an iterator over the map values. The values are ordered in lexicographical order of + * keys. * * @throws IllegalStateException if this map is not valid */ Iterator 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. + * + *

Key hashing in proof maps

+ * + *

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 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); }