From 61959768f13d84600322fe3c220135a2659bd8a4 Mon Sep 17 00:00:00 2001 From: Dmitry Timofeev Date: Thu, 9 Jan 2020 14:03:01 +0200 Subject: [PATCH 01/20] WIP Add IndexProof --- .../exonum/binding/core/blockchain/Block.java | 25 +++++- .../binding/core/blockchain/Blockchain.java | 44 +++++++++- .../core/blockchain/BlockchainProofs.java | 56 +++++++++++++ .../core/blockchain/proofs/BlockProof.java | 58 +++++++++++++ .../core/blockchain/proofs/IndexProof.java | 60 ++++++++++++++ .../exonum/binding/core/service/Schema.java | 5 +- .../test/BlockchainIntegrationTest.java | 82 +++++++++++++++++-- 7 files changed, 316 insertions(+), 14 deletions(-) create mode 100644 exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/BlockchainProofs.java create mode 100644 exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/proofs/BlockProof.java create mode 100644 exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/proofs/IndexProof.java diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/Block.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/Block.java index 8e22ef20f9..31b83a6344 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/Block.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/Block.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkState; import com.exonum.binding.common.hash.HashCode; +import com.exonum.binding.core.blockchain.serialization.BlockSerializer; import com.exonum.binding.core.blockchain.serialization.CoreTypeAdapterFactory; import com.exonum.binding.core.service.Schema; import com.google.auto.value.AutoValue; @@ -122,6 +123,28 @@ public static TypeAdapter typeAdapter(Gson gson) { return new AutoValue_Block.GsonTypeAdapter(gson); } +// todo: Shall we add it? It will have to serialize the message to compute the hash. +// Or we can get rid of hash? Also, parseFrom(byte[])? + + /** + * Creates a block from the block message. + * @param blockMessage a block + */ + public static Block fromMessage(com.exonum.core.messages.Blockchain.Block blockMessage) { + // fixme: If we *do* keep it — then fix the redundant serialization + return parseFrom(blockMessage.toByteArray()); + } + + /** + * Creates a block from the serialized block message. + * @param serializedBlock a serialized block message + * @throws IllegalArgumentException if the block bytes are not a serialized + * {@link com.exonum.core.messages.Blockchain.Block} + */ + public static Block parseFrom(byte[] serializedBlock) { + return BlockSerializer.INSTANCE.fromBytes(serializedBlock); + } + /** * Creates a new block builder. */ @@ -172,7 +195,7 @@ public abstract static class Builder { * Sets the blockchain state hash at the moment this block was committed. The blockchain * state hash reflects the state of each service in the database. * - * @see Schema#getStateHashes() + * @see Schema */ public abstract Builder stateHash(HashCode blockchainStateHash); 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 3ad7aba9cc..90c58efef7 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 @@ -24,6 +24,9 @@ import com.exonum.binding.common.blockchain.TransactionLocation; import com.exonum.binding.common.hash.HashCode; import com.exonum.binding.common.message.TransactionMessage; +import com.exonum.binding.core.blockchain.proofs.BlockProof; +import com.exonum.binding.core.blockchain.proofs.IndexProof; +import com.exonum.binding.core.storage.database.Snapshot; import com.exonum.binding.core.storage.database.View; import com.exonum.binding.core.storage.indices.KeySetIndexProxy; import com.exonum.binding.core.storage.indices.ListIndex; @@ -32,6 +35,7 @@ import com.exonum.binding.core.storage.indices.ProofMapIndexProxy; import com.exonum.core.messages.Blockchain.CallInBlock; import com.exonum.core.messages.Blockchain.Config; +import com.exonum.core.messages.Proofs; import com.exonum.core.messages.Runtime.ExecutionError; import com.exonum.core.messages.Runtime.ExecutionStatus; import com.google.common.annotations.VisibleForTesting; @@ -43,14 +47,23 @@ * blockchain::Schema features in the Core API: blocks, transaction messages, execution * results. * + *

Proofs

+ * - Types + * - What each proves + * - How to create + * + *
+ * *

All method arguments are non-null by default. */ public final class Blockchain { + private final View view; private final CoreSchema schema; @VisibleForTesting - Blockchain(CoreSchema schema) { + Blockchain(View view, CoreSchema schema) { + this.view = view; this.schema = schema; } @@ -59,7 +72,33 @@ public final class Blockchain { */ public static Blockchain newInstance(View view) { CoreSchema coreSchema = CoreSchema.newInstance(view); - return new Blockchain(coreSchema); + return new Blockchain(view, coreSchema); + } + + /** + * Creates a proof for the block at the given height. + * @param blockHeight a height of the block for which to create a proof + * @throws IndexOutOfBoundsException if the height is not valid + */ + public BlockProof createBlockProof(long blockHeight) { + checkHeight(blockHeight); + Proofs.BlockProof blockProof = BlockchainProofs.createBlockProof(view, blockHeight); + return BlockProof.newInstance(blockProof); + } + + /** + * Creates a proof for a single index in the database. + * @param fullIndexName the full index name for which to create a proof + * @throws IllegalStateException if the view is not a snapshot, because a state of a service index + * can be proved only for the latest committed block, not for any intermediate state during + * transaction processing + */ + public IndexProof createIndexProof(String fullIndexName) { + checkState(!view.canModify(), "Cannot create an index proof for a mutable view (%s).", + view); + Proofs.IndexProof indexProof = BlockchainProofs + .createIndexProof((Snapshot) view, fullIndexName); + return IndexProof.newInstance(indexProof); } /** @@ -243,6 +282,7 @@ public Block getBlock(long height) { return blocks.get(blockHash); } + /** Checks if the blockchain height is valid. */ private void checkHeight(long height) { long blockchainHeight = getHeight(); if (height < 0 || height > blockchainHeight) { diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/BlockchainProofs.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/BlockchainProofs.java new file mode 100644 index 0000000000..7ae77f9baa --- /dev/null +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/BlockchainProofs.java @@ -0,0 +1,56 @@ +/* + * Copyright 2020 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 + * + * https://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.blockchain; + +import com.exonum.binding.core.storage.database.Snapshot; +import com.exonum.binding.core.storage.database.View; +import com.exonum.binding.core.util.LibraryLoader; +import com.exonum.core.messages.Proofs.BlockProof; +import com.exonum.core.messages.Proofs.IndexProof; + +/** + * Provides constructors of block and index proofs. + */ +final class BlockchainProofs { + + static { + LibraryLoader.load(); + } + + /** + * Creates a block proof for the block at the given height. + * @param view a database view + * @param height the height of the block + */ + static BlockProof createBlockProof( + /* todo: here snapshot is not strictly required — but shall we allow Forks (see the ticket) */ + View view, + long height) { + return null; + } + + /** + * Creates an index proof for the index with the given full name, as of the given snapshot. + * @param snapshot a database snapshot + * @param fullIndexName the full name of a proof index for which to create a proof + */ + static IndexProof createIndexProof(Snapshot snapshot, String fullIndexName) { + return null; + } + + private BlockchainProofs() {} +} diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/proofs/BlockProof.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/proofs/BlockProof.java new file mode 100644 index 0000000000..11d7b23b7f --- /dev/null +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/proofs/BlockProof.java @@ -0,0 +1,58 @@ +/* + * Copyright 2020 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 + * + * https://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.blockchain.proofs; + +import com.exonum.core.messages.Proofs; +import com.google.auto.value.AutoValue; +import com.google.protobuf.InvalidProtocolBufferException; + +/** + * A block with a proof. A proof contains signed precommit messages from the network validators + * that agreed to commit this block. + * + *

A block proof can be used independently or as a part of {@linkplain IndexProof index proof}; + * or transaction proof. + * + * + * @see com.exonum.binding.core.blockchain.Block + */ +@AutoValue +public abstract class BlockProof { + + /** + * Returns the proof as a protobuf message. + */ + public abstract Proofs.BlockProof getAsMessage(); + + /** + * Parses a serialized block proof message. + * @throws InvalidProtocolBufferException if the message is not + * {@link com.exonum.core.messages.Proofs.BlockProof} + */ + public static BlockProof parseFrom(byte[] blockProof) + throws InvalidProtocolBufferException { + Proofs.BlockProof parsed = Proofs.BlockProof.parseFrom(blockProof); + return newInstance(parsed); + } + + /** + * Creates a new BlockProof given the block proof message. + */ + public static BlockProof newInstance(Proofs.BlockProof proofMessage) { + return new AutoValue_BlockProof(proofMessage); + } +} diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/proofs/IndexProof.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/proofs/IndexProof.java new file mode 100644 index 0000000000..2f2caccba9 --- /dev/null +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/proofs/IndexProof.java @@ -0,0 +1,60 @@ +/* + * Copyright 2020 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 + * + * https://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.blockchain.proofs; + +import com.exonum.binding.core.storage.indices.MapProof; +import com.exonum.core.messages.Proofs; +import com.google.auto.value.AutoValue; +import com.google.protobuf.InvalidProtocolBufferException; + +/** + * Proof of authenticity for a single index in the database. + * + *

It is comprised of a {@link BlockProof} and a {@link MapProof} from the collection + * aggregating the index hashes of proof indexes for an index with a certain full name. + * + *

If an index does not exist in the database, then the MapProof will prove its absence. + * + * + * @see com.exonum.binding.core.service.Schema + */ +@AutoValue +public abstract class IndexProof { + + /** + * Returns the proof as a protobuf message. + */ + public abstract Proofs.IndexProof getAsMessage(); + + /** + * Parses a serialized index proof message. + * @throws InvalidProtocolBufferException if the message is not + * {@link com.exonum.core.messages.Proofs.IndexProof} + */ + public static IndexProof parseFrom(byte[] indexProof) + throws InvalidProtocolBufferException { + Proofs.IndexProof parsed = Proofs.IndexProof.parseFrom(indexProof); + return newInstance(parsed); + } + + /** + * Creates a new IndexProof given the index proof message. + */ + public static IndexProof newInstance(Proofs.IndexProof proofMessage) { + return new AutoValue_IndexProof(proofMessage); + } +} diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/service/Schema.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/service/Schema.java index b6c3d45cb5..5c1ac6b7f4 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/service/Schema.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/service/Schema.java @@ -16,6 +16,7 @@ package com.exonum.binding.core.service; +import com.exonum.binding.core.blockchain.Block; import com.exonum.binding.core.storage.indices.ProofEntryIndexProxy; import com.exonum.binding.core.storage.indices.ProofListIndexProxy; import com.exonum.binding.core.storage.indices.ProofMapIndexProxy; @@ -27,8 +28,8 @@ * the core automatically tracks every Merkelized collection used by the user * services. It aggregates state hashes of these collections into a single * Merkelized meta-map. The hash of this meta-map is considered the hash of the - * entire blockchain state and is recorded as such in blocks and Precommit - * messages. + * entire blockchain state and is recorded as such in {@linkplain Block#getStateHash() blocks} + * and Precommit messages. * *

Please note that if the service does not use any Merkelized collections, * the framework will not be able to verify that its transactions cause the same diff --git a/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java b/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java index 5bcd4351c6..5ce96ba7ca 100644 --- a/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java +++ b/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java @@ -27,6 +27,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import com.exonum.binding.common.blockchain.CallInBlocks; import com.exonum.binding.common.blockchain.ExecutionStatuses; @@ -41,6 +42,7 @@ import com.exonum.binding.common.message.TransactionMessage; import com.exonum.binding.core.blockchain.Block; import com.exonum.binding.core.blockchain.Blockchain; +import com.exonum.binding.core.blockchain.proofs.BlockProof; import com.exonum.binding.core.storage.database.Snapshot; import com.exonum.binding.core.storage.indices.KeySetIndexProxy; import com.exonum.binding.core.storage.indices.MapIndex; @@ -52,9 +54,13 @@ import com.exonum.core.messages.Blockchain.CallInBlock; import com.exonum.core.messages.Blockchain.Config; import com.exonum.core.messages.Blockchain.ValidatorKeys; +import com.exonum.core.messages.Consensus.Precommit; +import com.exonum.core.messages.Consensus.SignedMessage; +import com.exonum.core.messages.Proofs; import com.exonum.core.messages.Runtime.ErrorKind; import com.exonum.core.messages.Runtime.ExecutionError; import com.exonum.core.messages.Runtime.ExecutionStatus; +import com.exonum.core.messages.Types; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; @@ -63,11 +69,11 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Optional; -import java.util.function.Consumer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.ThrowingConsumer; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -78,7 +84,7 @@ class BlockchainIntegrationTest { private static final short VALIDATOR_COUNT = 1; private static final HashCode ZERO_HASH_CODE = HashCode.fromBytes( new byte[DEFAULT_HASH_SIZE_BYTES]); - private static final int GENESIS_BLOCK_HEIGHT = 0; + private static final long GENESIS_BLOCK_HEIGHT = 0; private static final String SERVICE_NAME = "service"; private static final int SERVICE_ID = 100; @@ -102,6 +108,29 @@ void destroyTestKit() { /** Tests specific to genesis-block only blockchain. */ @Nested class WithGenesisBlock { + + @Test + void createBlockProof() { + testKitTest(blockchain -> { + BlockProof blockProof = blockchain.createBlockProof(GENESIS_BLOCK_HEIGHT); + + // Check the block proof message + Proofs.BlockProof proof = blockProof.getAsMessage(); + com.exonum.core.messages.Blockchain.Block genesisBlock = proof.getBlock(); + assertThat(genesisBlock.getHeight()).isEqualTo(GENESIS_BLOCK_HEIGHT); + // A genesis block proof is a special case: it does not have precommit messages, + // for it is created based on the network configuration only, with no messages. + assertThat(proof.getPrecommitsList()).isEmpty(); + }); + } + + @Test + void createIndexProof() { + testKitTest(blockchain -> { + // todo: + }); + } + @Test void getHeight() { testKitTest((blockchain) -> { @@ -150,6 +179,29 @@ void commitBlockWithSingleTx() { block = testKit.createBlockWithTransactions(transactionMessage); } + @Test + void createBlockProof() { + testKitTest(blockchain -> { + long height = 1L; + BlockProof blockProof = blockchain.createBlockProof(height); + + // Check the block proof message + Proofs.BlockProof proof = blockProof.getAsMessage(); + // Verify the block + Block blockInProof = Block.fromMessage(proof.getBlock()); + assertThat(blockInProof).isEqualTo(block); + // Verify the precommits + assertThat(proof.getPrecommitsList()).hasSize(VALIDATOR_COUNT); + // todo: consider using our (currently, package-private) SignedMessage wrapper. + SignedMessage rawPrecommit = proof.getPrecommits(0).getRaw(); + // todo: author can be verified only via getConsensusConfiguration — which we test separately + PublicKey authorPk = pkFromProto(rawPrecommit.getAuthor()); + Precommit precommit = Precommit.parseFrom(rawPrecommit.getPayload()); + HashCode blockHash = hashFromProto(precommit.getBlockHash()); + assertThat(blockHash).isEqualTo(block.getBlockHash()); + }); + } + @Test void containsBlock() { testKitTest((blockchain) -> assertThat(blockchain.containsBlock(block)).isTrue()); @@ -331,7 +383,7 @@ void getTxLocations() { MapIndex txLocations = blockchain.getTxLocations(); Map txLocationsMap = toMap(txLocations); TransactionLocation expectedTransactionLocation = - TransactionLocation.valueOf(block.getHeight(), 0L); + TransactionLocation.valueOf(block.getHeight(), 0); assertThat(txLocationsMap) .isEqualTo(ImmutableMap.of(expectedBlockTransaction.hash(), expectedTransactionLocation)); @@ -344,7 +396,7 @@ void getTxLocation() { Optional txLocation = blockchain.getTxLocation(expectedBlockTransaction.hash()); TransactionLocation expectedTransactionLocation = - TransactionLocation.valueOf(block.getHeight(), 0L); + TransactionLocation.valueOf(block.getHeight(), 0); assertThat(txLocation).hasValue(expectedTransactionLocation); }); } @@ -433,9 +485,7 @@ void getConsensusConfiguration() { // Check the public service key of the emulated node is included List serviceKeys = configuration.getValidatorKeysList().stream() .map(ValidatorKeys::getServiceKey) - // fixme: [ECR-3734] highly error-prone and verbose key#getData.toByteArray susceptible - // to incorrect key#toByteArray. - .map(key -> PublicKey.fromBytes(key.getData().toByteArray())) + .map(key -> pkFromProto(key)) .collect(toList()); EmulatedNode emulatedNode = testKit.getEmulatedNode(); PublicKey emulatedNodeServiceKey = emulatedNode.getServiceKeyPair().getPublicKey(); @@ -507,10 +557,14 @@ private void checkExecutionError(ExecutionError executionError) { } } - private void testKitTest(Consumer test) { + private void testKitTest(ThrowingConsumer test) { Snapshot view = testKit.getSnapshot(); Blockchain blockchain = Blockchain.newInstance(view); - test.accept(blockchain); + try { + test.accept(blockchain); + } catch (Throwable t) { + fail(t); + } } private static void assertGenesisBlock(Block actualBlock) { @@ -561,4 +615,14 @@ private static Block.Builder aBlock(long blockHeight) { .txRootHash(hashFunction.hashString("transactions at" + blockHeight, UTF_8)) .stateHash(hashFunction.hashString("state hash at " + blockHeight, UTF_8)); } + + private static PublicKey pkFromProto(Types.PublicKey key) { + // fixme: [ECR-3734] highly error-prone and verbose key#getData.toByteArray susceptible + // to incorrect key#toByteArray. + return PublicKey.fromBytes(key.getData().toByteArray()); + } + + private static HashCode hashFromProto(Types.Hash hash) { + return HashCode.fromBytes(hash.getData().toByteArray()); + } } From 2751fe5409c89f4cef9e9fa31128760feacbc0f7 Mon Sep 17 00:00:00 2001 From: Dmitry Timofeev Date: Fri, 10 Jan 2020 10:37:25 +0200 Subject: [PATCH 02/20] Make SignedMessage public: Make SignedMessage public and prevent redundant message serialization in #parseFrom. --- .../binding/common/message/SignedMessage.java | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/exonum-java-binding/common/src/main/java/com/exonum/binding/common/message/SignedMessage.java b/exonum-java-binding/common/src/main/java/com/exonum/binding/common/message/SignedMessage.java index e111e544f0..e0bafef610 100644 --- a/exonum-java-binding/common/src/main/java/com/exonum/binding/common/message/SignedMessage.java +++ b/exonum-java-binding/common/src/main/java/com/exonum/binding/common/message/SignedMessage.java @@ -16,9 +16,10 @@ package com.exonum.binding.common.message; +import static com.exonum.binding.common.hash.Hashing.sha256; + import com.exonum.binding.common.crypto.PublicKey; import com.exonum.binding.common.hash.HashCode; -import com.exonum.binding.common.hash.Hashing; import com.exonum.core.messages.Consensus; import com.exonum.core.messages.Consensus.ExonumMessage; import com.google.protobuf.ByteString; @@ -31,7 +32,7 @@ *

It currently does not support verification of the signature against the author's public * key — such functionality may be added later if needed. */ -final class SignedMessage { +public final class SignedMessage { private final ExonumMessage payload; private final PublicKey authorPk; @@ -56,10 +57,11 @@ private SignedMessage(ExonumMessage payload, PublicKey authorPk, * {@link Consensus.SignedMessage}; or if the payload of the message is not * {@link Consensus.ExonumMessage} */ - static SignedMessage parseFrom(byte[] messageBytes) throws InvalidProtocolBufferException { + public static SignedMessage parseFrom(byte[] messageBytes) throws InvalidProtocolBufferException { // Try to decode the SignedMessage container + HashCode hash = sha256().hashBytes(messageBytes); Consensus.SignedMessage message = Consensus.SignedMessage.parseFrom(messageBytes); - return fromProto(message); + return fromProto(message, hash); } /** @@ -69,26 +71,28 @@ static SignedMessage parseFrom(byte[] messageBytes) throws InvalidProtocolBuffer * @throws InvalidProtocolBufferException if a signed message does not contain a valid payload * that is a serialized {@link Consensus.ExonumMessage} */ - static SignedMessage fromProto(Consensus.SignedMessage message) + public static SignedMessage fromProto(Consensus.SignedMessage message) throws InvalidProtocolBufferException { + HashCode hash = sha256().hashBytes(message.toByteArray()); + return fromProto(message, hash); + } + + private static SignedMessage fromProto(Consensus.SignedMessage message, + HashCode messageHash) throws InvalidProtocolBufferException { // Try to decode the payload, which is stored as bytes. It is expected to be an ExonumMessage ByteString payloadBytes = message.getPayload(); ExonumMessage payload = ExonumMessage.parseFrom(payloadBytes); - PublicKey authorPk = PublicKey.fromBytes(message.getAuthor() .getData() .toByteArray()); ByteString signature = message.getSignature().getData(); - - HashCode hash = Hashing.sha256().hashBytes(message.toByteArray()); - - return new SignedMessage(payload, authorPk, signature, hash); + return new SignedMessage(payload, authorPk, signature, messageHash); } /** * Returns the message payload. */ - Consensus.ExonumMessage getPayload() { + public Consensus.ExonumMessage getPayload() { return payload; } @@ -98,7 +102,7 @@ Consensus.ExonumMessage getPayload() { *

The correctness of the signature is not verified against this key * and must be done separately if needed. */ - PublicKey getAuthorPk() { + public PublicKey getAuthorPk() { return authorPk; } @@ -109,7 +113,7 @@ PublicKey getAuthorPk() { *

The correctness of the signature is not verified against this key * and must be done separately if needed. */ - byte[] getSignature() { + public byte[] getSignature() { return signature.toByteArray(); } @@ -117,7 +121,7 @@ byte[] getSignature() { * Returns the hash of the signed message, which is the hash of the protobuf-serialized * representation. */ - HashCode hash() { + public HashCode hash() { return hash; } } From 1912ce45c10c28d790e9f0c1889e14b9725adb7e Mon Sep 17 00:00:00 2001 From: Dmitry Timofeev Date: Fri, 10 Jan 2020 11:56:57 +0200 Subject: [PATCH 03/20] Finish tests --- .../binding/core/blockchain/Blockchain.java | 11 +++ .../binding/fakeservice/FakeSchema.java | 10 +-- .../binding/fakeservice/FakeService.java | 2 +- .../test/BlockchainIntegrationTest.java | 87 +++++++++++++++---- 4 files changed, 89 insertions(+), 21 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 90c58efef7..6bf317e1b1 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 @@ -77,8 +77,15 @@ public static Blockchain newInstance(View view) { /** * Creates a proof for the block at the given height. + * + *

It allows creating genesis block proofs, but they make little sense, as a genesis + * block is supposed to be a "root of trust", hence, well-known to the clients verifying + * any subsequent proofs coming from the blockchain. + * + *

If you need to create a proof for a service index, use {@link #createIndexProof(String)}. * @param blockHeight a height of the block for which to create a proof * @throws IndexOutOfBoundsException if the height is not valid + * @see #createIndexProof(String) */ public BlockProof createBlockProof(long blockHeight) { checkHeight(blockHeight); @@ -93,6 +100,10 @@ public BlockProof createBlockProof(long blockHeight) { * can be proved only for the latest committed block, not for any intermediate state during * transaction processing */ + /* + Todo: Shall we allow creating proofs for invalid (e.g., impossible) index names or throw + an exception? + */ public IndexProof createIndexProof(String fullIndexName) { checkState(!view.canModify(), "Cannot create an index proof for a mutable view (%s).", view); diff --git a/exonum-java-binding/fake-service/src/main/java/com/exonum/binding/fakeservice/FakeSchema.java b/exonum-java-binding/fake-service/src/main/java/com/exonum/binding/fakeservice/FakeSchema.java index a1df050656..8e6bc1d7d6 100644 --- a/exonum-java-binding/fake-service/src/main/java/com/exonum/binding/fakeservice/FakeSchema.java +++ b/exonum-java-binding/fake-service/src/main/java/com/exonum/binding/fakeservice/FakeSchema.java @@ -20,20 +20,20 @@ import com.exonum.binding.core.service.Schema; import com.exonum.binding.core.storage.database.View; -import com.exonum.binding.core.storage.indices.MapIndexProxy; +import com.exonum.binding.core.storage.indices.ProofMapIndexProxy; -class FakeSchema implements Schema { +public final class FakeSchema implements Schema { private final String namespace; private final View view; - FakeSchema(String serviceName, View view) { + public FakeSchema(String serviceName, View view) { this.namespace = serviceName; this.view = view; } - MapIndexProxy testMap() { + public ProofMapIndexProxy testMap() { String fullName = namespace + ".test-map"; - return MapIndexProxy.newInstance(fullName, view, string(), string()); + return ProofMapIndexProxy.newInstance(fullName, view, string(), string()); } } diff --git a/exonum-java-binding/fake-service/src/main/java/com/exonum/binding/fakeservice/FakeService.java b/exonum-java-binding/fake-service/src/main/java/com/exonum/binding/fakeservice/FakeService.java index 14876dd602..e49dec3631 100644 --- a/exonum-java-binding/fake-service/src/main/java/com/exonum/binding/fakeservice/FakeService.java +++ b/exonum-java-binding/fake-service/src/main/java/com/exonum/binding/fakeservice/FakeService.java @@ -48,7 +48,7 @@ public void createPublicApiHandlers(Node node, Router router) { } /** - * Puts an entry (a key-value pair) into the test map. + * Puts an entry (a key-value pair) into the test proof map. */ @Transaction(PUT_TX_ID) public void putEntry(Transactions.PutTransactionArgs arguments, diff --git a/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java b/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java index 5ce96ba7ca..a765d403ad 100644 --- a/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java +++ b/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java @@ -39,14 +39,17 @@ import com.exonum.binding.common.hash.HashCode; import com.exonum.binding.common.hash.HashFunction; import com.exonum.binding.common.hash.Hashing; +import com.exonum.binding.common.message.SignedMessage; import com.exonum.binding.common.message.TransactionMessage; import com.exonum.binding.core.blockchain.Block; import com.exonum.binding.core.blockchain.Blockchain; import com.exonum.binding.core.blockchain.proofs.BlockProof; +import com.exonum.binding.core.blockchain.proofs.IndexProof; import com.exonum.binding.core.storage.database.Snapshot; import com.exonum.binding.core.storage.indices.KeySetIndexProxy; import com.exonum.binding.core.storage.indices.MapIndex; import com.exonum.binding.core.storage.indices.ProofMapIndexProxy; +import com.exonum.binding.fakeservice.FakeSchema; import com.exonum.binding.fakeservice.Transactions.PutTransactionArgs; import com.exonum.binding.fakeservice.Transactions.RaiseErrorArgs; import com.exonum.binding.testkit.EmulatedNode; @@ -54,8 +57,12 @@ import com.exonum.core.messages.Blockchain.CallInBlock; import com.exonum.core.messages.Blockchain.Config; import com.exonum.core.messages.Blockchain.ValidatorKeys; +import com.exonum.core.messages.Consensus; +import com.exonum.core.messages.Consensus.ExonumMessage; +import com.exonum.core.messages.Consensus.ExonumMessage.KindCase; import com.exonum.core.messages.Consensus.Precommit; -import com.exonum.core.messages.Consensus.SignedMessage; +import com.exonum.core.messages.MapProofOuterClass.MapProof; +import com.exonum.core.messages.MapProofOuterClass.OptionalEntry; import com.exonum.core.messages.Proofs; import com.exonum.core.messages.Runtime.ErrorKind; import com.exonum.core.messages.Runtime.ExecutionError; @@ -64,6 +71,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; +import com.google.protobuf.ByteString; +import com.google.protobuf.Empty; import com.google.protobuf.MessageLite; import java.util.List; import java.util.Map; @@ -71,6 +80,7 @@ import java.util.Optional; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.ThrowingConsumer; @@ -109,6 +119,7 @@ void destroyTestKit() { @Nested class WithGenesisBlock { + @Disabled("ECR-4025") @Test void createBlockProof() { testKitTest(blockchain -> { @@ -124,13 +135,6 @@ void createBlockProof() { }); } - @Test - void createIndexProof() { - testKitTest(blockchain -> { - // todo: - }); - } - @Test void getHeight() { testKitTest((blockchain) -> { @@ -179,6 +183,7 @@ void commitBlockWithSingleTx() { block = testKit.createBlockWithTransactions(transactionMessage); } + @Disabled("ECR-4025") @Test void createBlockProof() { testKitTest(blockchain -> { @@ -187,21 +192,73 @@ void createBlockProof() { // Check the block proof message Proofs.BlockProof proof = blockProof.getAsMessage(); - // Verify the block + // 1 Verify the block Block blockInProof = Block.fromMessage(proof.getBlock()); assertThat(blockInProof).isEqualTo(block); - // Verify the precommits + // 2 Verify the proof: the precommit messages assertThat(proof.getPrecommitsList()).hasSize(VALIDATOR_COUNT); - // todo: consider using our (currently, package-private) SignedMessage wrapper. - SignedMessage rawPrecommit = proof.getPrecommits(0).getRaw(); - // todo: author can be verified only via getConsensusConfiguration — which we test separately - PublicKey authorPk = pkFromProto(rawPrecommit.getAuthor()); - Precommit precommit = Precommit.parseFrom(rawPrecommit.getPayload()); + // Check the precommit message from the single validator + Consensus.SignedMessage rawPrecommitMessage = proof.getPrecommits(0).getRaw(); + SignedMessage rawPrecommit = SignedMessage.fromProto(rawPrecommitMessage); + ExonumMessage payload = rawPrecommit.getPayload(); + assertThat(payload.getKindCase()).isEqualTo(KindCase.PRECOMMIT); + Precommit precommit = payload.getPrecommit(); HashCode blockHash = hashFromProto(precommit.getBlockHash()); + // Check the block hash in precommit matches the actual block hash assertThat(blockHash).isEqualTo(block.getBlockHash()); }); } + @Disabled("ECR-4025") + @Test + void createIndexProof() { + testKitTest(blockchain -> { + String testMapName = SERVICE_NAME + ".test-map"; + IndexProof indexProof = blockchain.createIndexProof(testMapName); + + // Check the index proof message + Proofs.IndexProof proof = indexProof.getAsMessage(); + // 1 Verify the block proof + Proofs.BlockProof blockProof = proof.getBlockProof(); + Block blockInProof = Block.fromMessage(blockProof.getBlock()); + assertThat(blockInProof).isEqualTo(block); + // Verify the precommits + assertThat(blockProof.getPrecommitsList()).hasSize(VALIDATOR_COUNT); + + // 2 Verify the aggregating index proof + MapProof aggregatingIndexProof = proof.getIndexProof(); + // It must have a single entry: (testMapName, indexHash(testMap)) + Snapshot snapshot = testKit.getSnapshot(); + FakeSchema serviceSchema = new FakeSchema(SERVICE_NAME, snapshot); + HashCode testMapHash = serviceSchema.testMap().getIndexHash(); + OptionalEntry expectedEntry = OptionalEntry.newBuilder() + .setKey(ByteString.copyFromUtf8(testMapName)) + .setValue(ByteString.copyFrom(testMapHash.asBytes())) + .build(); + assertThat(aggregatingIndexProof.getEntriesList()).containsExactly(expectedEntry); + }); + } + + @Disabled("ECR-4025") + @Test + void createIndexProofForUnknownIndex() { + testKitTest(blockchain -> { + String testIndexName = "unknown-index"; + IndexProof indexProof = blockchain.createIndexProof(testIndexName); + + // Check the index proof message + Proofs.IndexProof proof = indexProof.getAsMessage(); + // Verify the aggregating index proof + MapProof aggregatingIndexProof = proof.getIndexProof(); + // It must have a single entry: (testIndexName, no index hash) + OptionalEntry expectedEntry = OptionalEntry.newBuilder() + .setKey(ByteString.copyFromUtf8(testIndexName)) + .setNoValue(Empty.getDefaultInstance()) + .build(); + assertThat(aggregatingIndexProof.getEntriesList()).containsExactly(expectedEntry); + }); + } + @Test void containsBlock() { testKitTest((blockchain) -> assertThat(blockchain.containsBlock(block)).isTrue()); From b23e0ccfdcf454297223d3922ced181b82f15574 Mon Sep 17 00:00:00 2001 From: Dmitry Timofeev Date: Fri, 10 Jan 2020 14:52:16 +0200 Subject: [PATCH 04/20] Improve documentation on proofs Also, remove bad/redundant anchors. --- .../binding/core/blockchain/Blockchain.java | 102 ++++++++++++++++-- .../core/blockchain/proofs/BlockProof.java | 2 +- .../core/blockchain/proofs/IndexProof.java | 2 +- .../core/storage/indices/ListProof.java | 4 +- .../core/storage/indices/MapProof.java | 4 +- .../storage/indices/ProofListIndexProxy.java | 1 + .../storage/indices/ProofMapIndexProxy.java | 4 +- .../core/storage/indices/package-info.java | 6 +- .../cryptocurrency/CryptocurrencySchema.java | 3 +- .../exonum/binding/qaservice/QaSchema.java | 3 +- .../com/exonum/binding/time/TimeSchema.java | 4 +- 11 files changed, 110 insertions(+), 25 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 6bf317e1b1..d99eb1e330 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 @@ -47,14 +47,99 @@ * blockchain::Schema features in the Core API: blocks, transaction messages, execution * results. * - *

Proofs

- * - Types - * - What each proves - * - How to create + * + *

Proofs

+ * + *

Blockchain allows creating cryptographic proofs that some data is indeed stored + * in the database. Exonum supports the following types of proofs: + *

+ * + *

Block Proof

+ * + *

A block proof proves correctness of a blockchain block. It can be created with + * {@link #createBlockProof(long)} for any committed block. See also {@link BlockProof}. + * + *

Transaction Execution Proof

+ * + *

A transaction execution proof proves that a transaction with a given message hash was + * executed in a block at a certain height at a certain + * {@linkplain TransactionLocation location}. It consists of a block proof, + * and a list proof from {@link #getBlockTransactions(long)}. It may be extended to + * a call result proof — read the next section. + * + *

Call Result Proof

+ * + *

A call result proof proves that a given service call completed with a particular + * result in a block at a certain height. It consists of a block proof and a map proof + * from {@link #getCallErrors(long)}. In case of transaction calls, it also + * includes a list proof from {@link #getBlockTransactions(long)}. + * + *

Service Data Proof

+ * + *

A service data proof proves that some service index contains certain data as of the last + * committed block. It includes: + *

+ * + *

An index proof is created with {@link #createIndexProof(String)}. + * + *

Example

+ * + *

Consider a simple timestamping service that keeps timestamps for event ids, and supports + * proofs of their authenticity. + * + *

First, create a message definition for a proof: + * + *

+ *   message TimestampProof {
+ *     MapProof timestamp = 1;
+ *     IndexProof indexProof = 2;
+ *   }
+ * 
+ * + *

Then create the two components: timestamp proof from a service index and index proof + * for that index from the blockchain: + * + *

+ *   TimestampProof createTimestampProof(Snapshot s,
+ *                                       String eventId) {
+ *     // 1. Create a timestamp proof
+ *     // The literal is for illustrative purposes —
+ *     // usually the service name is prepended elsewhere
+ *     var fullIndexName = "timestamping.timestamp";
+ *     var timestamps = ProofMapIndexProxy.newInstance(fullIndexName, s,
+ *         string(), timestamp());
+ *     var tsProof = timestamps.getProof(eventId);
+ *
+ *     // 2. Create an index proof
+ *     var blockchain = Blockchain.newInstance(s);
+ *     var indexProof = blockchain.createIndexProof(fullIndexName);
+ *
+ *     // 3. Create a complete service data proof
+ *     return TimestampProof.newBuilder()
+ *       .setTimestamp(tsProof.getAsMessage())
+ *       .setIndexProof(indexProof.getAsMessage())
+ *       .build();
+ *   }
+ * 
+ * + *

Finally, serialize the proof and send it to the client. * *


* *

All method arguments are non-null by default. + * */ public final class Blockchain { @@ -154,9 +239,9 @@ public ListIndex getBlockHashes() { * Returns a proof list of transaction hashes committed in the block at the given height. * *

The {@linkplain ProofListIndexProxy#getIndexHash() index hash} of this index is recorded - * in the block header as {@link Block#getTxRootHash()}. That allows constructing proofs - * - * that a transaction with a certain message hash was executed at a certain + * in the block header as {@link Block#getTxRootHash()}. That allows constructing + * proofs that a transaction + * with a certain message hash was executed at a certain * {@linkplain TransactionLocation location}: (block_height, tx_index_in_block) pair. * * @param height block height starting from 0 @@ -207,8 +292,7 @@ public MapIndex getTxMessages() { * *

The {@linkplain ProofMapIndexProxy#getIndexHash() index hash} of this index is recorded * in the block header as {@code Block#getErrorHash()}. That - * enables constructing proofs - * + * enables constructing proofs * that a certain operation was executed with a particular result. For example, * a proof that a transaction with a certain message hash at * a certain {@linkplain TransactionLocation location} had a certain result must include diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/proofs/BlockProof.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/proofs/BlockProof.java index 11d7b23b7f..08f2615df8 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/proofs/BlockProof.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/proofs/BlockProof.java @@ -26,8 +26,8 @@ * *

A block proof can be used independently or as a part of {@linkplain IndexProof index proof}; * or transaction proof. - * * + * @see Block Proof Creation * @see com.exonum.binding.core.blockchain.Block */ @AutoValue diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/proofs/IndexProof.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/proofs/IndexProof.java index 2f2caccba9..96a6905567 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/proofs/IndexProof.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/proofs/IndexProof.java @@ -28,8 +28,8 @@ * aggregating the index hashes of proof indexes for an index with a certain full name. * *

If an index does not exist in the database, then the MapProof will prove its absence. - * * + * @see Service Data Proofs * @see com.exonum.binding.core.service.Schema */ @AutoValue diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ListProof.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ListProof.java index 53233f78dd..1b191dcc3b 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ListProof.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ListProof.java @@ -27,12 +27,12 @@ * elements in the list, ListProof can assert that the list is shorter than the requested * range of indexes. * * * @see ProofListIndexProxy#getProof(long) * @see ProofListIndexProxy#getRangeProof(long, long) + * @see Service Data Proofs */ @AutoValue public abstract class ListProof { diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/MapProof.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/MapProof.java index a5c33db926..0cc04bbecf 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/MapProof.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/MapProof.java @@ -27,11 +27,11 @@ * Apart from proving the existing entries in the map, MapProof can assert absence of certain keys * in the underlying index. * * * @see ProofMapIndexProxy#getProof(Object, Object[]) + * @see Service Data Proofs */ @AutoValue public abstract class MapProof { diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ProofListIndexProxy.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ProofListIndexProxy.java index b624fb50ac..6edfb00bea 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ProofListIndexProxy.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/ProofListIndexProxy.java @@ -176,6 +176,7 @@ private ProofListIndexProxy(NativeHandle nativeHandle, IndexAddress address, Vie * @param index the element index * @throws IndexOutOfBoundsException if the index is invalid * @throws IllegalStateException if this list is not valid + * @see Blockchain Proofs */ public ListProof getProof(long index) { byte[] proofMessage = nativeGetProof(getNativeHandle(), index); 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 4c69e0a67b..55d756090a 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 @@ -47,7 +47,7 @@ *

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

+ *

Key hashing in proof maps

* *

By default, when creating the proof map using methods * {@link #newInstance(String, View, Serializer, Serializer) #newInstance} and @@ -317,6 +317,7 @@ public V get(K key) { * @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 * proof map that uses non-hashed keys) + * @see Blockchain Proofs */ public MapProof getProof(K key, K... otherKeys) { if (otherKeys.length == 0) { @@ -336,6 +337,7 @@ public MapProof getProof(K key, K... otherKeys) { * @throws IllegalArgumentException if the size of any of the keys is not 32 bytes (in case of a * proof map that uses non-hashed keys) or * keys collection is empty + * @see Blockchain Proofs */ public MapProof 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/package-info.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/package-info.java index 6bc4c0fdf4..6418317130 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/package-info.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/package-info.java @@ -21,14 +21,14 @@ * a {@linkplain com.exonum.binding.core.storage.database.View database view} is inherently * associated with an index. * - *

Index families

+ *

Index families

* *

An index family is a named group of indexes of the same type. Each index in the group * is identified by an identifier, an arbitrary byte string. An index in the group works * the same as an individual index. Indexes in a family are isolated from each other. * It is not possible to iterate through all elements that are stored inside an index group. * - *

Use cases

+ *

Use cases

* *

Index families provide a way to separate elements by a certain criterion. Applications include * indexing, where you create a separate collection group to index another collection of elements @@ -36,7 +36,7 @@ * where you keep an identifier into a collection in group Bar in a structure stored * in collection Foo. * - *

Limitations

+ *

Limitations

* *

Currently Exonum prepends an index identifier within a group to internal, * implementation-specific, keys of that index to keep their elements separate from each other. 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 c0643afa6d..ea70bc027e 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 @@ -43,8 +43,7 @@ public CryptocurrencySchema(View view, String serviceName) { } /** - * Returns a proof map of wallets. Note that this is a - * proof map that uses non-hashed keys. + * 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 0907eae266..ddece7a19a 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 @@ -63,8 +63,7 @@ public TimeSchema timeSchema() { } /** - * Returns a proof map of counter values. Note that this is a - * proof map that uses non-hashed keys. + * 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 b296225a7d..cbda81fc82 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,8 +53,8 @@ static TimeSchema newInstance(View dbView, String name) { ProofEntryIndexProxy getTime(); /** - * Returns the table that stores time for every validator. Note that this is a - * proof map that uses non-hashed keys. + * Returns the table that stores time for every validator. Note that this is a proof map that + * uses non-hashed keys. */ ProofMapIndexProxy getValidatorsTimes(); } From 123b951b529ed023a30dfd72e9e46c0da7b84c65 Mon Sep 17 00:00:00 2001 From: Dmitry Timofeev Date: Fri, 10 Jan 2020 14:59:56 +0200 Subject: [PATCH 05/20] Implement proof constructors --- .../core/blockchain/BlockchainProofs.java | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/BlockchainProofs.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/BlockchainProofs.java index 7ae77f9baa..525bb26fb0 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/BlockchainProofs.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/BlockchainProofs.java @@ -21,6 +21,7 @@ import com.exonum.binding.core.util.LibraryLoader; import com.exonum.core.messages.Proofs.BlockProof; import com.exonum.core.messages.Proofs.IndexProof; +import com.google.protobuf.InvalidProtocolBufferException; /** * Provides constructors of block and index proofs. @@ -40,7 +41,12 @@ static BlockProof createBlockProof( /* todo: here snapshot is not strictly required — but shall we allow Forks (see the ticket) */ View view, long height) { - return null; + byte[] blockProof = nativeCreateBlockProof(view.getViewNativeHandle(), height); + try { + return BlockProof.parseFrom(blockProof); + } catch (InvalidProtocolBufferException e) { + throw new AssertionError("Invalid block proof from native", e); + } } /** @@ -49,8 +55,17 @@ static BlockProof createBlockProof( * @param fullIndexName the full name of a proof index for which to create a proof */ static IndexProof createIndexProof(Snapshot snapshot, String fullIndexName) { - return null; + byte[] indexProof = nativeCreateIndexProof(snapshot.getViewNativeHandle(), fullIndexName); + try { + return IndexProof.parseFrom(indexProof); + } catch (InvalidProtocolBufferException e) { + throw new AssertionError("Invalid index proof from native", e); + } } + static native byte[] nativeCreateBlockProof(long viewNativeHandle, long blockHeight); + + static native byte[] nativeCreateIndexProof(long snapshotNativeHandle, String fullIndexName); + private BlockchainProofs() {} } From 13c4ed5bc0c512e69f54c5f722a0156c1230f98a Mon Sep 17 00:00:00 2001 From: Dmitry Timofeev Date: Fri, 10 Jan 2020 15:13:37 +0200 Subject: [PATCH 06/20] Todo/fixes --- .../java/com/exonum/binding/core/blockchain/Blockchain.java | 6 ++++-- .../main/java/com/exonum/binding/core/service/Schema.java | 2 +- .../com/exonum/binding/test/BlockchainIntegrationTest.java | 4 +++- 3 files changed, 8 insertions(+), 4 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 d99eb1e330..418dbaace5 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 @@ -186,8 +186,10 @@ public BlockProof createBlockProof(long blockHeight) { * transaction processing */ /* - Todo: Shall we allow creating proofs for invalid (e.g., impossible) index names or throw - an exception? + todo: Shall we allow creating proofs for invalid (e.g., impossible) index names or throw + an exception? + + todo: If index proofs for "uninitialized" indexes are forbidden, document that. */ public IndexProof createIndexProof(String fullIndexName) { checkState(!view.canModify(), "Cannot create an index proof for a mutable view (%s).", diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/service/Schema.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/service/Schema.java index 5c1ac6b7f4..cb8dd6cd06 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/service/Schema.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/service/Schema.java @@ -38,7 +38,7 @@ * @see ProofListIndexProxy#getIndexHash() * @see ProofMapIndexProxy#getIndexHash() * @see ProofEntryIndexProxy#getIndexHash() + * @see com.exonum.binding.core.blockchain.Blockchain */ - public interface Schema { } diff --git a/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java b/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java index a765d403ad..1eac83d49f 100644 --- a/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java +++ b/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java @@ -674,12 +674,14 @@ private static Block.Builder aBlock(long blockHeight) { } private static PublicKey pkFromProto(Types.PublicKey key) { - // fixme: [ECR-3734] highly error-prone and verbose key#getData.toByteArray susceptible + // todo: [ECR-3734] highly error-prone and verbose key#getData.toByteArray susceptible // to incorrect key#toByteArray. return PublicKey.fromBytes(key.getData().toByteArray()); } private static HashCode hashFromProto(Types.Hash hash) { + // todo: [ECR-3734] highly error-prone and verbose hash#getData.toByteArray susceptible + // to incorrect hash#toByteArray. return HashCode.fromBytes(hash.getData().toByteArray()); } } From c5d340884f4853b2107f8697ff27ce1447af4eaa Mon Sep 17 00:00:00 2001 From: Dmitry Timofeev Date: Fri, 10 Jan 2020 15:38:03 +0200 Subject: [PATCH 07/20] Fix documentation --- .../src/main/java/com/exonum/binding/core/blockchain/Block.java | 2 +- .../java/com/exonum/binding/core/blockchain/Blockchain.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/Block.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/Block.java index 31b83a6344..85751f3f7e 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/Block.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/Block.java @@ -96,7 +96,7 @@ public final boolean isEmpty() { /** * Root hash of exceptions occurred in the block. * - * @see Blockchain#getCallErrors() + * @see Blockchain#getCallErrors(long) */ public abstract HashCode getErrorHash(); 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 418dbaace5..e3731881a4 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 @@ -293,7 +293,7 @@ public MapIndex getTxMessages() { * are preserved for transactions and before/after transaction handlers. * *

The {@linkplain ProofMapIndexProxy#getIndexHash() index hash} of this index is recorded - * in the block header as {@code Block#getErrorHash()}. That + * in the block header as {@link Block#getErrorHash()}. That * enables constructing proofs * that a certain operation was executed with a particular result. For example, * a proof that a transaction with a certain message hash at From d8db838e8ddd39641399d061cbbde9e28e887254 Mon Sep 17 00:00:00 2001 From: Dmitry Timofeev Date: Fri, 10 Jan 2020 16:03:22 +0200 Subject: [PATCH 08/20] Reimplement Block#fromMessage Prevent redundant deserialization. --- .../exonum/binding/core/blockchain/Block.java | 10 +++---- .../serialization/BlockSerializer.java | 30 ++++++++++++------- .../serialization/BlockSerializerTest.java | 12 ++++++++ 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/Block.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/Block.java index 85751f3f7e..ead65b4b36 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/Block.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/Block.java @@ -16,6 +16,7 @@ package com.exonum.binding.core.blockchain; +import static com.exonum.binding.common.hash.Hashing.sha256; import static com.google.common.base.Preconditions.checkState; import com.exonum.binding.common.hash.HashCode; @@ -123,16 +124,15 @@ public static TypeAdapter typeAdapter(Gson gson) { return new AutoValue_Block.GsonTypeAdapter(gson); } -// todo: Shall we add it? It will have to serialize the message to compute the hash. -// Or we can get rid of hash? Also, parseFrom(byte[])? - /** * Creates a block from the block message. * @param blockMessage a block */ public static Block fromMessage(com.exonum.core.messages.Blockchain.Block blockMessage) { - // fixme: If we *do* keep it — then fix the redundant serialization - return parseFrom(blockMessage.toByteArray()); + // Such implementation prevents a redundant deserialization of Block message + // (in BlockSerializer#fromBytes). + HashCode blockHash = sha256().hashBytes(blockMessage.toByteArray()); + return BlockSerializer.newBlockInternal(blockMessage, blockHash); } /** diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/serialization/BlockSerializer.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/serialization/BlockSerializer.java index e4623a9cb2..c2c28d9c22 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/serialization/BlockSerializer.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/serialization/BlockSerializer.java @@ -16,11 +16,11 @@ package com.exonum.binding.core.blockchain.serialization; +import static com.exonum.binding.common.hash.Hashing.sha256; import static com.exonum.binding.common.serialization.StandardSerializers.protobuf; import static com.google.common.collect.ImmutableMap.toImmutableMap; import com.exonum.binding.common.hash.HashCode; -import com.exonum.binding.common.hash.Hashing; import com.exonum.binding.common.serialization.Serializer; import com.exonum.binding.core.blockchain.Block; import com.exonum.core.messages.Blockchain; @@ -57,18 +57,26 @@ public byte[] toBytes(Block value) { @Override public Block fromBytes(byte[] binaryBlock) { - HashCode blockHash = Hashing.sha256().hashBytes(binaryBlock); - Blockchain.Block copiedBlocks = PROTO_SERIALIZER.fromBytes(binaryBlock); + Blockchain.Block blockMessage = PROTO_SERIALIZER.fromBytes(binaryBlock); + HashCode blockHash = sha256().hashBytes(binaryBlock); + return newBlockInternal(blockMessage, blockHash); + } + + /** + * Creates a block from a message and the block hash. Does not check the hash correctness — hence + * for internal usage only. + */ + public static Block newBlockInternal(Blockchain.Block blockMessage, HashCode blockHash) { return Block.builder() - .proposerId(copiedBlocks.getProposerId()) - .height(copiedBlocks.getHeight()) - .numTransactions(copiedBlocks.getTxCount()) + .proposerId(blockMessage.getProposerId()) + .height(blockMessage.getHeight()) + .numTransactions(blockMessage.getTxCount()) .blockHash(blockHash) - .previousBlockHash(toHashCode(copiedBlocks.getPrevHash())) - .txRootHash(toHashCode(copiedBlocks.getTxHash())) - .stateHash(toHashCode(copiedBlocks.getStateHash())) - .errorHash(toHashCode(copiedBlocks.getErrorHash())) - .additionalHeaders(toHeadersMap(copiedBlocks.getAdditionalHeaders())) + .previousBlockHash(toHashCode(blockMessage.getPrevHash())) + .txRootHash(toHashCode(blockMessage.getTxHash())) + .stateHash(toHashCode(blockMessage.getStateHash())) + .errorHash(toHashCode(blockMessage.getErrorHash())) + .additionalHeaders(toHeadersMap(blockMessage.getAdditionalHeaders())) .build(); } diff --git a/exonum-java-binding/core/src/test/java/com/exonum/binding/core/blockchain/serialization/BlockSerializerTest.java b/exonum-java-binding/core/src/test/java/com/exonum/binding/core/blockchain/serialization/BlockSerializerTest.java index 1b0fe469ad..cd6bffbb11 100644 --- a/exonum-java-binding/core/src/test/java/com/exonum/binding/core/blockchain/serialization/BlockSerializerTest.java +++ b/exonum-java-binding/core/src/test/java/com/exonum/binding/core/blockchain/serialization/BlockSerializerTest.java @@ -26,11 +26,13 @@ import com.exonum.binding.common.serialization.Serializer; import com.exonum.binding.core.blockchain.Block; import com.exonum.binding.core.blockchain.Blocks; +import com.exonum.core.messages.Blockchain; import com.exonum.core.messages.Blockchain.AdditionalHeaders; import com.exonum.core.messages.KeyValueSequenceOuterClass.KeyValue; import com.exonum.core.messages.KeyValueSequenceOuterClass.KeyValueSequence; import com.google.common.collect.ImmutableMap; import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -49,6 +51,16 @@ void roundTrip(Block expected) { assertThat(actual, equalTo(expected)); } + @ParameterizedTest + @MethodSource("testSource") + void roundTripFromMessage(Block expected) throws InvalidProtocolBufferException { + byte[] asBytes = BLOCK_SERIALIZER.toBytes(expected); + Blockchain.Block asMessage = Blockchain.Block.parseFrom(asBytes); + Block actual = Block.fromMessage(asMessage); + + assertThat(actual, equalTo(expected)); + } + private static Stream testSource() { Block block1 = Block.builder() .proposerId(0) From 42601791e2dfb43133ba50b347ce89e1c4f76eaf Mon Sep 17 00:00:00 2001 From: Dmitry Timofeev Date: Fri, 10 Jan 2020 16:06:26 +0200 Subject: [PATCH 09/20] Fix checkstyle [skip ci] --- .../exonum/binding/core/blockchain/Blockchain.java | 12 ++++++------ .../core/storage/indices/ProofMapIndexProxy.java | 2 +- .../binding/core/storage/indices/package-info.java | 6 +++--- 3 files changed, 10 insertions(+), 10 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 e3731881a4..8f14db98b0 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 @@ -185,13 +185,13 @@ public BlockProof createBlockProof(long blockHeight) { * can be proved only for the latest committed block, not for any intermediate state during * transaction processing */ - /* - todo: Shall we allow creating proofs for invalid (e.g., impossible) index names or throw - an exception? - - todo: If index proofs for "uninitialized" indexes are forbidden, document that. - */ public IndexProof createIndexProof(String fullIndexName) { + /* + todo: Shall we allow creating proofs for invalid (e.g., impossible) index names or throw + an exception? + + todo: If index proofs for "uninitialized" indexes are forbidden, document that. + */ checkState(!view.canModify(), "Cannot create an index proof for a mutable view (%s).", view); Proofs.IndexProof indexProof = BlockchainProofs 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 55d756090a..04c8bf7299 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 @@ -47,7 +47,7 @@ *

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

+ *

Key hashing in proof maps>

* *

By default, when creating the proof map using methods * {@link #newInstance(String, View, Serializer, Serializer) #newInstance} and diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/package-info.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/package-info.java index 6418317130..3175d23860 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/package-info.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/storage/indices/package-info.java @@ -21,14 +21,14 @@ * a {@linkplain com.exonum.binding.core.storage.database.View database view} is inherently * associated with an index. * - *

Index families

+ *

Index families

* *

An index family is a named group of indexes of the same type. Each index in the group * is identified by an identifier, an arbitrary byte string. An index in the group works * the same as an individual index. Indexes in a family are isolated from each other. * It is not possible to iterate through all elements that are stored inside an index group. * - *

Use cases

+ *

Use cases

* *

Index families provide a way to separate elements by a certain criterion. Applications include * indexing, where you create a separate collection group to index another collection of elements @@ -36,7 +36,7 @@ * where you keep an identifier into a collection in group Bar in a structure stored * in collection Foo. * - *

Limitations

+ *

Limitations

* *

Currently Exonum prepends an index identifier within a group to internal, * implementation-specific, keys of that index to keep their elements separate from each other. From 6ed552a8a12d1c263600f2a2c0bc6fc72ed87454 Mon Sep 17 00:00:00 2001 From: Dmitry Timofeev Date: Fri, 10 Jan 2020 16:40:26 +0200 Subject: [PATCH 10/20] Add jira ref --- .../java/com/exonum/binding/core/blockchain/Blockchain.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8f14db98b0..5575a1d64d 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,7 @@ *


* *

All method arguments are non-null by default. - * + * */ public final class Blockchain { From eea3ce6f9255059059195ed1796af0d831ab271c Mon Sep 17 00:00:00 2001 From: Dmitry Timofeev Date: Fri, 10 Jan 2020 16:50:41 +0200 Subject: [PATCH 11/20] Add changelog entry [skip ci] --- exonum-java-binding/CHANGELOG.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/exonum-java-binding/CHANGELOG.md b/exonum-java-binding/CHANGELOG.md index 6d58a1c10c..112b14af32 100644 --- a/exonum-java-binding/CHANGELOG.md +++ b/exonum-java-binding/CHANGELOG.md @@ -16,14 +16,26 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] ### Added +- Support of creation of various blockchain proofs: + - Block Proof + - Transaction Execution Proof + - Call Result Proof + - Service Data Proof. + + See [`Blockchain`][blockchain-proofs], `BlockProof` and `IndexProof` + for details. (#1355) - Support of creation of Protobuf-based proofs for maps and lists. Such proofs can be easily serialized using Protocol Buffers and sent to the light clients. - See `ProofMapIndexProxy#getProof` and `MapProof`; - `ProofListIndexProxy.getProof`, `ProofListIndexProxy.getRangeProof` and - `ListProof`. + See: + - `ProofMapIndexProxy#getProof` and `MapProof`; + - `ProofListIndexProxy.getProof`, `ProofListIndexProxy.getRangeProof` and + `ListProof`; + - [`Blockchain`][blockchain-proofs]. - `ProofEntryIndexProxy` collection. +[blockchain-proofs]: https://exonum.com/doc/api/java-binding/0.10.0-SNAPSHOT/com/exonum/binding/core/blockchain/Blockchain.html#proofs + ### Changed - Transactions are now implemented as service methods annotated with `@Transaction(TX_ID)`, instead of classes implementing From 253a45adaf1e90183f10adb75bc2a78dc33f58ba Mon Sep 17 00:00:00 2001 From: Dmitry Timofeev Date: Thu, 16 Jan 2020 11:19:21 +0200 Subject: [PATCH 12/20] Document the exception in case no index proof can be created. --- .../binding/core/blockchain/Blockchain.java | 14 ++++++++------ .../core/blockchain/BlockchainProofs.java | 3 +++ .../binding/test/BlockchainIntegrationTest.java | 17 +++++------------ 3 files changed, 16 insertions(+), 18 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 5575a1d64d..95501b9706 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 @@ -26,6 +26,8 @@ import com.exonum.binding.common.message.TransactionMessage; import com.exonum.binding.core.blockchain.proofs.BlockProof; import com.exonum.binding.core.blockchain.proofs.IndexProof; +import com.exonum.binding.core.service.Configuration; +import com.exonum.binding.core.storage.database.Fork; import com.exonum.binding.core.storage.database.Snapshot; import com.exonum.binding.core.storage.database.View; import com.exonum.binding.core.storage.indices.KeySetIndexProxy; @@ -184,14 +186,14 @@ public BlockProof createBlockProof(long blockHeight) { * @throws IllegalStateException if the view is not a snapshot, because a state of a service index * can be proved only for the latest committed block, not for any intermediate state during * transaction processing + * @throws RuntimeException if the index with the given name does not exist; or is not Merkelized. + * An index does not exist until it is initialized — created for the first time + * with a {@link com.exonum.binding.core.storage.database.Fork}. Depending on the service + * logic, an index may remain uninitialized indefinitely. Therefore, if proofs for an + * empty index need to be created, it must be initialized early in the service lifecycle + * (e.g., in {@link com.exonum.binding.core.service.Service#initialize(Fork, Configuration)}. */ public IndexProof createIndexProof(String fullIndexName) { - /* - todo: Shall we allow creating proofs for invalid (e.g., impossible) index names or throw - an exception? - - todo: If index proofs for "uninitialized" indexes are forbidden, document that. - */ checkState(!view.canModify(), "Cannot create an index proof for a mutable view (%s).", view); Proofs.IndexProof indexProof = BlockchainProofs diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/BlockchainProofs.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/BlockchainProofs.java index 525bb26fb0..8ec3311204 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/BlockchainProofs.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/BlockchainProofs.java @@ -55,6 +55,9 @@ static BlockProof createBlockProof( * @param fullIndexName the full name of a proof index for which to create a proof */ static IndexProof createIndexProof(Snapshot snapshot, String fullIndexName) { + // IndexProof for non-existent index is not supported because it doesn't make sense + // to combine a proof from an uninitialized index (that is not aggregated) with + // a proof of absence in the aggregating collection. byte[] indexProof = nativeCreateIndexProof(snapshot.getViewNativeHandle(), fullIndexName); try { return IndexProof.parseFrom(indexProof); diff --git a/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java b/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java index 1eac83d49f..bc91fe3549 100644 --- a/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java +++ b/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java @@ -72,7 +72,6 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import com.google.protobuf.ByteString; -import com.google.protobuf.Empty; import com.google.protobuf.MessageLite; import java.util.List; import java.util.Map; @@ -244,18 +243,12 @@ void createIndexProof() { void createIndexProofForUnknownIndex() { testKitTest(blockchain -> { String testIndexName = "unknown-index"; - IndexProof indexProof = blockchain.createIndexProof(testIndexName); - // Check the index proof message - Proofs.IndexProof proof = indexProof.getAsMessage(); - // Verify the aggregating index proof - MapProof aggregatingIndexProof = proof.getIndexProof(); - // It must have a single entry: (testIndexName, no index hash) - OptionalEntry expectedEntry = OptionalEntry.newBuilder() - .setKey(ByteString.copyFromUtf8(testIndexName)) - .setNoValue(Empty.getDefaultInstance()) - .build(); - assertThat(aggregatingIndexProof.getEntriesList()).containsExactly(expectedEntry); + Exception e = assertThrows(RuntimeException.class, + () -> blockchain.createIndexProof(testIndexName)); + + // todo: Extend the verifications ECR-4025 + assertThat(e.getMessage()).contains(testIndexName); }); } From d47524e388340b64d5e12366a658ac21d83ce931 Mon Sep 17 00:00:00 2001 From: Dmitry Timofeev Date: Thu, 16 Jan 2020 13:09:37 +0200 Subject: [PATCH 13/20] Update to the new native spec --- .../binding/core/blockchain/Blockchain.java | 17 +++++++++----- .../core/blockchain/BlockchainProofs.java | 23 +++++++++++-------- .../test/BlockchainIntegrationTest.java | 3 +-- 3 files changed, 26 insertions(+), 17 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 95501b9706..36a413a819 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 @@ -181,24 +181,29 @@ public BlockProof createBlockProof(long blockHeight) { } /** - * Creates a proof for a single index in the database. + * Creates a proof for a single index in the database. It is usually a part + * of a Service Data Proof. + * * @param fullIndexName the full index name for which to create a proof * @throws IllegalStateException if the view is not a snapshot, because a state of a service index * can be proved only for the latest committed block, not for any intermediate state during * transaction processing - * @throws RuntimeException if the index with the given name does not exist; or is not Merkelized. - * An index does not exist until it is initialized — created for the first time + * @throws IllegalArgumentException if the index with the given name does not exist; + * or is not Merkelized. An index does not exist until it is initialized — + * created for the first time * with a {@link com.exonum.binding.core.storage.database.Fork}. Depending on the service * logic, an index may remain uninitialized indefinitely. Therefore, if proofs for an * empty index need to be created, it must be initialized early in the service lifecycle * (e.g., in {@link com.exonum.binding.core.service.Service#initialize(Fork, Configuration)}. + * */ public IndexProof createIndexProof(String fullIndexName) { checkState(!view.canModify(), "Cannot create an index proof for a mutable view (%s).", view); - Proofs.IndexProof indexProof = BlockchainProofs - .createIndexProof((Snapshot) view, fullIndexName); - return IndexProof.newInstance(indexProof); + return BlockchainProofs.createIndexProof((Snapshot) view, fullIndexName) + .map(IndexProof::newInstance) + .orElseThrow(() -> new IllegalArgumentException( + String.format("Index %s does not exist or is not Merkelized", fullIndexName))); } /** diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/BlockchainProofs.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/BlockchainProofs.java index 8ec3311204..9f651fa033 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/BlockchainProofs.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/BlockchainProofs.java @@ -22,6 +22,8 @@ import com.exonum.core.messages.Proofs.BlockProof; import com.exonum.core.messages.Proofs.IndexProof; import com.google.protobuf.InvalidProtocolBufferException; +import java.util.Optional; +import javax.annotation.Nullable; /** * Provides constructors of block and index proofs. @@ -54,21 +56,24 @@ static BlockProof createBlockProof( * @param snapshot a database snapshot * @param fullIndexName the full name of a proof index for which to create a proof */ - static IndexProof createIndexProof(Snapshot snapshot, String fullIndexName) { + static Optional createIndexProof(Snapshot snapshot, String fullIndexName) { // IndexProof for non-existent index is not supported because it doesn't make sense - // to combine a proof from an uninitialized index (that is not aggregated) with + // to combine a proof from an uninitialized index (which is not aggregated) with // a proof of absence in the aggregating collection. - byte[] indexProof = nativeCreateIndexProof(snapshot.getViewNativeHandle(), fullIndexName); - try { - return IndexProof.parseFrom(indexProof); - } catch (InvalidProtocolBufferException e) { - throw new AssertionError("Invalid index proof from native", e); - } + return Optional + .ofNullable(nativeCreateIndexProof(snapshot.getViewNativeHandle(), fullIndexName)) + .map(proof -> { + try { + return IndexProof.parseFrom(proof); + } catch (InvalidProtocolBufferException e) { + throw new AssertionError("Invalid index proof from native", e); + } + }); } static native byte[] nativeCreateBlockProof(long viewNativeHandle, long blockHeight); - static native byte[] nativeCreateIndexProof(long snapshotNativeHandle, String fullIndexName); + @Nullable static native byte[] nativeCreateIndexProof(long snapshotNativeHandle, String fullIndexName); private BlockchainProofs() {} } diff --git a/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java b/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java index bc91fe3549..fa87a28e8a 100644 --- a/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java +++ b/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java @@ -244,10 +244,9 @@ void createIndexProofForUnknownIndex() { testKitTest(blockchain -> { String testIndexName = "unknown-index"; - Exception e = assertThrows(RuntimeException.class, + Exception e = assertThrows(IllegalArgumentException.class, () -> blockchain.createIndexProof(testIndexName)); - // todo: Extend the verifications ECR-4025 assertThat(e.getMessage()).contains(testIndexName); }); } From 4a1d3cad4e44ce5d5791cee95a29f2a27c6a873d Mon Sep 17 00:00:00 2001 From: Dmitry Timofeev Date: Thu, 16 Jan 2020 13:22:38 +0200 Subject: [PATCH 14/20] Document the initialization connection --- .../src/main/java/com/exonum/binding/core/service/Schema.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/service/Schema.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/service/Schema.java index cb8dd6cd06..0ee908133c 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/service/Schema.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/service/Schema.java @@ -31,6 +31,9 @@ * entire blockchain state and is recorded as such in {@linkplain Block#getStateHash() blocks} * and Precommit messages. * + *

Exonum starts aggregating a service collection state hash once it is initialized: + * created for the first time with a {@link com.exonum.binding.core.storage.database.Fork}. + * *

Please note that if the service does not use any Merkelized collections, * the framework will not be able to verify that its transactions cause the same * results on different nodes. From 0e835cc67c0c62366b21f6c932923dbb4f6048cb Mon Sep 17 00:00:00 2001 From: Dmitry Timofeev Date: Thu, 16 Jan 2020 13:25:27 +0200 Subject: [PATCH 15/20] Enable the tests depending on the native --- .../com/exonum/binding/test/BlockchainIntegrationTest.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java b/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java index 1cbee3d535..184b90a54e 100644 --- a/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java +++ b/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java @@ -26,7 +26,6 @@ import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import com.exonum.binding.common.blockchain.CallInBlocks; @@ -79,7 +78,6 @@ import java.util.Optional; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.ThrowingConsumer; @@ -118,7 +116,6 @@ void destroyTestKit() { @Nested class WithGenesisBlock { - @Disabled("ECR-4025") @Test void createBlockProof() { testKitTest(blockchain -> { @@ -182,7 +179,6 @@ void commitBlockWithSingleTx() { block = testKit.createBlockWithTransactions(transactionMessage); } - @Disabled("ECR-4025") @Test void createBlockProof() { testKitTest(blockchain -> { @@ -208,7 +204,6 @@ void createBlockProof() { }); } - @Disabled("ECR-4025") @Test void createIndexProof() { testKitTest(blockchain -> { @@ -238,7 +233,6 @@ void createIndexProof() { }); } - @Disabled("ECR-4025") @Test void createIndexProofForUnknownIndex() { testKitTest(blockchain -> { From d0c47f664a1d15683591f186c8a88e21f523136e Mon Sep 17 00:00:00 2001 From: Dmitry Timofeev Date: Thu, 16 Jan 2020 13:30:52 +0200 Subject: [PATCH 16/20] Fix checkstyle --- .../com/exonum/binding/core/blockchain/BlockchainProofs.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/BlockchainProofs.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/BlockchainProofs.java index 9f651fa033..efbbe25332 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/BlockchainProofs.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/blockchain/BlockchainProofs.java @@ -73,7 +73,8 @@ static Optional createIndexProof(Snapshot snapshot, String fullIndex static native byte[] nativeCreateBlockProof(long viewNativeHandle, long blockHeight); - @Nullable static native byte[] nativeCreateIndexProof(long snapshotNativeHandle, String fullIndexName); + @Nullable static native byte[] nativeCreateIndexProof(long snapshotNativeHandle, + String fullIndexName); private BlockchainProofs() {} } From edd78a41aa0c3febd2a639fd885c2320e1c91a84 Mon Sep 17 00:00:00 2001 From: Dmitry Timofeev Date: Thu, 16 Jan 2020 13:33:22 +0200 Subject: [PATCH 17/20] Empty to trigger CI From 581bf98439c45a71951beb8a81c1215baf289c06 Mon Sep 17 00:00:00 2001 From: Dmitry Timofeev Date: Thu, 16 Jan 2020 15:02:07 +0200 Subject: [PATCH 18/20] Fix import --- .../java/com/exonum/binding/test/BlockchainIntegrationTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java b/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java index 184b90a54e..b0c1b20e3a 100644 --- a/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java +++ b/exonum-java-binding/integration-tests/src/test/java/com/exonum/binding/test/BlockchainIntegrationTest.java @@ -26,6 +26,7 @@ import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import com.exonum.binding.common.blockchain.CallInBlocks; From d85adb2f29efe6af3feb2d49c2fdcfdf78bd83b4 Mon Sep 17 00:00:00 2001 From: Dmitry Timofeev Date: Thu, 16 Jan 2020 15:05:37 +0200 Subject: [PATCH 19/20] Fix native methods names --- exonum-java-binding/core/rust/src/storage/blockchain.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/exonum-java-binding/core/rust/src/storage/blockchain.rs b/exonum-java-binding/core/rust/src/storage/blockchain.rs index 41d155b5d3..040e45d4b3 100644 --- a/exonum-java-binding/core/rust/src/storage/blockchain.rs +++ b/exonum-java-binding/core/rust/src/storage/blockchain.rs @@ -20,7 +20,7 @@ use { /// - index is not initialized (index have not been used before calling the method) /// - index is not Merkelized #[no_mangle] -pub extern "system" fn Java_com_exonum_binding_core_blockchain_Blockchain_nativeCreateIndexProof( +pub extern "system" fn Java_com_exonum_binding_core_blockchain_BlockchainProofs_nativeCreateIndexProof( env: JNIEnv, _: JObject, snapshot_handle: jlong, @@ -50,7 +50,7 @@ pub extern "system" fn Java_com_exonum_binding_core_blockchain_Blockchain_native /// - there is no such block /// - passed `snapshot_handle` is Fork handle #[no_mangle] -pub extern "system" fn Java_com_exonum_binding_core_blockchain_Blockchain_nativeCreateBlockProof( +pub extern "system" fn Java_com_exonum_binding_core_blockchain_BlockchainProofs_nativeCreateBlockProof( env: JNIEnv, _: JObject, snapshot_handle: jlong, From 15df4b775d41e87d4d306386d2b50901c89212e8 Mon Sep 17 00:00:00 2001 From: Oleg Bondar Date: Fri, 17 Jan 2020 11:51:24 +0200 Subject: [PATCH 20/20] s/the core/Exonum/ [skip ci] --- .../src/main/java/com/exonum/binding/core/service/Schema.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/service/Schema.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/service/Schema.java index 0ee908133c..a85d7f424d 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/service/Schema.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/service/Schema.java @@ -25,7 +25,7 @@ * A schema of the collections (a.k.a. indices) of a service. * *

To verify the integrity of the database state on each node in the network, - * the core automatically tracks every Merkelized collection used by the user + * Exonum automatically tracks every Merkelized collection used by the user * services. It aggregates state hashes of these collections into a single * Merkelized meta-map. The hash of this meta-map is considered the hash of the * entire blockchain state and is recorded as such in {@linkplain Block#getStateHash() blocks}