From a5daeba71d83cf0142622dcce428d826b17231f4 Mon Sep 17 00:00:00 2001 From: Karim T Date: Wed, 14 Aug 2019 17:47:01 +0200 Subject: [PATCH] Implement eth_getproof JSON RPC API (#1824) Signed-off-by: Adrian Sutton --- .../ethereum/proof/WorldStateProof.java | 68 +++ .../proof/WorldStateProofProvider.java | 84 ++++ .../worldstate/WorldStateArchive.java | 15 + .../proof/WorldStateProofProviderTest.java | 153 +++++++ .../jsonrpc/JsonRpcMethodsFactory.java | 2 + .../pantheon/ethereum/jsonrpc/RpcMethod.java | 1 + .../jsonrpc/internal/methods/EthGetProof.java | 95 ++++ .../internal/response/JsonRpcError.java | 6 + .../results/proof/GetProofResult.java | 122 ++++++ .../results/proof/StorageEntryProof.java | 53 +++ .../internal/methods/EthGetProofTest.java | 211 +++++++++ .../ethereum/trie/MerklePatriciaTrie.java | 8 + .../pegasys/pantheon/ethereum/trie/Proof.java | 38 ++ .../pantheon/ethereum/trie/ProofVisitor.java | 61 +++ .../trie/SimpleMerklePatriciaTrie.java | 12 + .../trie/StoredMerklePatriciaTrie.java | 12 + .../trie/AbstractMerklePatriciaTrieTest.java | 409 ++++++++++++++++++ .../trie/SimpleMerklePatriciaTrieTest.java | 274 +----------- .../trie/StoredMerklePatriciaTrieTest.java | 307 +------------ 19 files changed, 1360 insertions(+), 571 deletions(-) create mode 100644 ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/proof/WorldStateProof.java create mode 100644 ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/proof/WorldStateProofProvider.java create mode 100644 ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/proof/WorldStateProofProviderTest.java create mode 100644 ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/EthGetProof.java create mode 100644 ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/results/proof/GetProofResult.java create mode 100644 ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/results/proof/StorageEntryProof.java create mode 100644 ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/EthGetProofTest.java create mode 100644 ethereum/trie/src/main/java/tech/pegasys/pantheon/ethereum/trie/Proof.java create mode 100644 ethereum/trie/src/main/java/tech/pegasys/pantheon/ethereum/trie/ProofVisitor.java create mode 100644 ethereum/trie/src/test/java/tech/pegasys/pantheon/ethereum/trie/AbstractMerklePatriciaTrieTest.java diff --git a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/proof/WorldStateProof.java b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/proof/WorldStateProof.java new file mode 100644 index 0000000000..3956c5333f --- /dev/null +++ b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/proof/WorldStateProof.java @@ -0,0 +1,68 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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 tech.pegasys.pantheon.ethereum.proof; + +import tech.pegasys.pantheon.ethereum.rlp.RLP; +import tech.pegasys.pantheon.ethereum.trie.Proof; +import tech.pegasys.pantheon.ethereum.worldstate.StateTrieAccountValue; +import tech.pegasys.pantheon.util.bytes.BytesValue; +import tech.pegasys.pantheon.util.uint.UInt256; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.SortedMap; + +public class WorldStateProof { + + private final StateTrieAccountValue stateTrieAccountValue; + + private final Proof accountProof; + + private final Map> storageProofs; + + public WorldStateProof( + final StateTrieAccountValue stateTrieAccountValue, + final Proof accountProof, + final SortedMap> storageProofs) { + this.stateTrieAccountValue = stateTrieAccountValue; + this.accountProof = accountProof; + this.storageProofs = storageProofs; + } + + public StateTrieAccountValue getStateTrieAccountValue() { + return stateTrieAccountValue; + } + + public List getAccountProof() { + return accountProof.getProofRelatedNodes(); + } + + public List getStorageKeys() { + return new ArrayList<>(storageProofs.keySet()); + } + + public UInt256 getStorageValue(final UInt256 key) { + Optional value = storageProofs.get(key).getValue(); + if (value.isEmpty()) { + return UInt256.ZERO; + } else { + return RLP.input(value.get()).readUInt256Scalar(); + } + } + + public List getStorageProof(final UInt256 key) { + return storageProofs.get(key).getProofRelatedNodes(); + } +} diff --git a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/proof/WorldStateProofProvider.java b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/proof/WorldStateProofProvider.java new file mode 100644 index 0000000000..86b59f60e1 --- /dev/null +++ b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/proof/WorldStateProofProvider.java @@ -0,0 +1,84 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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 tech.pegasys.pantheon.ethereum.proof; + +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.rlp.RLP; +import tech.pegasys.pantheon.ethereum.trie.MerklePatriciaTrie; +import tech.pegasys.pantheon.ethereum.trie.Proof; +import tech.pegasys.pantheon.ethereum.trie.StoredMerklePatriciaTrie; +import tech.pegasys.pantheon.ethereum.worldstate.StateTrieAccountValue; +import tech.pegasys.pantheon.ethereum.worldstate.WorldStateStorage; +import tech.pegasys.pantheon.util.bytes.Bytes32; +import tech.pegasys.pantheon.util.bytes.BytesValue; +import tech.pegasys.pantheon.util.uint.UInt256; + +import java.util.List; +import java.util.Optional; +import java.util.SortedMap; +import java.util.TreeMap; + +public class WorldStateProofProvider { + + private final WorldStateStorage worldStateStorage; + + public WorldStateProofProvider(final WorldStateStorage worldStateStorage) { + this.worldStateStorage = worldStateStorage; + } + + public Optional getAccountProof( + final Hash worldStateRoot, + final Address accountAddress, + final List accountStorageKeys) { + + if (!worldStateStorage.isWorldStateAvailable(worldStateRoot)) { + return Optional.empty(); + } else { + final Hash addressHash = Hash.hash(accountAddress); + final Proof accountProof = + newAccountStateTrie(worldStateRoot).getValueWithProof(addressHash); + + return accountProof + .getValue() + .map(RLP::input) + .map(StateTrieAccountValue::readFrom) + .map( + account -> { + final SortedMap> storageProofs = + getStorageProofs(account, accountStorageKeys); + return new WorldStateProof(account, accountProof, storageProofs); + }); + } + } + + private SortedMap> getStorageProofs( + final StateTrieAccountValue account, final List accountStorageKeys) { + final MerklePatriciaTrie storageTrie = + newAccountStorageTrie(account.getStorageRoot()); + final SortedMap> storageProofs = new TreeMap<>(); + accountStorageKeys.forEach( + key -> storageProofs.put(key, storageTrie.getValueWithProof(Hash.hash(key.getBytes())))); + return storageProofs; + } + + private MerklePatriciaTrie newAccountStateTrie(final Bytes32 rootHash) { + return new StoredMerklePatriciaTrie<>( + worldStateStorage::getAccountStateTrieNode, rootHash, b -> b, b -> b); + } + + private MerklePatriciaTrie newAccountStorageTrie(final Bytes32 rootHash) { + return new StoredMerklePatriciaTrie<>( + worldStateStorage::getAccountStorageTrieNode, rootHash, b -> b, b -> b); + } +} diff --git a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/worldstate/WorldStateArchive.java b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/worldstate/WorldStateArchive.java index 6d6109989b..973c8d2b51 100644 --- a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/worldstate/WorldStateArchive.java +++ b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/worldstate/WorldStateArchive.java @@ -12,23 +12,31 @@ */ package tech.pegasys.pantheon.ethereum.worldstate; +import tech.pegasys.pantheon.ethereum.core.Address; import tech.pegasys.pantheon.ethereum.core.Hash; import tech.pegasys.pantheon.ethereum.core.MutableWorldState; import tech.pegasys.pantheon.ethereum.core.WorldState; +import tech.pegasys.pantheon.ethereum.proof.WorldStateProof; +import tech.pegasys.pantheon.ethereum.proof.WorldStateProofProvider; import tech.pegasys.pantheon.ethereum.trie.MerklePatriciaTrie; import tech.pegasys.pantheon.util.bytes.BytesValue; +import tech.pegasys.pantheon.util.uint.UInt256; +import java.util.List; import java.util.Optional; public class WorldStateArchive { private final WorldStateStorage worldStateStorage; private final WorldStatePreimageStorage preimageStorage; + private final WorldStateProofProvider worldStateProof; + private static final Hash EMPTY_ROOT_HASH = Hash.wrap(MerklePatriciaTrie.EMPTY_TRIE_NODE_HASH); public WorldStateArchive( final WorldStateStorage worldStateStorage, final WorldStatePreimageStorage preimageStorage) { this.worldStateStorage = worldStateStorage; this.preimageStorage = preimageStorage; + this.worldStateProof = new WorldStateProofProvider(worldStateStorage); } public Optional get(final Hash rootHash) { @@ -61,4 +69,11 @@ public class WorldStateArchive { public WorldStateStorage getWorldStateStorage() { return worldStateStorage; } + + public Optional getAccountProof( + final Hash worldStateRoot, + final Address accountAddress, + final List accountStorageKeys) { + return worldStateProof.getAccountProof(worldStateRoot, accountAddress, accountStorageKeys); + } } diff --git a/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/proof/WorldStateProofProviderTest.java b/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/proof/WorldStateProofProviderTest.java new file mode 100644 index 0000000000..ab06df5877 --- /dev/null +++ b/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/proof/WorldStateProofProviderTest.java @@ -0,0 +1,153 @@ +/* + * Copyright 2018 ConsenSys AG. + * + * 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 tech.pegasys.pantheon.ethereum.proof; + +import static org.assertj.core.api.Assertions.assertThat; + +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.core.Wei; +import tech.pegasys.pantheon.ethereum.rlp.RLP; +import tech.pegasys.pantheon.ethereum.storage.keyvalue.WorldStateKeyValueStorage; +import tech.pegasys.pantheon.ethereum.trie.MerklePatriciaTrie; +import tech.pegasys.pantheon.ethereum.trie.StoredMerklePatriciaTrie; +import tech.pegasys.pantheon.ethereum.worldstate.StateTrieAccountValue; +import tech.pegasys.pantheon.ethereum.worldstate.WorldStateStorage; +import tech.pegasys.pantheon.ethereum.worldstate.WorldStateStorage.Updater; +import tech.pegasys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import tech.pegasys.pantheon.util.bytes.Bytes32; +import tech.pegasys.pantheon.util.bytes.BytesValue; +import tech.pegasys.pantheon.util.uint.UInt256; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class WorldStateProofProviderTest { + + private static final Address address = + Address.fromHexString("0x1234567890123456789012345678901234567890"); + + private WorldStateStorage worldStateStorage = + new WorldStateKeyValueStorage(new InMemoryKeyValueStorage()); + + private WorldStateProofProvider worldStateProofProvider; + + @Before + public void setup() { + worldStateProofProvider = new WorldStateProofProvider(worldStateStorage); + } + + @Test + public void getProofWhenWorldStateNotAvailable() { + Optional accountProof = + worldStateProofProvider.getAccountProof(Hash.EMPTY, address, new ArrayList<>()); + + assertThat(accountProof).isEmpty(); + } + + @Test + public void getProofWhenWorldStateAvailable() { + final MerklePatriciaTrie worldStateTrie = emptyWorldStateTrie(); + final MerklePatriciaTrie storageTrie = emptyStorageTrie(); + + final Updater updater = worldStateStorage.updater(); + + // Add some storage values + writeStorageValue(storageTrie, UInt256.of(1L), UInt256.of(2L)); + writeStorageValue(storageTrie, UInt256.of(2L), UInt256.of(4L)); + writeStorageValue(storageTrie, UInt256.of(3L), UInt256.of(6L)); + // Save to Storage + storageTrie.commit(updater::putAccountStorageTrieNode); + + // Define account value + final Hash addressHash = Hash.hash(address); + final Hash codeHash = Hash.hash(BytesValue.fromHexString("0x1122")); + final StateTrieAccountValue accountValue = + new StateTrieAccountValue( + 1L, Wei.of(2L), Hash.wrap(storageTrie.getRootHash()), codeHash, 0); + // Save to storage + worldStateTrie.put(addressHash, RLP.encode(accountValue::writeTo)); + worldStateTrie.commit(updater::putAccountStateTrieNode); + + // Persist updates + updater.commit(); + + final List storageKeys = Arrays.asList(UInt256.of(1L), UInt256.of(3L), UInt256.of(6L)); + final Optional accountProof = + worldStateProofProvider.getAccountProof( + Hash.wrap(worldStateTrie.getRootHash()), address, storageKeys); + + assertThat(accountProof).isPresent(); + assertThat(accountProof.get().getStateTrieAccountValue()) + .isEqualToComparingFieldByField(accountValue); + assertThat(accountProof.get().getAccountProof().size()).isGreaterThanOrEqualTo(1); + // Check storage fields + assertThat(accountProof.get().getStorageKeys()).isEqualTo(storageKeys); + // Check key 1 + UInt256 storageKey = UInt256.of(1L); + assertThat(accountProof.get().getStorageValue(storageKey)).isEqualTo(UInt256.of(2L)); + assertThat(accountProof.get().getStorageProof(storageKey).size()).isGreaterThanOrEqualTo(1); + // Check key 3 + storageKey = UInt256.of(3L); + assertThat(accountProof.get().getStorageValue(storageKey)).isEqualTo(UInt256.of(6L)); + assertThat(accountProof.get().getStorageProof(storageKey).size()).isGreaterThanOrEqualTo(1); + // Check key 6 + storageKey = UInt256.of(6L); + assertThat(accountProof.get().getStorageValue(storageKey)).isEqualTo(UInt256.of(0L)); + assertThat(accountProof.get().getStorageProof(storageKey).size()).isGreaterThanOrEqualTo(1); + } + + @Test + public void getProofWhenStateTrieAccountUnavailable() { + final MerklePatriciaTrie worldStateTrie = emptyWorldStateTrie(); + + final Optional accountProof = + worldStateProofProvider.getAccountProof( + Hash.wrap(worldStateTrie.getRootHash()), address, new ArrayList<>()); + + assertThat(accountProof).isEmpty(); + } + + private void writeStorageValue( + final MerklePatriciaTrie storageTrie, + final UInt256 key, + final UInt256 value) { + storageTrie.put(storageKeyHash(key), encodeStorageValue(value)); + } + + private Bytes32 storageKeyHash(final UInt256 storageKey) { + return Hash.hash(storageKey.getBytes()); + } + + private BytesValue encodeStorageValue(final UInt256 storageValue) { + return RLP.encode(out -> out.writeUInt256Scalar(storageValue)); + } + + private MerklePatriciaTrie emptyStorageTrie() { + return new StoredMerklePatriciaTrie<>( + worldStateStorage::getAccountStateTrieNode, b -> b, b -> b); + } + + private MerklePatriciaTrie emptyWorldStateTrie() { + return new StoredMerklePatriciaTrie<>( + worldStateStorage::getAccountStorageTrieNode, b -> b, b -> b); + } +} diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcMethodsFactory.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcMethodsFactory.java index 87fa6905f8..ee35d9929b 100644 --- a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcMethodsFactory.java +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcMethodsFactory.java @@ -48,6 +48,7 @@ import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.EthGetCode; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.EthGetFilterChanges; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.EthGetFilterLogs; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.EthGetLogs; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.EthGetProof; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.EthGetStorageAt; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.EthGetTransactionByBlockHashAndIndex; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.EthGetTransactionByBlockNumberAndIndex; @@ -217,6 +218,7 @@ public class JsonRpcMethodsFactory { parameter), new EthGetCode(blockchainQueries, parameter), new EthGetLogs(blockchainQueries, parameter), + new EthGetProof(blockchainQueries, parameter), new EthGetUncleCountByBlockHash(blockchainQueries, parameter), new EthGetUncleCountByBlockNumber(blockchainQueries, parameter), new EthGetUncleByBlockNumberAndIndex(blockchainQueries, parameter), diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/RpcMethod.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/RpcMethod.java index 0a7303760d..d6bf899259 100644 --- a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/RpcMethod.java +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/RpcMethod.java @@ -57,6 +57,7 @@ public enum RpcMethod { ETH_GET_FILTER_CHANGES("eth_getFilterChanges"), ETH_GET_FILTER_LOGS("eth_getFilterLogs"), ETH_GET_LOGS("eth_getLogs"), + ETH_GET_PROOF("eth_getProof"), ETH_GET_STORAGE_AT("eth_getStorageAt"), ETH_GET_TRANSACTION_BY_BLOCK_HASH_AND_INDEX("eth_getTransactionByBlockHashAndIndex"), ETH_GET_TRANSACTION_BY_BLOCK_NUMBER_AND_INDEX("eth_getTransactionByBlockNumberAndIndex"), diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/EthGetProof.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/EthGetProof.java new file mode 100644 index 0000000000..ea5504ae06 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/EthGetProof.java @@ -0,0 +1,95 @@ +/* + * Copyright 2018 ConsenSys AG. + * + * 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 tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods; + +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.MutableWorldState; +import tech.pegasys.pantheon.ethereum.jsonrpc.RpcMethod; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.parameters.BlockParameter; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.results.proof.GetProofResult; +import tech.pegasys.pantheon.ethereum.proof.WorldStateProof; +import tech.pegasys.pantheon.util.uint.UInt256; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public class EthGetProof extends AbstractBlockParameterMethod { + + private final BlockchainQueries blockchain; + private final JsonRpcParameter parameters; + + public EthGetProof(final BlockchainQueries blockchain, final JsonRpcParameter parameters) { + super(blockchain, parameters); + this.blockchain = blockchain; + this.parameters = parameters; + } + + private Address getAddress(final JsonRpcRequest request) { + return parameters.required(request.getParams(), 0, Address.class); + } + + private List getStorageKeys(final JsonRpcRequest request) { + return Arrays.stream(parameters.required(request.getParams(), 1, String[].class)) + .map(UInt256::fromHexString) + .collect(Collectors.toList()); + } + + @Override + protected BlockParameter blockParameter(final JsonRpcRequest request) { + return parameters.required(request.getParams(), 2, BlockParameter.class); + } + + @Override + protected Object resultByBlockNumber(final JsonRpcRequest request, final long blockNumber) { + + final Address address = getAddress(request); + final List storageKeys = getStorageKeys(request); + + final Optional worldState = blockchain.getWorldState(blockNumber); + + if (worldState.isPresent()) { + Optional proofOptional = + blockchain + .getWorldStateArchive() + .getAccountProof(worldState.get().rootHash(), address, storageKeys); + return proofOptional + .map( + proof -> + (JsonRpcResponse) + new JsonRpcSuccessResponse( + request.getId(), GetProofResult.buildGetProofResult(address, proof))) + .orElse(new JsonRpcErrorResponse(request.getId(), JsonRpcError.NO_ACCOUNT_FOUND)); + } + + return new JsonRpcErrorResponse(request.getId(), JsonRpcError.WORLD_STATE_UNAVAILABLE); + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + return (JsonRpcResponse) findResultByParamType(request); + } + + @Override + public String getName() { + return RpcMethod.ETH_GET_PROOF.getMethodName(); + } +} diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcError.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcError.java index 138b8aa139..0c19e0cdb0 100644 --- a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcError.java +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcError.java @@ -60,6 +60,12 @@ public enum JsonRpcError { // Wallet errors COINBASE_NOT_SPECIFIED(-32000, "Coinbase must be explicitly specified"), + // Account errors + NO_ACCOUNT_FOUND(-32000, "Account not found"), + + // Worldstate erros + WORLD_STATE_UNAVAILABLE(-32000, "World state unavailable"), + // Debug failures PARENT_BLOCK_NOT_FOUND(-32000, "Parent block not found"), diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/results/proof/GetProofResult.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/results/proof/GetProofResult.java new file mode 100644 index 0000000000..960336acdd --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/results/proof/GetProofResult.java @@ -0,0 +1,122 @@ +/* + * Copyright 2018 ConsenSys AG. + * + * 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 tech.pegasys.pantheon.ethereum.jsonrpc.internal.results.proof; + +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.Wei; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.results.Quantity; +import tech.pegasys.pantheon.ethereum.proof.WorldStateProof; +import tech.pegasys.pantheon.ethereum.worldstate.StateTrieAccountValue; +import tech.pegasys.pantheon.util.bytes.Bytes32; +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonGetter; + +public class GetProofResult { + + private final List accountProof; + + private final Address address; + + private final Wei balance; + + private final Bytes32 codeHash; + + private final long nonce; + + private final Bytes32 storageHash; + + private final List storageEntries; + + public GetProofResult( + final Address address, + final Wei balance, + final Bytes32 codeHash, + final long nonce, + final Bytes32 storageHash, + final List accountProof, + final List storageEntries) { + this.address = address; + this.balance = balance; + this.codeHash = codeHash; + this.nonce = nonce; + this.storageHash = storageHash; + this.accountProof = accountProof; + this.storageEntries = storageEntries; + } + + public static GetProofResult buildGetProofResult( + final Address address, final WorldStateProof worldStateProof) { + + final StateTrieAccountValue stateTrieAccountValue = worldStateProof.getStateTrieAccountValue(); + + final List storageEntries = new ArrayList<>(); + worldStateProof + .getStorageKeys() + .forEach( + key -> + storageEntries.add( + new StorageEntryProof( + key, + worldStateProof.getStorageValue(key), + worldStateProof.getStorageProof(key)))); + + return new GetProofResult( + address, + stateTrieAccountValue.getBalance(), + stateTrieAccountValue.getCodeHash(), + stateTrieAccountValue.getNonce(), + stateTrieAccountValue.getStorageRoot(), + worldStateProof.getAccountProof(), + storageEntries); + } + + @JsonGetter(value = "address") + public String getAddress() { + return address.toString(); + } + + @JsonGetter(value = "balance") + public String getBalance() { + return Quantity.create(balance); + } + + @JsonGetter(value = "codeHash") + public String getCodeHash() { + return codeHash.toString(); + } + + @JsonGetter(value = "nonce") + public String getNonce() { + return Quantity.create(nonce); + } + + @JsonGetter(value = "storageHash") + public String getStorageHash() { + return storageHash.toString(); + } + + @JsonGetter(value = "accountProof") + public List getAccountProof() { + return accountProof.stream().map(BytesValue::toString).collect(Collectors.toList()); + } + + @JsonGetter(value = "storageProof") + public List getStorageProof() { + return storageEntries; + } +} diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/results/proof/StorageEntryProof.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/results/proof/StorageEntryProof.java new file mode 100644 index 0000000000..150d445237 --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/results/proof/StorageEntryProof.java @@ -0,0 +1,53 @@ +/* + * Copyright 2018 ConsenSys AG. + * + * 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 tech.pegasys.pantheon.ethereum.jsonrpc.internal.results.proof; + +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.results.Quantity; +import tech.pegasys.pantheon.util.bytes.BytesValue; +import tech.pegasys.pantheon.util.uint.UInt256; + +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonGetter; + +public class StorageEntryProof { + + private final UInt256 key; + + private final UInt256 value; + + private final List storageProof; + + public StorageEntryProof( + final UInt256 key, final UInt256 value, final List storageProof) { + this.key = key; + this.value = value; + this.storageProof = storageProof; + } + + @JsonGetter(value = "key") + public String getKey() { + return key.getBytes().toString(); + } + + @JsonGetter(value = "value") + public String getValue() { + return Quantity.create(value); + } + + @JsonGetter(value = "proof") + public List getStorageProof() { + return storageProof.stream().map(BytesValue::toString).collect(Collectors.toList()); + } +} diff --git a/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/EthGetProofTest.java b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/EthGetProofTest.java new file mode 100644 index 0000000000..9ef3afd6b1 --- /dev/null +++ b/ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/EthGetProofTest.java @@ -0,0 +1,211 @@ +/* + * Copyright 2018 ConsenSys AG. + * + * 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 tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.core.MutableWorldState; +import tech.pegasys.pantheon.ethereum.core.Wei; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.exception.InvalidJsonRpcParameters; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.parameters.JsonRpcParameter; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.results.proof.GetProofResult; +import tech.pegasys.pantheon.ethereum.proof.WorldStateProof; +import tech.pegasys.pantheon.ethereum.worldstate.StateTrieAccountValue; +import tech.pegasys.pantheon.ethereum.worldstate.WorldStateArchive; +import tech.pegasys.pantheon.util.bytes.BytesValue; +import tech.pegasys.pantheon.util.uint.UInt256; + +import java.util.Collections; +import java.util.Optional; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class EthGetProofTest { + + @Rule public final ExpectedException thrown = ExpectedException.none(); + + @Mock private BlockchainQueries blockchainQueries; + + private final JsonRpcParameter parameters = new JsonRpcParameter(); + + private EthGetProof method; + private final String JSON_RPC_VERSION = "2.0"; + private final String ETH_METHOD = "eth_getProof"; + + private final Address address = + Address.fromHexString("0x1234567890123456789012345678901234567890"); + private final UInt256 storageKey = + UInt256.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000001"); + private final long blockNumber = 1; + + @Before + public void setUp() { + method = new EthGetProof(blockchainQueries, parameters); + } + + @Test + public void returnsCorrectMethodName() { + assertThat(method.getName()).isEqualTo(ETH_METHOD); + } + + @Test + public void errorWhenNoAddressAccountSupplied() { + final JsonRpcRequest request = requestWithParams(null, null, "latest"); + + thrown.expect(InvalidJsonRpcParameters.class); + thrown.expectMessage("Missing required json rpc parameter at index 0"); + + method.response(request); + } + + @Test + public void errorWhenNoStorageKeysSupplied() { + final JsonRpcRequest request = requestWithParams(address.toString(), null, "latest"); + + thrown.expect(InvalidJsonRpcParameters.class); + thrown.expectMessage("Missing required json rpc parameter at index 1"); + + method.response(request); + } + + @Test + public void errorWhenNoBlockNumberSupplied() { + final JsonRpcRequest request = requestWithParams(address.toString(), new String[] {}); + + thrown.expect(InvalidJsonRpcParameters.class); + thrown.expectMessage("Missing required json rpc parameter at index 2"); + + method.response(request); + } + + @Test + public void errorWhenAccountNotFound() { + + generateWorldState(); + + final JsonRpcErrorResponse expectedResponse = + new JsonRpcErrorResponse(null, JsonRpcError.NO_ACCOUNT_FOUND); + + final JsonRpcRequest request = + requestWithParams( + Address.fromHexString("0x0000000000000000000000000000000000000000"), + new String[] {storageKey.toString()}, + String.valueOf(blockNumber)); + + final JsonRpcErrorResponse response = (JsonRpcErrorResponse) method.response(request); + + assertThat(response).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void errorWhenWorldStateUnavailable() { + + when(blockchainQueries.getWorldState(blockNumber)).thenReturn(Optional.empty()); + + final JsonRpcErrorResponse expectedResponse = + new JsonRpcErrorResponse(null, JsonRpcError.WORLD_STATE_UNAVAILABLE); + + final JsonRpcRequest request = + requestWithParams( + Address.fromHexString("0x0000000000000000000000000000000000000000"), + new String[] {storageKey.toString()}, + String.valueOf(blockNumber)); + + final JsonRpcErrorResponse response = (JsonRpcErrorResponse) method.response(request); + + assertThat(response).isEqualToComparingFieldByField(expectedResponse); + } + + @Test + public void getProof() { + + final GetProofResult expectedResponse = generateWorldState(); + + final JsonRpcRequest request = + requestWithParams( + address.toString(), new String[] {storageKey.toString()}, String.valueOf(blockNumber)); + + final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); + + assertThat(response.getResult()).isEqualToComparingFieldByFieldRecursively(expectedResponse); + } + + private JsonRpcRequest requestWithParams(final Object... params) { + return new JsonRpcRequest(JSON_RPC_VERSION, ETH_METHOD, params); + } + + @SuppressWarnings("unchecked") + private GetProofResult generateWorldState() { + + final Wei balance = Wei.of(1); + final Hash codeHash = + Hash.fromHexString("0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"); + final long nonce = 1; + final Hash rootHash = + Hash.fromHexString("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b431"); + final Hash storageRoot = + Hash.fromHexString("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"); + + final WorldStateArchive worldStateArchive = mock(WorldStateArchive.class); + + when(blockchainQueries.getWorldStateArchive()).thenReturn(worldStateArchive); + + final StateTrieAccountValue stateTrieAccountValue = mock(StateTrieAccountValue.class); + when(stateTrieAccountValue.getBalance()).thenReturn(balance); + when(stateTrieAccountValue.getCodeHash()).thenReturn(codeHash); + when(stateTrieAccountValue.getNonce()).thenReturn(nonce); + when(stateTrieAccountValue.getStorageRoot()).thenReturn(storageRoot); + + final WorldStateProof worldStateProof = mock(WorldStateProof.class); + when(worldStateProof.getAccountProof()) + .thenReturn( + Collections.singletonList( + BytesValue.fromHexString( + "0x1111111111111111111111111111111111111111111111111111111111111111"))); + when(worldStateProof.getStateTrieAccountValue()).thenReturn(stateTrieAccountValue); + when(worldStateProof.getStorageKeys()).thenReturn(Collections.singletonList(storageKey)); + when(worldStateProof.getStorageProof(storageKey)) + .thenReturn( + Collections.singletonList( + BytesValue.fromHexString( + "0x2222222222222222222222222222222222222222222222222222222222222222"))); + when(worldStateProof.getStorageValue(storageKey)).thenReturn(UInt256.ZERO); + + when(worldStateArchive.getAccountProof(eq(rootHash), eq(address), anyList())) + .thenReturn(Optional.of(worldStateProof)); + + final MutableWorldState mutableWorldState = mock(MutableWorldState.class); + when(mutableWorldState.rootHash()).thenReturn(rootHash); + when(blockchainQueries.getWorldState(blockNumber)).thenReturn(Optional.of(mutableWorldState)); + + return GetProofResult.buildGetProofResult(address, worldStateProof); + } +} diff --git a/ethereum/trie/src/main/java/tech/pegasys/pantheon/ethereum/trie/MerklePatriciaTrie.java b/ethereum/trie/src/main/java/tech/pegasys/pantheon/ethereum/trie/MerklePatriciaTrie.java index 1cac37b452..f72e04b1c7 100644 --- a/ethereum/trie/src/main/java/tech/pegasys/pantheon/ethereum/trie/MerklePatriciaTrie.java +++ b/ethereum/trie/src/main/java/tech/pegasys/pantheon/ethereum/trie/MerklePatriciaTrie.java @@ -36,6 +36,14 @@ public interface MerklePatriciaTrie { */ Optional get(K key); + /** + * Returns value and ordered proof-related nodes mapped to the hash if it exists; otherwise empty. + * + * @param key The key for the value. + * @return value and ordered proof-related nodes + */ + Proof getValueWithProof(K key); + /** * Updates the value mapped to the specified key, creating the mapping if one does not already * exist. diff --git a/ethereum/trie/src/main/java/tech/pegasys/pantheon/ethereum/trie/Proof.java b/ethereum/trie/src/main/java/tech/pegasys/pantheon/ethereum/trie/Proof.java new file mode 100644 index 0000000000..3f88b0472e --- /dev/null +++ b/ethereum/trie/src/main/java/tech/pegasys/pantheon/ethereum/trie/Proof.java @@ -0,0 +1,38 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * 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 tech.pegasys.pantheon.ethereum.trie; + +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import java.util.List; +import java.util.Optional; + +public class Proof { + + private final Optional value; + + private final List proofRelatedNodes; + + public Proof(final Optional value, final List proofRelatedNodes) { + this.value = value; + this.proofRelatedNodes = proofRelatedNodes; + } + + public Optional getValue() { + return value; + } + + public List getProofRelatedNodes() { + return proofRelatedNodes; + } +} diff --git a/ethereum/trie/src/main/java/tech/pegasys/pantheon/ethereum/trie/ProofVisitor.java b/ethereum/trie/src/main/java/tech/pegasys/pantheon/ethereum/trie/ProofVisitor.java new file mode 100644 index 0000000000..4b8d721c3c --- /dev/null +++ b/ethereum/trie/src/main/java/tech/pegasys/pantheon/ethereum/trie/ProofVisitor.java @@ -0,0 +1,61 @@ +/* + * Copyright 2018 ConsenSys AG. + * + * 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 tech.pegasys.pantheon.ethereum.trie; + +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayList; +import java.util.List; + +class ProofVisitor extends GetVisitor implements PathNodeVisitor { + + private final Node rootNode; + private final List> proof = new ArrayList<>(); + + ProofVisitor(final Node rootNode) { + this.rootNode = rootNode; + } + + @Override + public Node visit(final ExtensionNode extensionNode, final BytesValue path) { + maybeTrackNode(extensionNode); + return super.visit(extensionNode, path); + } + + @Override + public Node visit(final BranchNode branchNode, final BytesValue path) { + maybeTrackNode(branchNode); + return super.visit(branchNode, path); + } + + @Override + public Node visit(final LeafNode leafNode, final BytesValue path) { + maybeTrackNode(leafNode); + return super.visit(leafNode, path); + } + + @Override + public Node visit(final NullNode nullNode, final BytesValue path) { + return super.visit(nullNode, path); + } + + public List> getProof() { + return proof; + } + + private void maybeTrackNode(final Node node) { + if (node.equals(rootNode) || node.isReferencedByHash()) { + proof.add(node); + } + } +} diff --git a/ethereum/trie/src/main/java/tech/pegasys/pantheon/ethereum/trie/SimpleMerklePatriciaTrie.java b/ethereum/trie/src/main/java/tech/pegasys/pantheon/ethereum/trie/SimpleMerklePatriciaTrie.java index 25fda9395c..bc2e034eb0 100644 --- a/ethereum/trie/src/main/java/tech/pegasys/pantheon/ethereum/trie/SimpleMerklePatriciaTrie.java +++ b/ethereum/trie/src/main/java/tech/pegasys/pantheon/ethereum/trie/SimpleMerklePatriciaTrie.java @@ -18,10 +18,12 @@ import static tech.pegasys.pantheon.ethereum.trie.CompactEncoding.bytesToPath; import tech.pegasys.pantheon.util.bytes.Bytes32; import tech.pegasys.pantheon.util.bytes.BytesValue; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Consumer; import java.util.function.Function; +import java.util.stream.Collectors; /** * An in-memory {@link MerklePatriciaTrie}. @@ -51,6 +53,16 @@ public class SimpleMerklePatriciaTrie implements Merkle return root.accept(getVisitor, bytesToPath(key)).getValue(); } + @Override + public Proof getValueWithProof(final K key) { + checkNotNull(key); + final ProofVisitor proofVisitor = new ProofVisitor<>(root); + final Optional value = root.accept(proofVisitor, bytesToPath(key)).getValue(); + final List proof = + proofVisitor.getProof().stream().map(Node::getRlp).collect(Collectors.toList()); + return new Proof<>(value, proof); + } + @Override public void put(final K key, final V value) { checkNotNull(key); diff --git a/ethereum/trie/src/main/java/tech/pegasys/pantheon/ethereum/trie/StoredMerklePatriciaTrie.java b/ethereum/trie/src/main/java/tech/pegasys/pantheon/ethereum/trie/StoredMerklePatriciaTrie.java index 78438cff60..42fe68a1d9 100644 --- a/ethereum/trie/src/main/java/tech/pegasys/pantheon/ethereum/trie/StoredMerklePatriciaTrie.java +++ b/ethereum/trie/src/main/java/tech/pegasys/pantheon/ethereum/trie/StoredMerklePatriciaTrie.java @@ -18,10 +18,12 @@ import static tech.pegasys.pantheon.ethereum.trie.CompactEncoding.bytesToPath; import tech.pegasys.pantheon.util.bytes.Bytes32; import tech.pegasys.pantheon.util.bytes.BytesValue; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Consumer; import java.util.function.Function; +import java.util.stream.Collectors; /** * A {@link MerklePatriciaTrie} that persists trie nodes to a {@link MerkleStorage} key/value store. @@ -76,6 +78,16 @@ public class StoredMerklePatriciaTrie implements Merkle return root.accept(getVisitor, bytesToPath(key)).getValue(); } + @Override + public Proof getValueWithProof(final K key) { + checkNotNull(key); + final ProofVisitor proofVisitor = new ProofVisitor<>(root); + final Optional value = root.accept(proofVisitor, bytesToPath(key)).getValue(); + final List proof = + proofVisitor.getProof().stream().map(Node::getRlp).collect(Collectors.toList()); + return new Proof<>(value, proof); + } + @Override public void put(final K key, final V value) { checkNotNull(key); diff --git a/ethereum/trie/src/test/java/tech/pegasys/pantheon/ethereum/trie/AbstractMerklePatriciaTrieTest.java b/ethereum/trie/src/test/java/tech/pegasys/pantheon/ethereum/trie/AbstractMerklePatriciaTrieTest.java new file mode 100644 index 0000000000..0a6ce9aaa7 --- /dev/null +++ b/ethereum/trie/src/test/java/tech/pegasys/pantheon/ethereum/trie/AbstractMerklePatriciaTrieTest.java @@ -0,0 +1,409 @@ +/* + * Copyright 2018 ConsenSys AG. + * + * 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 tech.pegasys.pantheon.ethereum.trie; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static junit.framework.TestCase.assertFalse; +import static org.assertj.core.api.Assertions.assertThat; + +import tech.pegasys.pantheon.services.kvstore.InMemoryKeyValueStorage; +import tech.pegasys.pantheon.services.kvstore.KeyValueStorage; +import tech.pegasys.pantheon.util.bytes.Bytes32; +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import java.util.List; +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; + +public abstract class AbstractMerklePatriciaTrieTest { + protected MerklePatriciaTrie trie; + + @Before + public void setup() { + trie = createTrie(); + } + + protected abstract MerklePatriciaTrie createTrie(); + + @Test + public void emptyTreeReturnsEmpty() { + assertFalse(trie.get(BytesValue.EMPTY).isPresent()); + } + + @Test + public void emptyTreeHasKnownRootHash() { + assertThat(trie.getRootHash().toString()) + .isEqualTo("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"); + } + + @Test(expected = NullPointerException.class) + public void throwsOnUpdateWithNull() { + trie.put(BytesValue.EMPTY, null); + } + + @Test + public void replaceSingleValue() { + final BytesValue key = BytesValue.of(1); + final String value1 = "value1"; + trie.put(key, value1); + assertThat(trie.get(key)).isEqualTo(Optional.of(value1)); + + final String value2 = "value2"; + trie.put(key, value2); + assertThat(trie.get(key)).isEqualTo(Optional.of(value2)); + } + + @Test + public void hashChangesWhenSingleValueReplaced() { + final BytesValue key = BytesValue.of(1); + final String value1 = "value1"; + trie.put(key, value1); + final Bytes32 hash1 = trie.getRootHash(); + + final String value2 = "value2"; + trie.put(key, value2); + final Bytes32 hash2 = trie.getRootHash(); + + assertThat(hash1).isNotEqualTo(hash2); + + trie.put(key, value1); + assertThat(trie.getRootHash()).isEqualTo(hash1); + } + + @Test + public void readPastLeaf() { + final BytesValue key1 = BytesValue.of(1); + trie.put(key1, "value"); + final BytesValue key2 = BytesValue.of(1, 3); + assertFalse(trie.get(key2).isPresent()); + } + + @Test + public void branchValue() { + final BytesValue key1 = BytesValue.of(1); + final BytesValue key2 = BytesValue.of(16); + + final String value1 = "value1"; + trie.put(key1, value1); + + final String value2 = "value2"; + trie.put(key2, value2); + + assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); + assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); + } + + @Test + public void readPastBranch() { + final BytesValue key1 = BytesValue.of(12); + final BytesValue key2 = BytesValue.of(12, 54); + + final String value1 = "value1"; + trie.put(key1, value1); + final String value2 = "value2"; + trie.put(key2, value2); + + final BytesValue key3 = BytesValue.of(3); + assertFalse(trie.get(key3).isPresent()); + } + + @Test + public void branchWithValue() { + final BytesValue key1 = BytesValue.of(5); + final BytesValue key2 = BytesValue.EMPTY; + + final String value1 = "value1"; + trie.put(key1, value1); + + final String value2 = "value2"; + trie.put(key2, value2); + + assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); + assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); + } + + @Test + public void extendAndBranch() { + final BytesValue key1 = BytesValue.of(1, 5, 9); + final BytesValue key2 = BytesValue.of(1, 5, 2); + + final String value1 = "value1"; + trie.put(key1, value1); + + final String value2 = "value2"; + trie.put(key2, value2); + + assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); + assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); + assertFalse(trie.get(BytesValue.of(1, 4)).isPresent()); + } + + @Test + public void branchFromTopOfExtend() { + final BytesValue key1 = BytesValue.of(0xfe, 1); + final BytesValue key2 = BytesValue.of(0xfe, 2); + final BytesValue key3 = BytesValue.of(0xe1, 1); + + final String value1 = "value1"; + trie.put(key1, value1); + + final String value2 = "value2"; + trie.put(key2, value2); + + final String value3 = "value3"; + trie.put(key3, value3); + + assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); + assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); + assertThat(trie.get(key3)).isEqualTo(Optional.of(value3)); + assertFalse(trie.get(BytesValue.of(1, 4)).isPresent()); + assertFalse(trie.get(BytesValue.of(2, 4)).isPresent()); + assertFalse(trie.get(BytesValue.of(3)).isPresent()); + } + + @Test + public void splitBranchExtension() { + final BytesValue key1 = BytesValue.of(1, 5, 9); + final BytesValue key2 = BytesValue.of(1, 5, 2); + + final String value1 = "value1"; + trie.put(key1, value1); + + final String value2 = "value2"; + trie.put(key2, value2); + + final BytesValue key3 = BytesValue.of(1, 9, 1); + + final String value3 = "value3"; + trie.put(key3, value3); + + assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); + assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); + assertThat(trie.get(key3)).isEqualTo(Optional.of(value3)); + } + + @Test + public void replaceBranchChild() { + final BytesValue key1 = BytesValue.of(0); + final BytesValue key2 = BytesValue.of(1); + + final String value1 = "value1"; + trie.put(key1, value1); + final String value2 = "value2"; + trie.put(key2, value2); + + assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); + assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); + + final String value3 = "value3"; + trie.put(key1, value3); + + assertThat(trie.get(key1)).isEqualTo(Optional.of(value3)); + assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); + } + + @Test + public void inlineBranchInBranch() { + final BytesValue key1 = BytesValue.of(0); + final BytesValue key2 = BytesValue.of(1); + final BytesValue key3 = BytesValue.of(2); + final BytesValue key4 = BytesValue.of(0, 0); + final BytesValue key5 = BytesValue.of(0, 1); + + trie.put(key1, "value1"); + trie.put(key2, "value2"); + trie.put(key3, "value3"); + trie.put(key4, "value4"); + trie.put(key5, "value5"); + + trie.remove(key2); + trie.remove(key3); + + assertThat(trie.get(key1)).isEqualTo(Optional.of("value1")); + assertFalse(trie.get(key2).isPresent()); + assertFalse(trie.get(key3).isPresent()); + assertThat(trie.get(key4)).isEqualTo(Optional.of("value4")); + assertThat(trie.get(key5)).isEqualTo(Optional.of("value5")); + } + + @Test + public void removeNodeInBranchExtensionHasNoEffect() { + final BytesValue key1 = BytesValue.of(1, 5, 9); + final BytesValue key2 = BytesValue.of(1, 5, 2); + + final String value1 = "value1"; + trie.put(key1, value1); + + final String value2 = "value2"; + trie.put(key2, value2); + + final BytesValue hash = trie.getRootHash(); + + trie.remove(BytesValue.of(1, 4)); + assertThat(trie.getRootHash()).isEqualTo(hash); + } + + @Test + public void hashChangesWhenValueChanged() { + final BytesValue key1 = BytesValue.of(1, 5, 8, 9); + final BytesValue key2 = BytesValue.of(1, 6, 1, 2); + final BytesValue key3 = BytesValue.of(1, 6, 1, 3); + + final String value1 = "value1"; + trie.put(key1, value1); + final Bytes32 hash1 = trie.getRootHash(); + + final String value2 = "value2"; + trie.put(key2, value2); + final String value3 = "value3"; + trie.put(key3, value3); + final Bytes32 hash2 = trie.getRootHash(); + + assertThat(hash1).isNotEqualTo(hash2); + + final String value4 = "value4"; + trie.put(key1, value4); + final Bytes32 hash3 = trie.getRootHash(); + + assertThat(hash1).isNotEqualTo(hash3); + assertThat(hash2).isNotEqualTo(hash3); + + trie.put(key1, value1); + assertThat(trie.getRootHash()).isEqualTo(hash2); + + trie.remove(key2); + trie.remove(key3); + assertThat(trie.getRootHash()).isEqualTo(hash1); + } + + @Test + public void shouldRetrieveStoredExtensionWithInlinedChild() { + final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage(); + final MerkleStorage merkleStorage = new KeyValueMerkleStorage(keyValueStorage); + final StoredMerklePatriciaTrie trie = + new StoredMerklePatriciaTrie<>(merkleStorage::get, b -> b, b -> b); + + // Both of these can be inlined in its parent branch and the branch + // itself can be inlined into its parent extension. + trie.put(BytesValue.fromHexString("0x0400"), BytesValue.of(1)); + trie.put(BytesValue.fromHexString("0x0800"), BytesValue.of(2)); + trie.commit(merkleStorage::put); + + // Ensure the extension branch can be loaded correct with its inlined child. + final Bytes32 rootHash = trie.getRootHash(); + final StoredMerklePatriciaTrie newTrie = + new StoredMerklePatriciaTrie<>(merkleStorage::get, rootHash, b -> b, b -> b); + newTrie.get(BytesValue.fromHexString("0x0401")); + } + + @Test + public void shouldInlineNodesInParentAcrossModifications() { + // Misuse of StorageNode allowed inlineable trie nodes to end + // up being stored as a hash in its parent, which this would fail for. + final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage(); + final MerkleStorage merkleStorage = new KeyValueMerkleStorage(keyValueStorage); + final StoredMerklePatriciaTrie trie = + new StoredMerklePatriciaTrie<>(merkleStorage::get, b -> b, b -> b); + + // Both of these can be inlined in its parent branch. + trie.put(BytesValue.fromHexString("0x0400"), BytesValue.of(1)); + trie.put(BytesValue.fromHexString("0x0800"), BytesValue.of(2)); + trie.commit(merkleStorage::put); + + final Bytes32 rootHash = trie.getRootHash(); + final StoredMerklePatriciaTrie newTrie = + new StoredMerklePatriciaTrie<>(merkleStorage::get, rootHash, b -> b, b -> b); + + newTrie.put(BytesValue.fromHexString("0x0800"), BytesValue.of(3)); + newTrie.get(BytesValue.fromHexString("0x0401")); + trie.commit(merkleStorage::put); + + newTrie.get(BytesValue.fromHexString("0x0401")); + } + + @Test + public void getValueWithProof_emptyTrie() { + final BytesValue key1 = BytesValue.of(0xfe, 1); + + Proof valueWithProof = trie.getValueWithProof(key1); + assertThat(valueWithProof.getValue()).isEmpty(); + assertThat(valueWithProof.getProofRelatedNodes()).hasSize(0); + } + + @Test + public void getValueWithProof_forExistingValues() { + final BytesValue key1 = BytesValue.of(0xfe, 1); + final BytesValue key2 = BytesValue.of(0xfe, 2); + final BytesValue key3 = BytesValue.of(0xfe, 3); + + final String value1 = "value1"; + trie.put(key1, value1); + + final String value2 = "value2"; + trie.put(key2, value2); + + final String value3 = "value3"; + trie.put(key3, value3); + + final Proof valueWithProof = trie.getValueWithProof(key1); + assertThat(valueWithProof.getProofRelatedNodes()).hasSize(2); + assertThat(valueWithProof.getValue()).contains(value1); + + List> nodes = + TrieNodeDecoder.decodeNodes(valueWithProof.getProofRelatedNodes().get(1)); + + assertThat(new String(nodes.get(1).getValue().get().extractArray(), UTF_8)).isEqualTo(value1); + assertThat(new String(nodes.get(2).getValue().get().extractArray(), UTF_8)).isEqualTo(value2); + } + + @Test + public void getValueWithProof_forNonExistentValue() { + final BytesValue key1 = BytesValue.of(0xfe, 1); + final BytesValue key2 = BytesValue.of(0xfe, 2); + final BytesValue key3 = BytesValue.of(0xfe, 3); + final BytesValue key4 = BytesValue.of(0xfe, 4); + + final String value1 = "value1"; + trie.put(key1, value1); + + final String value2 = "value2"; + trie.put(key2, value2); + + final String value3 = "value3"; + trie.put(key3, value3); + + final Proof valueWithProof = trie.getValueWithProof(key4); + assertThat(valueWithProof.getValue()).isEmpty(); + assertThat(valueWithProof.getProofRelatedNodes()).hasSize(2); + } + + @Test + public void getValueWithProof_singleNodeTrie() { + final BytesValue key1 = BytesValue.of(0xfe, 1); + final String value1 = "1"; + trie.put(key1, value1); + + final Proof valueWithProof = trie.getValueWithProof(key1); + assertThat(valueWithProof.getValue()).contains(value1); + assertThat(valueWithProof.getProofRelatedNodes()).hasSize(1); + + List> nodes = + TrieNodeDecoder.decodeNodes(valueWithProof.getProofRelatedNodes().get(0)); + + assertThat(nodes.size()).isEqualTo(1); + final String nodeValue = new String(nodes.get(0).getValue().get().extractArray(), UTF_8); + assertThat(nodeValue).isEqualTo(value1); + } +} diff --git a/ethereum/trie/src/test/java/tech/pegasys/pantheon/ethereum/trie/SimpleMerklePatriciaTrieTest.java b/ethereum/trie/src/test/java/tech/pegasys/pantheon/ethereum/trie/SimpleMerklePatriciaTrieTest.java index 1ce7ff46d1..797f92a881 100644 --- a/ethereum/trie/src/test/java/tech/pegasys/pantheon/ethereum/trie/SimpleMerklePatriciaTrieTest.java +++ b/ethereum/trie/src/test/java/tech/pegasys/pantheon/ethereum/trie/SimpleMerklePatriciaTrieTest.java @@ -12,277 +12,15 @@ */ package tech.pegasys.pantheon.ethereum.trie; -import static junit.framework.TestCase.assertFalse; -import static org.assertj.core.api.Assertions.assertThat; - -import tech.pegasys.pantheon.util.bytes.Bytes32; import tech.pegasys.pantheon.util.bytes.BytesValue; import java.nio.charset.Charset; -import java.util.Optional; - -import org.junit.Before; -import org.junit.Test; - -public class SimpleMerklePatriciaTrieTest { - private SimpleMerklePatriciaTrie trie; - - @Before - public void setup() { - trie = - new SimpleMerklePatriciaTrie<>( - value -> - (value != null) ? BytesValue.wrap(value.getBytes(Charset.forName("UTF-8"))) : null); - } - - @Test - public void emptyTreeReturnsEmpty() { - assertFalse(trie.get(BytesValue.EMPTY).isPresent()); - } - - @Test - public void emptyTreeHasKnownRootHash() { - assertThat(trie.getRootHash().toString()) - .isEqualTo("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"); - } - - @Test(expected = NullPointerException.class) - public void throwsOnUpdateWithNull() { - trie.put(BytesValue.EMPTY, null); - } - - @Test - public void replaceSingleValue() { - final BytesValue key = BytesValue.of(1); - final String value1 = "value1"; - trie.put(key, value1); - assertThat(trie.get(key)).isEqualTo(Optional.of(value1)); - - final String value2 = "value2"; - trie.put(key, value2); - assertThat(trie.get(key)).isEqualTo(Optional.of(value2)); - } - - @Test - public void hashChangesWhenSingleValueReplaced() { - final BytesValue key = BytesValue.of(1); - final String value1 = "value1"; - trie.put(key, value1); - final Bytes32 hash1 = trie.getRootHash(); - - final String value2 = "value2"; - trie.put(key, value2); - final Bytes32 hash2 = trie.getRootHash(); - - assertThat(hash1).isNotEqualTo(hash2); - - trie.put(key, value1); - assertThat(trie.getRootHash()).isEqualTo(hash1); - } - - @Test - public void readPastLeaf() { - final BytesValue key1 = BytesValue.of(1); - trie.put(key1, "value"); - final BytesValue key2 = BytesValue.of(1, 3); - assertFalse(trie.get(key2).isPresent()); - } - - @Test - public void branchValue() { - final BytesValue key1 = BytesValue.of(1); - final BytesValue key2 = BytesValue.of(16); - - final String value1 = "value1"; - trie.put(key1, value1); - - final String value2 = "value2"; - trie.put(key2, value2); - - assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); - assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); - } - - @Test - public void readPastBranch() { - final BytesValue key1 = BytesValue.of(12); - final BytesValue key2 = BytesValue.of(12, 54); - - final String value1 = "value1"; - trie.put(key1, value1); - final String value2 = "value2"; - trie.put(key2, value2); - - final BytesValue key3 = BytesValue.of(3); - assertFalse(trie.get(key3).isPresent()); - } - - @Test - public void branchWithValue() { - final BytesValue key1 = BytesValue.of(5); - final BytesValue key2 = BytesValue.EMPTY; - - final String value1 = "value1"; - trie.put(key1, value1); - - final String value2 = "value2"; - trie.put(key2, value2); - - assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); - assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); - } - - @Test - public void extendAndBranch() { - final BytesValue key1 = BytesValue.of(1, 5, 9); - final BytesValue key2 = BytesValue.of(1, 5, 2); - - final String value1 = "value1"; - trie.put(key1, value1); - - final String value2 = "value2"; - trie.put(key2, value2); - - assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); - assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); - assertFalse(trie.get(BytesValue.of(1, 4)).isPresent()); - } - - @Test - public void branchFromTopOfExtend() { - final BytesValue key1 = BytesValue.of(0xfe, 1); - final BytesValue key2 = BytesValue.of(0xfe, 2); - final BytesValue key3 = BytesValue.of(0xe1, 1); - - final String value1 = "value1"; - trie.put(key1, value1); - - final String value2 = "value2"; - trie.put(key2, value2); - - final String value3 = "value3"; - trie.put(key3, value3); - - assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); - assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); - assertThat(trie.get(key3)).isEqualTo(Optional.of(value3)); - assertFalse(trie.get(BytesValue.of(1, 4)).isPresent()); - assertFalse(trie.get(BytesValue.of(2, 4)).isPresent()); - assertFalse(trie.get(BytesValue.of(3)).isPresent()); - } - - @Test - public void splitBranchExtension() { - final BytesValue key1 = BytesValue.of(1, 5, 9); - final BytesValue key2 = BytesValue.of(1, 5, 2); - - final String value1 = "value1"; - trie.put(key1, value1); - - final String value2 = "value2"; - trie.put(key2, value2); - - final BytesValue key3 = BytesValue.of(1, 9, 1); - - final String value3 = "value3"; - trie.put(key3, value3); - - assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); - assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); - assertThat(trie.get(key3)).isEqualTo(Optional.of(value3)); - } - - @Test - public void replaceBranchChild() { - final BytesValue key1 = BytesValue.of(0); - final BytesValue key2 = BytesValue.of(1); - - final String value1 = "value1"; - trie.put(key1, value1); - final String value2 = "value2"; - trie.put(key2, value2); - - assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); - assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); - - final String value3 = "value3"; - trie.put(key1, value3); - - assertThat(trie.get(key1)).isEqualTo(Optional.of(value3)); - assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); - } - - @Test - public void inlineBranchInBranch() { - final BytesValue key1 = BytesValue.of(0); - final BytesValue key2 = BytesValue.of(1); - final BytesValue key3 = BytesValue.of(2); - final BytesValue key4 = BytesValue.of(0, 0); - final BytesValue key5 = BytesValue.of(0, 1); - - trie.put(key1, "value1"); - trie.put(key2, "value2"); - trie.put(key3, "value3"); - trie.put(key4, "value4"); - trie.put(key5, "value5"); - - trie.remove(key2); - trie.remove(key3); - - assertThat(trie.get(key1)).isEqualTo(Optional.of("value1")); - assertFalse(trie.get(key2).isPresent()); - assertFalse(trie.get(key3).isPresent()); - assertThat(trie.get(key4)).isEqualTo(Optional.of("value4")); - assertThat(trie.get(key5)).isEqualTo(Optional.of("value5")); - } - - @Test - public void removeNodeInBranchExtensionHasNoEffect() { - final BytesValue key1 = BytesValue.of(1, 5, 9); - final BytesValue key2 = BytesValue.of(1, 5, 2); - - final String value1 = "value1"; - trie.put(key1, value1); - - final String value2 = "value2"; - trie.put(key2, value2); - - final Bytes32 hash = trie.getRootHash(); - - trie.remove(BytesValue.of(1, 4)); - assertThat(trie.getRootHash()).isEqualTo(hash); - } - - @Test - public void hashChangesWhenValueChanged() { - final BytesValue key1 = BytesValue.of(1, 5, 8, 9); - final BytesValue key2 = BytesValue.of(1, 6, 1, 2); - final BytesValue key3 = BytesValue.of(1, 6, 1, 3); - - final String value1 = "value1"; - trie.put(key1, value1); - final Bytes32 hash1 = trie.getRootHash(); - - final String value2 = "value2"; - trie.put(key2, value2); - final String value3 = "value3"; - trie.put(key3, value3); - final Bytes32 hash2 = trie.getRootHash(); - - assertThat(hash1).isNotEqualTo(hash2); - - final String value4 = "value4"; - trie.put(key1, value4); - final Bytes32 hash3 = trie.getRootHash(); - - assertThat(hash1).isNotEqualTo(hash3); - assertThat(hash2).isNotEqualTo(hash3); - - trie.put(key1, value1); - assertThat(trie.getRootHash()).isEqualTo(hash2); - trie.remove(key2); - trie.remove(key3); - assertThat(trie.getRootHash()).isEqualTo(hash1); +public class SimpleMerklePatriciaTrieTest extends AbstractMerklePatriciaTrieTest { + @Override + protected MerklePatriciaTrie createTrie() { + return new SimpleMerklePatriciaTrie<>( + value -> + (value != null) ? BytesValue.wrap(value.getBytes(Charset.forName("UTF-8"))) : null); } } diff --git a/ethereum/trie/src/test/java/tech/pegasys/pantheon/ethereum/trie/StoredMerklePatriciaTrieTest.java b/ethereum/trie/src/test/java/tech/pegasys/pantheon/ethereum/trie/StoredMerklePatriciaTrieTest.java index cb8932d8d8..b9c6c93acd 100644 --- a/ethereum/trie/src/test/java/tech/pegasys/pantheon/ethereum/trie/StoredMerklePatriciaTrieTest.java +++ b/ethereum/trie/src/test/java/tech/pegasys/pantheon/ethereum/trie/StoredMerklePatriciaTrieTest.java @@ -12,7 +12,6 @@ */ package tech.pegasys.pantheon.ethereum.trie; -import static junit.framework.TestCase.assertFalse; import static org.assertj.core.api.Assertions.assertThat; import tech.pegasys.pantheon.services.kvstore.InMemoryKeyValueStorage; @@ -24,275 +23,22 @@ import java.nio.charset.Charset; import java.util.Optional; import java.util.function.Function; -import org.junit.Before; import org.junit.Test; -public class StoredMerklePatriciaTrieTest { +public class StoredMerklePatriciaTrieTest extends AbstractMerklePatriciaTrieTest { private KeyValueStorage keyValueStore; private MerkleStorage merkleStorage; private Function valueSerializer; private Function valueDeserializer; - private StoredMerklePatriciaTrie trie; - @Before - public void setup() { + @Override + protected MerklePatriciaTrie createTrie() { keyValueStore = new InMemoryKeyValueStorage(); merkleStorage = new KeyValueMerkleStorage(keyValueStore); valueSerializer = value -> (value != null) ? BytesValue.wrap(value.getBytes(Charset.forName("UTF-8"))) : null; valueDeserializer = bytes -> new String(bytes.getArrayUnsafe(), Charset.forName("UTF-8")); - trie = new StoredMerklePatriciaTrie<>(merkleStorage::get, valueSerializer, valueDeserializer); - } - - @Test - public void emptyTreeReturnsEmpty() { - assertFalse(trie.get(BytesValue.EMPTY).isPresent()); - } - - @Test - public void emptyTreeHasKnownRootHash() { - assertThat(trie.getRootHash().toString()) - .isEqualTo("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"); - } - - @Test(expected = NullPointerException.class) - public void throwsOnUpdateWithNull() { - trie.put(BytesValue.EMPTY, null); - } - - @Test - public void replaceSingleValue() { - final BytesValue key = BytesValue.of(1); - final String value1 = "value1"; - trie.put(key, value1); - assertThat(trie.get(key)).isEqualTo(Optional.of(value1)); - - final String value2 = "value2"; - trie.put(key, value2); - assertThat(trie.get(key)).isEqualTo(Optional.of(value2)); - } - - @Test - public void hashChangesWhenSingleValueReplaced() { - final BytesValue key = BytesValue.of(1); - final String value1 = "value1"; - trie.put(key, value1); - final Bytes32 hash1 = trie.getRootHash(); - - final String value2 = "value2"; - trie.put(key, value2); - final Bytes32 hash2 = trie.getRootHash(); - - assertThat(hash1).isNotEqualTo(hash2); - - trie.put(key, value1); - assertThat(trie.getRootHash()).isEqualTo(hash1); - } - - @Test - public void readPastLeaf() { - final BytesValue key1 = BytesValue.of(1); - trie.put(key1, "value"); - final BytesValue key2 = BytesValue.of(1, 3); - assertFalse(trie.get(key2).isPresent()); - } - - @Test - public void branchValue() { - final BytesValue key1 = BytesValue.of(1); - final BytesValue key2 = BytesValue.of(16); - - final String value1 = "value1"; - trie.put(key1, value1); - - final String value2 = "value2"; - trie.put(key2, value2); - - assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); - assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); - } - - @Test - public void readPastBranch() { - final BytesValue key1 = BytesValue.of(12); - final BytesValue key2 = BytesValue.of(12, 54); - - final String value1 = "value1"; - trie.put(key1, value1); - final String value2 = "value2"; - trie.put(key2, value2); - - final BytesValue key3 = BytesValue.of(3); - assertFalse(trie.get(key3).isPresent()); - } - - @Test - public void branchWithValue() { - final BytesValue key1 = BytesValue.of(5); - final BytesValue key2 = BytesValue.EMPTY; - - final String value1 = "value1"; - trie.put(key1, value1); - - final String value2 = "value2"; - trie.put(key2, value2); - - assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); - assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); - } - - @Test - public void extendAndBranch() { - final BytesValue key1 = BytesValue.of(1, 5, 9); - final BytesValue key2 = BytesValue.of(1, 5, 2); - - final String value1 = "value1"; - trie.put(key1, value1); - - final String value2 = "value2"; - trie.put(key2, value2); - - assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); - assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); - assertFalse(trie.get(BytesValue.of(1, 4)).isPresent()); - } - - @Test - public void branchFromTopOfExtend() { - final BytesValue key1 = BytesValue.of(0xfe, 1); - final BytesValue key2 = BytesValue.of(0xfe, 2); - final BytesValue key3 = BytesValue.of(0xe1, 1); - - final String value1 = "value1"; - trie.put(key1, value1); - - final String value2 = "value2"; - trie.put(key2, value2); - - final String value3 = "value3"; - trie.put(key3, value3); - - assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); - assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); - assertThat(trie.get(key3)).isEqualTo(Optional.of(value3)); - assertFalse(trie.get(BytesValue.of(1, 4)).isPresent()); - assertFalse(trie.get(BytesValue.of(2, 4)).isPresent()); - assertFalse(trie.get(BytesValue.of(3)).isPresent()); - } - - @Test - public void splitBranchExtension() { - final BytesValue key1 = BytesValue.of(1, 5, 9); - final BytesValue key2 = BytesValue.of(1, 5, 2); - - final String value1 = "value1"; - trie.put(key1, value1); - - final String value2 = "value2"; - trie.put(key2, value2); - - final BytesValue key3 = BytesValue.of(1, 9, 1); - - final String value3 = "value3"; - trie.put(key3, value3); - - assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); - assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); - assertThat(trie.get(key3)).isEqualTo(Optional.of(value3)); - } - - @Test - public void replaceBranchChild() { - final BytesValue key1 = BytesValue.of(0); - final BytesValue key2 = BytesValue.of(1); - - final String value1 = "value1"; - trie.put(key1, value1); - final String value2 = "value2"; - trie.put(key2, value2); - - assertThat(trie.get(key1)).isEqualTo(Optional.of(value1)); - assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); - - final String value3 = "value3"; - trie.put(key1, value3); - - assertThat(trie.get(key1)).isEqualTo(Optional.of(value3)); - assertThat(trie.get(key2)).isEqualTo(Optional.of(value2)); - } - - @Test - public void inlineBranchInBranch() { - final BytesValue key1 = BytesValue.of(0); - final BytesValue key2 = BytesValue.of(1); - final BytesValue key3 = BytesValue.of(2); - final BytesValue key4 = BytesValue.of(0, 0); - final BytesValue key5 = BytesValue.of(0, 1); - - trie.put(key1, "value1"); - trie.put(key2, "value2"); - trie.put(key3, "value3"); - trie.put(key4, "value4"); - trie.put(key5, "value5"); - - trie.remove(key2); - trie.remove(key3); - - assertThat(trie.get(key1)).isEqualTo(Optional.of("value1")); - assertFalse(trie.get(key2).isPresent()); - assertFalse(trie.get(key3).isPresent()); - assertThat(trie.get(key4)).isEqualTo(Optional.of("value4")); - assertThat(trie.get(key5)).isEqualTo(Optional.of("value5")); - } - - @Test - public void removeNodeInBranchExtensionHasNoEffect() { - final BytesValue key1 = BytesValue.of(1, 5, 9); - final BytesValue key2 = BytesValue.of(1, 5, 2); - - final String value1 = "value1"; - trie.put(key1, value1); - - final String value2 = "value2"; - trie.put(key2, value2); - - final BytesValue hash = trie.getRootHash(); - - trie.remove(BytesValue.of(1, 4)); - assertThat(trie.getRootHash()).isEqualTo(hash); - } - - @Test - public void hashChangesWhenValueChanged() { - final BytesValue key1 = BytesValue.of(1, 5, 8, 9); - final BytesValue key2 = BytesValue.of(1, 6, 1, 2); - final BytesValue key3 = BytesValue.of(1, 6, 1, 3); - - final String value1 = "value1"; - trie.put(key1, value1); - final Bytes32 hash1 = trie.getRootHash(); - - final String value2 = "value2"; - trie.put(key2, value2); - final String value3 = "value3"; - trie.put(key3, value3); - final Bytes32 hash2 = trie.getRootHash(); - - assertThat(hash1).isNotEqualTo(hash2); - - final String value4 = "value4"; - trie.put(key1, value4); - final Bytes32 hash3 = trie.getRootHash(); - - assertThat(hash1).isNotEqualTo(hash3); - assertThat(hash2).isNotEqualTo(hash3); - - trie.put(key1, value1); - assertThat(trie.getRootHash()).isEqualTo(hash2); - - trie.remove(key2); - trie.remove(key3); - assertThat(trie.getRootHash()).isEqualTo(hash1); + return new StoredMerklePatriciaTrie<>(merkleStorage::get, valueSerializer, valueDeserializer); } @Test @@ -372,49 +118,4 @@ public class StoredMerklePatriciaTrieTest { assertThat(trie.get(key2)).isEqualTo(Optional.of("value2")); assertThat(trie.get(key3)).isEqualTo(Optional.of("value3")); } - - @Test - public void shouldRetrieveStoredExtensionWithInlinedChild() { - final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage(); - final MerkleStorage merkleStorage = new KeyValueMerkleStorage(keyValueStorage); - final StoredMerklePatriciaTrie trie = - new StoredMerklePatriciaTrie<>(merkleStorage::get, b -> b, b -> b); - - // Both of these can be inlined in its parent branch and the branch - // itself can be inlined into its parent extension. - trie.put(BytesValue.fromHexString("0x0400"), BytesValue.of(1)); - trie.put(BytesValue.fromHexString("0x0800"), BytesValue.of(2)); - trie.commit(merkleStorage::put); - - // Ensure the extension branch can be loaded correct with its inlined child. - final Bytes32 rootHash = trie.getRootHash(); - final StoredMerklePatriciaTrie newTrie = - new StoredMerklePatriciaTrie<>(merkleStorage::get, rootHash, b -> b, b -> b); - newTrie.get(BytesValue.fromHexString("0x0401")); - } - - @Test - public void shouldInlineNodesInParentAcrossModifications() { - // Misuse of StorageNode allowed inlineable trie nodes to end - // up being stored as a hash in its parent, which this would fail for. - final KeyValueStorage keyValueStorage = new InMemoryKeyValueStorage(); - final MerkleStorage merkleStorage = new KeyValueMerkleStorage(keyValueStorage); - final StoredMerklePatriciaTrie trie = - new StoredMerklePatriciaTrie<>(merkleStorage::get, b -> b, b -> b); - - // Both of these can be inlined in its parent branch. - trie.put(BytesValue.fromHexString("0x0400"), BytesValue.of(1)); - trie.put(BytesValue.fromHexString("0x0800"), BytesValue.of(2)); - trie.commit(merkleStorage::put); - - final Bytes32 rootHash = trie.getRootHash(); - final StoredMerklePatriciaTrie newTrie = - new StoredMerklePatriciaTrie<>(merkleStorage::get, rootHash, b -> b, b -> b); - - newTrie.put(BytesValue.fromHexString("0x0800"), BytesValue.of(3)); - newTrie.get(BytesValue.fromHexString("0x0401")); - trie.commit(merkleStorage::put); - - newTrie.get(BytesValue.fromHexString("0x0401")); - } }