From e4a3631759eba32fae53386daef280539aeca0f1 Mon Sep 17 00:00:00 2001 From: "Sergei Makarov (LCL)" Date: Tue, 12 Nov 2019 01:02:06 -0500 Subject: [PATCH 1/8] ECR-3779 Add no key hashing option to ProofMapIndexProxy --- .../storage/indices/ProofMapIndexProxy.java | 129 ++- ...roofMapKeyCheckingSerializerDecorator.java | 9 +- ...MapKeySizeCheckingSerializerDecorator.java | 60 ++ ...BaseProofMapIndexProxyIntegrationTest.java | 806 ++++++++++++++++ ...roofMapIndexProxyGroupIntegrationTest.java | 10 +- ...ProxyGroupNoKeyHashingIntegrationTest.java | 51 ++ .../ProofMapIndexProxyIntegrationTest.java | 859 +----------------- ...IndexProxyNoKeyHashingIntegrationTest.java | 160 ++++ 8 files changed, 1195 insertions(+), 889 deletions(-) create mode 100644 exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ProofMapKeySizeCheckingSerializerDecorator.java create mode 100644 exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/BaseProofMapIndexProxyIntegrationTest.java create mode 100644 exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/ProofMapIndexProxyGroupNoKeyHashingIntegrationTest.java create mode 100644 exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/ProofMapIndexProxyNoKeyHashingIntegrationTest.java 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..d0dc81739e 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,15 @@ * 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. + * + *

This map can be instantiated in two ways - either with keys hashing with methods + * {@link #newInstance(String, View, Serializer, Serializer)} and + * {@link #newInGroupUnsafe(String, byte[], View, Serializer, Serializer)} or with no key hashing + * with methods {@link #newInstanceNoKeyHashing(String, View, Serializer, Serializer)} and + * {@link #newInGroupUnsafeNoKeyHashing(String, byte[], View, Serializer, Serializer)}. In case of + * no key hashing keys are required to be 32-byte long. Note that the former option is considered + * a default one. * *

The "destructive" methods of the map, i.e., the one that change the map contents, * are specified to throw {@link UnsupportedOperationException} if @@ -58,7 +65,7 @@ *

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 */ @@ -74,7 +81,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 +93,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 with no key hashing. 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 + * @see ProofMapIndexProxy + */ + 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 +132,35 @@ 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 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 @@ -108,27 +169,26 @@ public static ProofMapIndexProxy newInstance( * @throws IllegalStateException if the view is not valid * @throws IllegalArgumentException if the name or index id is empty * @see StandardSerializers + * @see ProofMapIndexProxy */ - 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 +199,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) { + ProofMapKeyCheckingSerializerDecorator ks = decorateKeySerializer(keySerializer, keyHashing); CheckingSerializerDecorator vs = CheckingSerializerDecorator.from(valueSerializer); NativeHandle mapNativeHandle = createNativeMap(view, nativeMapConstructor); @@ -151,6 +210,17 @@ private static ProofMapIndexProxy newMapIndexProxy(IndexAddress add return map; } + private static ProofMapKeyCheckingSerializerDecorator decorateKeySerializer( + Serializer keySerializer, boolean keyHashing) { + if (!keyHashing) { + ProofMapKeySizeCheckingSerializerDecorator sizeCheckingSerializerDecorator = + ProofMapKeySizeCheckingSerializerDecorator.from(keySerializer); + return ProofMapKeyCheckingSerializerDecorator.from(sizeCheckingSerializerDecorator); + } else { + return ProofMapKeyCheckingSerializerDecorator.from(keySerializer); + } + } + private static NativeHandle createNativeMap(View view, LongSupplier nativeMapConstructor) { NativeHandle mapNativeHandle = new NativeHandle(nativeMapConstructor.getAsLong()); @@ -160,10 +230,10 @@ 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, @@ -184,10 +254,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 + * no key hashing proof map) * @throws UnsupportedOperationException if this map is read-only */ @Override @@ -227,11 +298,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 + * no key hashing proof map) */ public UncheckedMapProof getProof(K key, K... otherKeys) { if (otherKeys.length == 0) { @@ -246,10 +317,10 @@ 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 + * no key hashing proof map) 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/main/java/com/exonum/binding/core/storage/indices/ProofMapKeyCheckingSerializerDecorator.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ProofMapKeyCheckingSerializerDecorator.java index 2d406e1783..64f2ed47a4 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ProofMapKeyCheckingSerializerDecorator.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ProofMapKeyCheckingSerializerDecorator.java @@ -16,15 +16,12 @@ package com.exonum.binding.core.storage.indices; -import static com.exonum.binding.core.storage.indices.StoragePreconditions.checkProofKey; import static com.google.common.base.Preconditions.checkNotNull; import com.exonum.binding.common.serialization.Serializer; /** - * A serializer decorator that checks proof map keys for correctness. - * - * @see StoragePreconditions#checkProofKey(byte[]) + * A serializer decorator that checks proof map keys are not null. */ final class ProofMapKeyCheckingSerializerDecorator implements Serializer { @@ -49,12 +46,12 @@ private ProofMapKeyCheckingSerializerDecorator(Serializer delegate) { @Override public byte[] toBytes(T proofKey) { byte[] dbValue = delegate.toBytes(proofKey); - return checkProofKey(dbValue); + return checkNotNull(dbValue); } @Override public T fromBytes(byte[] serializedProofKey) { - checkProofKey(serializedProofKey); + checkNotNull(serializedProofKey); return delegate.fromBytes(serializedProofKey); } } diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ProofMapKeySizeCheckingSerializerDecorator.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ProofMapKeySizeCheckingSerializerDecorator.java new file mode 100644 index 0000000000..1f5c9acea9 --- /dev/null +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ProofMapKeySizeCheckingSerializerDecorator.java @@ -0,0 +1,60 @@ +/* + * 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.StoragePreconditions.checkProofKey; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.exonum.binding.common.serialization.Serializer; + +/** + * A serializer decorator that checks that proof map keys are 32-byte long. + * + * @see StoragePreconditions#checkProofKey(byte[]) + */ +final class ProofMapKeySizeCheckingSerializerDecorator implements Serializer { + + private final Serializer delegate; + + /** + * Creates a proof map key checking serializer decorator. Will not decorate itself. + * + * @param serializer a serializer to decorate + */ + public static ProofMapKeySizeCheckingSerializerDecorator from(Serializer serializer) { + if (serializer instanceof ProofMapKeySizeCheckingSerializerDecorator) { + return (ProofMapKeySizeCheckingSerializerDecorator) serializer; + } + return new ProofMapKeySizeCheckingSerializerDecorator<>(serializer); + } + + private ProofMapKeySizeCheckingSerializerDecorator(Serializer delegate) { + this.delegate = checkNotNull(delegate); + } + + @Override + public byte[] toBytes(T proofKey) { + byte[] dbValue = delegate.toBytes(proofKey); + return checkProofKey(dbValue); + } + + @Override + public T fromBytes(byte[] serializedProofKey) { + checkProofKey(serializedProofKey); + return delegate.fromBytes(serializedProofKey); + } +} diff --git a/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/BaseProofMapIndexProxyIntegrationTest.java b/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/BaseProofMapIndexProxyIntegrationTest.java new file mode 100644 index 0000000000..3510a72423 --- /dev/null +++ b/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/BaseProofMapIndexProxyIntegrationTest.java @@ -0,0 +1,806 @@ +/* + * 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.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 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.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.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 BaseProofMapIndexProxyIntegrationTest + extends BaseIndexProxyTestable> { + + private static final String MAP_NAME = "test_proof_map"; + + private final HashCode key1 = getProofKeys().get(0); + private final HashCode key2 = getProofKeys().get(1); + private final HashCode key3 = getProofKeys().get(2); + + private static final HashCode EMPTY_MAP_INDEX_HASH = HashCode.fromString( + "7324b5c72b51bb5d4c180f1109cfd347b60473882145841c39f3e584576296f9"); + + abstract List getProofKeys(); + + @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_FourEntryMap_LastByte_Contains1() { + runTestWithView(database::createFork, (map) -> { + + Stream proofKeys = Stream.of( + (byte) 0b0000_0000, + (byte) 0b0000_0001, + (byte) 0b1000_0000, + (byte) 0b1000_0001 + ).map(BaseProofMapIndexProxyIntegrationTest::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(BaseProofMapIndexProxyIntegrationTest::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(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 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(BaseProofMapIndexProxyIntegrationTest::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(BaseProofMapIndexProxyIntegrationTest::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(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 = 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(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)))); + }); + } + + /** + * 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); + } + + 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); + } + + /** + * Creates `numOfEntries` map entries, sorted by key: + * [(00…0PK1, V1), (00…0PK2, V2), … (00…0PKi, Vi)]. + */ + List> createSortedMapEntries() { + // Use PROOF_KEYS which are already sorted. + return createMapEntries(getProofKeys().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/ProofMapIndexProxyGroupIntegrationTest.java b/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/ProofMapIndexProxyGroupIntegrationTest.java index 6cd951e810..67f14b2665 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,9 +16,9 @@ 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; @@ -27,6 +27,10 @@ class ProofMapIndexProxyGroupIntegrationTest extends BaseMapIndexGroupTestable { + private static final HashCode PK1 = HashCode.fromString(K1); + private static final HashCode PK2 = HashCode.fromString(K2); + private static final HashCode PK3 = HashCode.fromString(K3); + private static final String GROUP_NAME = "proof_map_group_IT"; @Override 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..6d860bebaf --- /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.PROOF_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 = PROOF_KEYS.get(0); + private static final HashCode PK2 = PROOF_KEYS.get(1); + private static final HashCode PK3 = PROOF_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..ad908b4c05 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,68 +16,17 @@ 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; class ProofMapIndexProxyIntegrationTest - extends BaseIndexProxyTestable> { - - private static final String MAP_NAME = "test_proof_map"; + extends BaseProofMapIndexProxyIntegrationTest { private static final List PROOF_KEYS = Stream.of( Bytes.bytes(0x00), @@ -95,717 +44,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 getProofKeys() { + return PROOF_KEYS; } @Override ProofMapIndexProxy create(String name, View view) { - return createProofMap(name, view); + return ProofMapIndexProxy.newInstance(name, view, StandardSerializers.hash(), + StandardSerializers.string()); } @Override @@ -814,97 +64,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..8068d777e4 --- /dev/null +++ b/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/ProofMapIndexProxyNoKeyHashingIntegrationTest.java @@ -0,0 +1,160 @@ +/* + * 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.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.google.common.base.Preconditions.checkArgument; +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.google.common.collect.ImmutableList; +import com.google.common.primitives.UnsignedBytes; +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 BaseProofMapIndexProxyIntegrationTest { + + static final List PROOF_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(ProofMapIndexProxyNoKeyHashingIntegrationTest::createRawProofKey) + .sorted(UnsignedBytes.lexicographicalComparator()) + .map(HashCode::fromBytes) + .collect(Collectors.toList()); + + private static final HashCode INVALID_PROOF_KEY = HashCode.fromString("1234"); + + @Override + List getProofKeys() { + return PROOF_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)); + }); + } + + 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); + } +} From 4048d4cd233a05d0b75228bedac77a2b27c29803 Mon Sep 17 00:00:00 2001 From: "Sergei Makarov (LCL)" Date: Wed, 13 Nov 2019 01:18:47 -0500 Subject: [PATCH 2/8] ECR-3779 Review fixes --- .../core/checkstyle-suppressions.xml | 2 +- .../storage/indices/ProofMapIndexProxy.java | 59 +-- ...roofMapKeyCheckingSerializerDecorator.java | 9 +- ...MapKeySizeCheckingSerializerDecorator.java | 60 --- ...roofMapIndexProxyIntegrationTestable.java} | 348 +----------------- ...roofMapIndexProxyGroupIntegrationTest.java | 23 +- .../ProofMapIndexProxyIntegrationTest.java | 2 +- ...IndexProxyNoKeyHashingIntegrationTest.java | 342 ++++++++++++++++- 8 files changed, 399 insertions(+), 446 deletions(-) delete mode 100644 exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ProofMapKeySizeCheckingSerializerDecorator.java rename exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/{BaseProofMapIndexProxyIntegrationTest.java => BaseProofMapIndexProxyIntegrationTestable.java} (58%) diff --git a/exonum-java-binding/core/checkstyle-suppressions.xml b/exonum-java-binding/core/checkstyle-suppressions.xml index d2ed6f86df..460b2dbd44 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/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 d0dc81739e..98cf0ebc83 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 @@ -46,13 +46,27 @@ * *

This map is implemented as a Merkle-Patricia tree. It does not permit null keys and values. * - *

This map can be instantiated in two ways - either with keys hashing with methods + *

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)} or with no key hashing - * with methods {@link #newInstanceNoKeyHashing(String, View, Serializer, Serializer)} and - * {@link #newInGroupUnsafeNoKeyHashing(String, byte[], View, Serializer, Serializer)}. In case of - * no key hashing keys are required to be 32-byte long. Note that the former option is considered - * a default one. + * {@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: + * + *

* *

The "destructive" methods of the map, i.e., the one that change the map contents, * are specified to throw {@link UnsupportedOperationException} if @@ -71,7 +85,7 @@ */ public final class ProofMapIndexProxy extends AbstractIndexProxy implements MapIndex { - private final ProofMapKeyCheckingSerializerDecorator keySerializer; + private final Serializer keySerializer; private final CheckingSerializerDecorator valueSerializer; /** @@ -99,8 +113,9 @@ public static ProofMapIndexProxy newInstance( } /** - * Creates a ProofMapIndexProxy with no key hashing. Requires that keys are 32-byte long. + * Creates a non-key-hashing ProofMapIndexProxy. Requires that keys are 32-byte long. * + *

See more on key hashing in proof maps. * @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. @@ -112,7 +127,6 @@ public static ProofMapIndexProxy newInstance( * @throws IllegalStateException if the view is not valid * @throws IllegalArgumentException if the name is empty * @see StandardSerializers - * @see ProofMapIndexProxy */ public static ProofMapIndexProxy newInstanceNoKeyHashing( String name, View view, Serializer keySerializer, Serializer valueSerializer) { @@ -153,11 +167,13 @@ public static ProofMapIndexProxy newInGroupUnsafe( } /** - * Creates a new proof map in a collection group + * Creates a new non-key-hashing proof map in a collection group * with the given name. Requires that keys are 32-byte long. * *

See a caveat on index identifiers. * + *

See more on key hashing in proof maps. + * * @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 @@ -169,7 +185,6 @@ public static ProofMapIndexProxy newInGroupUnsafe( * @throws IllegalStateException if the view is not valid * @throws IllegalArgumentException if the name or index id is empty * @see StandardSerializers - * @see ProofMapIndexProxy */ public static ProofMapIndexProxy newInGroupUnsafeNoKeyHashing( String groupName, byte[] mapId, View view, Serializer keySerializer, @@ -200,7 +215,7 @@ private static ProofMapIndexProxy checkCachedInstance(StorageIndex private static ProofMapIndexProxy newMapIndexProxy(IndexAddress address, View view, Serializer keySerializer, Serializer valueSerializer, LongSupplier nativeMapConstructor, boolean keyHashing) { - ProofMapKeyCheckingSerializerDecorator ks = decorateKeySerializer(keySerializer, keyHashing); + Serializer ks = decorateKeySerializer(keySerializer, keyHashing); CheckingSerializerDecorator vs = CheckingSerializerDecorator.from(valueSerializer); NativeHandle mapNativeHandle = createNativeMap(view, nativeMapConstructor); @@ -210,14 +225,12 @@ private static ProofMapIndexProxy newMapIndexProxy(IndexAddress add return map; } - private static ProofMapKeyCheckingSerializerDecorator decorateKeySerializer( + private static Serializer decorateKeySerializer( Serializer keySerializer, boolean keyHashing) { if (!keyHashing) { - ProofMapKeySizeCheckingSerializerDecorator sizeCheckingSerializerDecorator = - ProofMapKeySizeCheckingSerializerDecorator.from(keySerializer); - return ProofMapKeyCheckingSerializerDecorator.from(sizeCheckingSerializerDecorator); - } else { return ProofMapKeyCheckingSerializerDecorator.from(keySerializer); + } else { + return CheckingSerializerDecorator.from(keySerializer); } } @@ -236,7 +249,7 @@ private static native long nativeCreateInGroup(String groupName, byte[] mapId, 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; @@ -258,7 +271,8 @@ public boolean containsKey(K 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 (in case of a - * no key hashing proof map) + * non-key-hashing proof map, see + * key hashing in proof maps) * @throws UnsupportedOperationException if this map is read-only */ @Override @@ -302,7 +316,8 @@ public V get(K key) { * @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 (in case of a - * no key hashing proof map) + * non-key-hashing proof map, see + * key hashing in proof maps) */ public UncheckedMapProof getProof(K key, K... otherKeys) { if (otherKeys.length == 0) { @@ -320,7 +335,9 @@ public UncheckedMapProof getProof(K key, K... otherKeys) { * @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 (in case of a - * no key hashing proof map) or keys collection is empty + * non-key-hashing proof map, see + * key hashing in proof maps) 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/main/java/com/exonum/binding/core/storage/indices/ProofMapKeyCheckingSerializerDecorator.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ProofMapKeyCheckingSerializerDecorator.java index 64f2ed47a4..2d406e1783 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ProofMapKeyCheckingSerializerDecorator.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ProofMapKeyCheckingSerializerDecorator.java @@ -16,12 +16,15 @@ package com.exonum.binding.core.storage.indices; +import static com.exonum.binding.core.storage.indices.StoragePreconditions.checkProofKey; import static com.google.common.base.Preconditions.checkNotNull; import com.exonum.binding.common.serialization.Serializer; /** - * A serializer decorator that checks proof map keys are not null. + * A serializer decorator that checks proof map keys for correctness. + * + * @see StoragePreconditions#checkProofKey(byte[]) */ final class ProofMapKeyCheckingSerializerDecorator implements Serializer { @@ -46,12 +49,12 @@ private ProofMapKeyCheckingSerializerDecorator(Serializer delegate) { @Override public byte[] toBytes(T proofKey) { byte[] dbValue = delegate.toBytes(proofKey); - return checkNotNull(dbValue); + return checkProofKey(dbValue); } @Override public T fromBytes(byte[] serializedProofKey) { - checkNotNull(serializedProofKey); + checkProofKey(serializedProofKey); return delegate.fromBytes(serializedProofKey); } } diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ProofMapKeySizeCheckingSerializerDecorator.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ProofMapKeySizeCheckingSerializerDecorator.java deleted file mode 100644 index 1f5c9acea9..0000000000 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ProofMapKeySizeCheckingSerializerDecorator.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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.StoragePreconditions.checkProofKey; -import static com.google.common.base.Preconditions.checkNotNull; - -import com.exonum.binding.common.serialization.Serializer; - -/** - * A serializer decorator that checks that proof map keys are 32-byte long. - * - * @see StoragePreconditions#checkProofKey(byte[]) - */ -final class ProofMapKeySizeCheckingSerializerDecorator implements Serializer { - - private final Serializer delegate; - - /** - * Creates a proof map key checking serializer decorator. Will not decorate itself. - * - * @param serializer a serializer to decorate - */ - public static ProofMapKeySizeCheckingSerializerDecorator from(Serializer serializer) { - if (serializer instanceof ProofMapKeySizeCheckingSerializerDecorator) { - return (ProofMapKeySizeCheckingSerializerDecorator) serializer; - } - return new ProofMapKeySizeCheckingSerializerDecorator<>(serializer); - } - - private ProofMapKeySizeCheckingSerializerDecorator(Serializer delegate) { - this.delegate = checkNotNull(delegate); - } - - @Override - public byte[] toBytes(T proofKey) { - byte[] dbValue = delegate.toBytes(proofKey); - return checkProofKey(dbValue); - } - - @Override - public T fromBytes(byte[] serializedProofKey) { - checkProofKey(serializedProofKey); - return delegate.fromBytes(serializedProofKey); - } -} diff --git a/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/BaseProofMapIndexProxyIntegrationTest.java b/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/BaseProofMapIndexProxyIntegrationTestable.java similarity index 58% rename from exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/BaseProofMapIndexProxyIntegrationTest.java rename to exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/BaseProofMapIndexProxyIntegrationTestable.java index 3510a72423..bb5bcfe15c 100644 --- a/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/BaseProofMapIndexProxyIntegrationTest.java +++ b/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/BaseProofMapIndexProxyIntegrationTestable.java @@ -31,7 +31,6 @@ 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; @@ -53,14 +52,11 @@ 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.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.List; import java.util.Map; @@ -72,7 +68,7 @@ import java.util.stream.Stream; import org.junit.jupiter.api.Test; -abstract class BaseProofMapIndexProxyIntegrationTest +abstract class BaseProofMapIndexProxyIntegrationTestable extends BaseIndexProxyTestable> { private static final String MAP_NAME = "test_proof_map"; @@ -217,85 +213,9 @@ void getProof_SingletonMapDoesNotContain() { } @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(BaseProofMapIndexProxyIntegrationTest::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(BaseProofMapIndexProxyIntegrationTest::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() { + void getProof_MultiEntryMapContains() { 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) - ); - + List> entries = createMapEntries(); putAll(map, entries); for (MapEntry e : entries) { @@ -305,21 +225,19 @@ void getProof_FourEntryMap_FirstAndLastByte_Contains() { } @Test - void getProof_MultiEntryMapContains() { + void getMultiProof_MultiEntryMapContains() { runTestWithView(database::createFork, (map) -> { - List> entries = createSortedMapEntries(); + List> entries = createMapEntries(); putAll(map, entries); - for (MapEntry e : entries) { - assertThat(map, provesThatPresent(e.getKey(), e.getValue())); - } + assertThat(map, provesThatPresent(entries)); }); } @Test void getProof_MultiEntryMapDoesNotContain() { runTestWithView(database::createFork, (map) -> { - List> entries = createSortedMapEntries(); + List> entries = createMapEntries(); putAll(map, entries); byte[] allOnes = new byte[PROOF_MAP_KEY_SIZE]; @@ -338,23 +256,6 @@ void getProof_MultiEntryMapDoesNotContain() { }); } - @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) -> @@ -397,135 +298,6 @@ void getMultiProof_TwoElementMapContains() { }); } - @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(BaseProofMapIndexProxyIntegrationTest::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(BaseProofMapIndexProxyIntegrationTest::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) -> { @@ -551,7 +323,7 @@ void clearEmptyHasNoEffect() { @Test void clearNonEmptyRemovesAllValues() { runTestWithView(database::createFork, (map) -> { - List> entries = createSortedMapEntries(); + List> entries = createMapEntries(); putAll(map, entries); @@ -635,45 +407,6 @@ void getProofFromCollection() { }); } - /** - * 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. * @@ -685,12 +418,12 @@ private static HashCode createProofKey(String suffix) { return createProofKey(suffixBytes); } - private static HashCode createProofKey(byte... suffixBytes) { + static HashCode createProofKey(byte... suffixBytes) { byte[] proofKey = createRawProofKey(suffixBytes); return HashCode.fromBytes(proofKey); } - private static byte[] createRawProofKey(byte... suffixBytes) { + 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, @@ -731,12 +464,7 @@ void update(ProofMapIndexProxy index) { index.put(key1, V1); } - /** - * Creates `numOfEntries` map entries, sorted by key: - * [(00…0PK1, V1), (00…0PK2, V2), … (00…0PKi, Vi)]. - */ - List> createSortedMapEntries() { - // Use PROOF_KEYS which are already sorted. + List> createMapEntries() { return createMapEntries(getProofKeys().stream()); } @@ -744,63 +472,11 @@ List> createSortedMapEntries() { * Creates map entries with the given keys. Uses values * from {@linkplain TestStorageItems#values} in a round-robin fashion. */ - private List> createMapEntries(Stream proofKeys) { + 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/ProofMapIndexProxyGroupIntegrationTest.java b/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/ProofMapIndexProxyGroupIntegrationTest.java index 67f14b2665..9c948240a0 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 @@ -20,33 +20,28 @@ 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; -class ProofMapIndexProxyGroupIntegrationTest extends BaseMapIndexGroupTestable { - - private static final HashCode PK1 = HashCode.fromString(K1); - private static final HashCode PK2 = HashCode.fromString(K2); - private static final HashCode PK3 = HashCode.fromString(K3); +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/ProofMapIndexProxyIntegrationTest.java b/exonum-java-binding/core/src/test/java/com/exonum/binding/core/storage/indices/ProofMapIndexProxyIntegrationTest.java index ad908b4c05..3b8f1da006 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 @@ -26,7 +26,7 @@ import java.util.stream.Stream; class ProofMapIndexProxyIntegrationTest - extends BaseProofMapIndexProxyIntegrationTest { + extends BaseProofMapIndexProxyIntegrationTestable { private static final List PROOF_KEYS = Stream.of( Bytes.bytes(0x00), 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 index 8068d777e4..d9535e6401 100644 --- 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 @@ -17,10 +17,12 @@ 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.StoragePreconditions.checkProofKey; import static com.exonum.binding.core.storage.indices.TestStorageItems.V1; -import static com.google.common.base.Preconditions.checkArgument; +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; @@ -30,8 +32,12 @@ 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; @@ -39,7 +45,7 @@ import org.junit.jupiter.api.Test; class ProofMapIndexProxyNoKeyHashingIntegrationTest - extends BaseProofMapIndexProxyIntegrationTest { + extends BaseProofMapIndexProxyIntegrationTestable { static final List PROOF_KEYS = Stream.of( Bytes.bytes(0x00), @@ -57,7 +63,7 @@ class ProofMapIndexProxyNoKeyHashingIntegrationTest Bytes.bytes(0x10, 0x01), Bytes.bytes(0x10, 0x10) ) - .map(ProofMapIndexProxyNoKeyHashingIntegrationTest::createRawProofKey) + .map(BaseProofMapIndexProxyIntegrationTestable::createRawProofKey) .sorted(UnsignedBytes.lexicographicalComparator()) .map(HashCode::fromBytes) .collect(Collectors.toList()); @@ -150,11 +156,327 @@ void entriesTest() { }); } - 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); + @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() { + // Use PROOF_KEYS which are already sorted. + return createMapEntries(getProofKeys().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; } } From 20e7ac892e03bf441c8251e64106faaf3e6fb71d Mon Sep 17 00:00:00 2001 From: "Sergei Makarov (LCL)" Date: Thu, 14 Nov 2019 01:34:52 -0500 Subject: [PATCH 3/8] ECR-3779 Review fixes --- .../core/rust/src/storage/proof_map_index.rs | 4 ++++ .../BaseProofMapIndexProxyIntegrationTestable.java | 10 +++++----- .../ProofMapIndexProxyGroupIntegrationTest.java | 2 ++ ...fMapIndexProxyGroupNoKeyHashingIntegrationTest.java | 8 ++++---- .../indices/ProofMapIndexProxyIntegrationTest.java | 9 ++++++--- .../ProofMapIndexProxyNoKeyHashingIntegrationTest.java | 9 ++++----- 6 files changed, 25 insertions(+), 17 deletions(-) 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 3965d5f2d6..eea04e116a 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 @@ -62,6 +62,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)?; @@ -85,6 +87,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/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 index bb5bcfe15c..f7f2e7f357 100644 --- 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 @@ -73,14 +73,14 @@ abstract class BaseProofMapIndexProxyIntegrationTestable private static final String MAP_NAME = "test_proof_map"; - private final HashCode key1 = getProofKeys().get(0); - private final HashCode key2 = getProofKeys().get(1); - private final HashCode key3 = getProofKeys().get(2); + 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 getProofKeys(); + abstract List getTestKeys(); @Test void containsKey() { @@ -465,7 +465,7 @@ void update(ProofMapIndexProxy index) { } List> createMapEntries() { - return createMapEntries(getProofKeys().stream()); + return createMapEntries(getTestKeys().stream()); } /** 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 9c948240a0..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 @@ -23,7 +23,9 @@ 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; +@Disabled("Disabled until native support is implemented - ECR-3765") class ProofMapIndexProxyGroupIntegrationTest extends BaseMapIndexGroupTestable { private static final String GROUP_NAME = "proof_map_group_IT"; 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 index 6d860bebaf..2fdcdde499 100644 --- 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 @@ -16,7 +16,7 @@ package com.exonum.binding.core.storage.indices; -import static com.exonum.binding.core.storage.indices.ProofMapIndexProxyNoKeyHashingIntegrationTest.PROOF_KEYS; +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; @@ -26,9 +26,9 @@ class ProofMapIndexProxyGroupNoKeyHashingIntegrationTest extends BaseMapIndexGroupTestable { - private static final HashCode PK1 = PROOF_KEYS.get(0); - private static final HashCode PK2 = PROOF_KEYS.get(1); - private static final HashCode PK3 = PROOF_KEYS.get(2); + 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"; 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 3b8f1da006..8ec52a52d0 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 @@ -22,13 +22,16 @@ import com.exonum.binding.common.serialization.StandardSerializers; import com.exonum.binding.core.storage.database.View; import com.exonum.binding.test.Bytes; +import org.junit.jupiter.api.Disabled; + import java.util.List; import java.util.stream.Stream; +@Disabled("Disabled until native support is implemented - ECR-3765") class ProofMapIndexProxyIntegrationTest extends BaseProofMapIndexProxyIntegrationTestable { - 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), @@ -48,8 +51,8 @@ class ProofMapIndexProxyIntegrationTest .collect(toList()); @Override - List getProofKeys() { - return PROOF_KEYS; + List getTestKeys() { + return TEST_KEYS; } @Override 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 index d9535e6401..e211f23c78 100644 --- 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 @@ -47,7 +47,7 @@ class ProofMapIndexProxyNoKeyHashingIntegrationTest extends BaseProofMapIndexProxyIntegrationTestable { - static final List PROOF_KEYS = Stream.of( + static final List SORTED_TEST_KEYS = Stream.of( Bytes.bytes(0x00), Bytes.bytes(0x01), Bytes.bytes(0x02), @@ -71,8 +71,8 @@ class ProofMapIndexProxyNoKeyHashingIntegrationTest private static final HashCode INVALID_PROOF_KEY = HashCode.fromString("1234"); @Override - List getProofKeys() { - return PROOF_KEYS; + List getTestKeys() { + return SORTED_TEST_KEYS; } @Override @@ -424,8 +424,7 @@ private static byte[] keyFromString(String prefix) { * [(00…0PK1, V1), (00…0PK2, V2), … (00…0PKi, Vi)]. */ List> createSortedMapEntries() { - // Use PROOF_KEYS which are already sorted. - return createMapEntries(getProofKeys().stream()); + return createMapEntries(SORTED_TEST_KEYS.stream()); } /** From 8edf1bf549ca79082deb751deb061b74770f06aa Mon Sep 17 00:00:00 2001 From: "Sergei Makarov (LCL)" Date: Thu, 14 Nov 2019 16:41:14 -0500 Subject: [PATCH 4/8] ECR-3779 Review fixes --- .../core/storage/indices/MapIndex.java | 5 ++--- .../core/storage/indices/MapIndexProxy.java | 15 ++++++++++++++ .../storage/indices/ProofMapIndexProxy.java | 20 +++++++++++++++++++ .../ProofMapIndexProxyIntegrationTest.java | 3 +-- 4 files changed, 38 insertions(+), 5 deletions(-) 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..bdb8a39df1 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,14 +94,14 @@ 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. * * @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. * * @throws IllegalStateException if this map is not valid */ @@ -109,7 +109,6 @@ default void putAll(Map sourceMap) { /** * 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/MapIndexProxy.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/MapIndexProxy.java index aa71928962..0cb1fda049 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/MapIndexProxy.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/MapIndexProxy.java @@ -229,6 +229,11 @@ public void remove(K key) { nativeRemove(getNativeHandle(), dbKey); } + /** + * {@inheritDoc} + * + * The keys are ordered in lexicographical order. + */ @Override public Iterator keys() { return StorageIterators.createIterator( @@ -241,6 +246,11 @@ public Iterator keys() { ); } + /** + * {@inheritDoc} + * + * The values are ordered in lexicographical order of keys. + */ @Override public Iterator values() { return StorageIterators.createIterator( @@ -253,6 +263,11 @@ public Iterator values() { ); } + /** + * {@inheritDoc} + * + * The entries are ordered by keys in lexicographical order. + */ @Override public Iterator> entries() { return StorageIterators.createIterator( 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 98cf0ebc83..80ec0f2797 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 @@ -393,6 +393,13 @@ public void remove(K key) { private native void nativeRemove(long nativeHandle, byte[] key); + /** + * {@inheritDoc} + * + * The keys are ordered in lexicographical order if this + * map is a non-key-hashing proof map, see + * key hashing in proof maps. + */ @Override public Iterator keys() { return StorageIterators.createIterator( @@ -411,6 +418,13 @@ public Iterator keys() { private native void nativeKeysIterFree(long iterNativeHandle); + /** + * {@inheritDoc} + * + * The values are ordered in lexicographical order of + * keys if this map is a non-key-hashing proof map, see + * key hashing in proof maps. + */ @Override public Iterator values() { return StorageIterators.createIterator( @@ -429,6 +443,12 @@ public Iterator values() { private native void nativeValuesIterFree(long iterNativeHandle); + /** + * {@inheritDoc} + * + * The entries are ordered by keys in lexicographical order if this map is a non-key-hashing + * proof map, see key hashing in proof maps. + */ @Override public Iterator> entries() { return StorageIterators.createIterator( 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 8ec52a52d0..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 @@ -22,10 +22,9 @@ import com.exonum.binding.common.serialization.StandardSerializers; import com.exonum.binding.core.storage.database.View; import com.exonum.binding.test.Bytes; -import org.junit.jupiter.api.Disabled; - import java.util.List; import java.util.stream.Stream; +import org.junit.jupiter.api.Disabled; @Disabled("Disabled until native support is implemented - ECR-3765") class ProofMapIndexProxyIntegrationTest From 48109cadfbe0a2c071f296e7e3a0228093d97cf2 Mon Sep 17 00:00:00 2001 From: "Sergei Makarov (LCL)" Date: Thu, 14 Nov 2019 17:17:03 -0500 Subject: [PATCH 5/8] ECR-3779 Checkstyle fix --- .../exonum/binding/core/storage/indices/MapIndexProxy.java | 6 +++--- .../binding/core/storage/indices/ProofMapIndexProxy.java | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/MapIndexProxy.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/MapIndexProxy.java index 0cb1fda049..fc2e2fac08 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/MapIndexProxy.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/MapIndexProxy.java @@ -232,7 +232,7 @@ public void remove(K key) { /** * {@inheritDoc} * - * The keys are ordered in lexicographical order. + *

The keys are ordered in lexicographical order. */ @Override public Iterator keys() { @@ -249,7 +249,7 @@ public Iterator keys() { /** * {@inheritDoc} * - * The values are ordered in lexicographical order of keys. + *

The values are ordered in lexicographical order of keys. */ @Override public Iterator values() { @@ -266,7 +266,7 @@ public Iterator values() { /** * {@inheritDoc} * - * The entries are ordered by keys in lexicographical order. + *

The entries are ordered by keys in lexicographical order. */ @Override public Iterator> entries() { 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 80ec0f2797..4a6c6eeba6 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 @@ -396,7 +396,7 @@ public void remove(K key) { /** * {@inheritDoc} * - * The keys are ordered in lexicographical order if this + *

The keys are ordered in lexicographical order if this * map is a non-key-hashing proof map, see * key hashing in proof maps. */ @@ -421,7 +421,7 @@ public Iterator keys() { /** * {@inheritDoc} * - * The values are ordered in lexicographical order of + *

The values are ordered in lexicographical order of * keys if this map is a non-key-hashing proof map, see * key hashing in proof maps. */ @@ -446,7 +446,7 @@ public Iterator values() { /** * {@inheritDoc} * - * The entries are ordered by keys in lexicographical order if this map is a non-key-hashing + *

The entries are ordered by keys in lexicographical order if this map is a non-key-hashing * proof map, see key hashing in proof maps. */ @Override From 3e215d1a479e77f516bc200e3d7c08fae8fbc9c6 Mon Sep 17 00:00:00 2001 From: "Sergei Makarov (LCL)" Date: Sat, 16 Nov 2019 00:24:03 -0500 Subject: [PATCH 6/8] ECR-3779 Fix Javadoc and update proof map instantiation in clients --- .../core/blockchain/CoreSchemaProxy.java | 2 +- .../storage/indices/ProofMapIndexProxy.java | 35 ++++++++----------- .../cryptocurrency/CryptocurrencySchema.java | 2 +- .../fakes/services/service/TestSchema.java | 2 +- .../exonum/binding/qaservice/QaSchema.java | 2 +- .../exonum/binding/time/TimeSchemaProxy.java | 2 +- 6 files changed, 20 insertions(+), 25 deletions(-) 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..59d8b6e0c5 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 @@ -152,7 +152,7 @@ MapIndex getTxMessages() { * Returns a map with a key-value pair of a transaction hash and execution result. */ 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/ProofMapIndexProxy.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ProofMapIndexProxy.java index 4a6c6eeba6..0314248747 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 @@ -113,9 +113,9 @@ public static ProofMapIndexProxy newInstance( } /** - * Creates a non-key-hashing ProofMapIndexProxy. Requires that keys are 32-byte long. + * Creates a ProofMapIndexProxy that uses non-hashed keys. + * Requires that keys are 32-byte long. * - *

See more on key hashing in proof maps. * @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. @@ -167,13 +167,12 @@ public static ProofMapIndexProxy newInGroupUnsafe( } /** - * Creates a new non-key-hashing proof map in a collection group - * with the given name. Requires that keys are 32-byte long. + * 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. * - *

See more on key hashing in proof maps. - * * @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 @@ -271,8 +270,7 @@ public boolean containsKey(K 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 (in case of a - * non-key-hashing proof map, see - * key hashing in proof maps) + * proof map that uses non-hashed keys) * @throws UnsupportedOperationException if this map is read-only */ @Override @@ -316,8 +314,7 @@ public V get(K key) { * @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 (in case of a - * non-key-hashing proof map, see - * key hashing in proof maps) + * proof map that uses non-hashed keys) */ public UncheckedMapProof getProof(K key, K... otherKeys) { if (otherKeys.length == 0) { @@ -335,9 +332,8 @@ public UncheckedMapProof getProof(K key, K... otherKeys) { * @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 (in case of a - * non-key-hashing proof map, see - * key hashing in proof maps) or keys - * collection is empty + * 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"); @@ -397,8 +393,8 @@ public void remove(K key) { * {@inheritDoc} * *

The keys are ordered in lexicographical order if this - * map is a non-key-hashing proof map, see - * key hashing in proof maps. + * map is a + * proof map that uses non-hashed keys. */ @Override public Iterator keys() { @@ -421,9 +417,8 @@ public Iterator keys() { /** * {@inheritDoc} * - *

The values are ordered in lexicographical order of - * keys if this map is a non-key-hashing proof map, see - * key hashing in proof maps. + *

The values are ordered in lexicographical order of keys if this map is a + * proof map that uses non-hashed keys. */ @Override public Iterator values() { @@ -446,8 +441,8 @@ public Iterator values() { /** * {@inheritDoc} * - *

The entries are ordered by keys in lexicographical order if this map is a non-key-hashing - * proof map, see key hashing in proof maps. + *

The entries are ordered by keys in lexicographical order if this map is a + * proof map that uses non-hashed keys. */ @Override public Iterator> 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..a989e26ea9 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 @@ -53,7 +53,7 @@ public List getStateHashes() { */ 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 383e624244..1901fc84c0 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 @@ -56,7 +56,7 @@ public List getStateHashes() { */ 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/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); } From 95e96ada6d616a0f872477c63f8eb03e88f21ce7 Mon Sep 17 00:00:00 2001 From: "Sergei Makarov (LCL)" Date: Mon, 18 Nov 2019 23:39:10 -0500 Subject: [PATCH 7/8] ECR-3779 Fix Javadocs --- .../binding/core/blockchain/Blockchain.java | 3 ++- .../core/blockchain/CoreSchemaProxy.java | 3 ++- .../core/storage/indices/MapIndex.java | 7 ++++--- .../core/storage/indices/MapIndexProxy.java | 15 --------------- .../storage/indices/ProofMapIndexProxy.java | 19 ------------------- .../cryptocurrency/CryptocurrencySchema.java | 3 ++- .../exonum/binding/qaservice/QaSchema.java | 3 ++- .../com/exonum/binding/time/TimeSchema.java | 3 ++- 8 files changed, 14 insertions(+), 42 deletions(-) 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 59d8b6e0c5..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,7 +149,8 @@ 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.newInstanceNoKeyHashing(CoreIndex.TRANSACTIONS_RESULTS, dbView, 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 bdb8a39df1..b51861a499 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,21 +94,22 @@ default void putAll(Map sourceMap) { void remove(K key); /** - * Returns an iterator over the map keys. + * 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. + * 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. + * 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/MapIndexProxy.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/MapIndexProxy.java index fc2e2fac08..aa71928962 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/MapIndexProxy.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/MapIndexProxy.java @@ -229,11 +229,6 @@ public void remove(K key) { nativeRemove(getNativeHandle(), dbKey); } - /** - * {@inheritDoc} - * - *

The keys are ordered in lexicographical order. - */ @Override public Iterator keys() { return StorageIterators.createIterator( @@ -246,11 +241,6 @@ public Iterator keys() { ); } - /** - * {@inheritDoc} - * - *

The values are ordered in lexicographical order of keys. - */ @Override public Iterator values() { return StorageIterators.createIterator( @@ -263,11 +253,6 @@ public Iterator values() { ); } - /** - * {@inheritDoc} - * - *

The entries are ordered by keys in lexicographical order. - */ @Override public Iterator> entries() { return StorageIterators.createIterator( 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 0314248747..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 @@ -389,13 +389,6 @@ public void remove(K key) { private native void nativeRemove(long nativeHandle, byte[] key); - /** - * {@inheritDoc} - * - *

The keys are ordered in lexicographical order if this - * map is a - * proof map that uses non-hashed keys. - */ @Override public Iterator keys() { return StorageIterators.createIterator( @@ -414,12 +407,6 @@ public Iterator keys() { private native void nativeKeysIterFree(long iterNativeHandle); - /** - * {@inheritDoc} - * - *

The values are ordered in lexicographical order of keys if this map is a - * proof map that uses non-hashed keys. - */ @Override public Iterator values() { return StorageIterators.createIterator( @@ -438,12 +425,6 @@ public Iterator values() { private native void nativeValuesIterFree(long iterNativeHandle); - /** - * {@inheritDoc} - * - *

The entries are ordered by keys in lexicographical order if this map is a - * proof map that uses non-hashed keys. - */ @Override public Iterator> entries() { return StorageIterators.createIterator( 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 a989e26ea9..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,7 +49,8 @@ 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"); 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 1901fc84c0..f598addee9 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 @@ -52,7 +52,8 @@ public List getStateHashes() { } /** - * 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"); 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(); } From 82bbba8f568528fff70cc760fe5b0ad6e16e3551 Mon Sep 17 00:00:00 2001 From: "Sergei Makarov (LCL)" Date: Wed, 20 Nov 2019 21:49:14 -0500 Subject: [PATCH 8/8] ECR-3779 Checkstyle fix --- .../java/com/exonum/binding/core/storage/indices/MapIndex.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 b51861a499..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 @@ -109,7 +109,8 @@ default void putAll(Map sourceMap) { 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 */