Implement eth_getproof JSON RPC API (#1824)

Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>
pull/2/head
Karim T 5 years ago committed by mbaxter
parent 8787a4c006
commit a5daeba71d
  1. 68
      ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/proof/WorldStateProof.java
  2. 84
      ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/proof/WorldStateProofProvider.java
  3. 15
      ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/worldstate/WorldStateArchive.java
  4. 153
      ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/proof/WorldStateProofProviderTest.java
  5. 2
      ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcMethodsFactory.java
  6. 1
      ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/RpcMethod.java
  7. 95
      ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/EthGetProof.java
  8. 6
      ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/response/JsonRpcError.java
  9. 122
      ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/results/proof/GetProofResult.java
  10. 53
      ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/results/proof/StorageEntryProof.java
  11. 211
      ethereum/jsonrpc/src/test/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/EthGetProofTest.java
  12. 8
      ethereum/trie/src/main/java/tech/pegasys/pantheon/ethereum/trie/MerklePatriciaTrie.java
  13. 38
      ethereum/trie/src/main/java/tech/pegasys/pantheon/ethereum/trie/Proof.java
  14. 61
      ethereum/trie/src/main/java/tech/pegasys/pantheon/ethereum/trie/ProofVisitor.java
  15. 12
      ethereum/trie/src/main/java/tech/pegasys/pantheon/ethereum/trie/SimpleMerklePatriciaTrie.java
  16. 12
      ethereum/trie/src/main/java/tech/pegasys/pantheon/ethereum/trie/StoredMerklePatriciaTrie.java
  17. 409
      ethereum/trie/src/test/java/tech/pegasys/pantheon/ethereum/trie/AbstractMerklePatriciaTrieTest.java
  18. 270
      ethereum/trie/src/test/java/tech/pegasys/pantheon/ethereum/trie/SimpleMerklePatriciaTrieTest.java
  19. 307
      ethereum/trie/src/test/java/tech/pegasys/pantheon/ethereum/trie/StoredMerklePatriciaTrieTest.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<BytesValue> accountProof;
private final Map<UInt256, Proof<BytesValue>> storageProofs;
public WorldStateProof(
final StateTrieAccountValue stateTrieAccountValue,
final Proof<BytesValue> accountProof,
final SortedMap<UInt256, Proof<BytesValue>> storageProofs) {
this.stateTrieAccountValue = stateTrieAccountValue;
this.accountProof = accountProof;
this.storageProofs = storageProofs;
}
public StateTrieAccountValue getStateTrieAccountValue() {
return stateTrieAccountValue;
}
public List<BytesValue> getAccountProof() {
return accountProof.getProofRelatedNodes();
}
public List<UInt256> getStorageKeys() {
return new ArrayList<>(storageProofs.keySet());
}
public UInt256 getStorageValue(final UInt256 key) {
Optional<BytesValue> value = storageProofs.get(key).getValue();
if (value.isEmpty()) {
return UInt256.ZERO;
} else {
return RLP.input(value.get()).readUInt256Scalar();
}
}
public List<BytesValue> getStorageProof(final UInt256 key) {
return storageProofs.get(key).getProofRelatedNodes();
}
}

@ -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<WorldStateProof> getAccountProof(
final Hash worldStateRoot,
final Address accountAddress,
final List<UInt256> accountStorageKeys) {
if (!worldStateStorage.isWorldStateAvailable(worldStateRoot)) {
return Optional.empty();
} else {
final Hash addressHash = Hash.hash(accountAddress);
final Proof<BytesValue> accountProof =
newAccountStateTrie(worldStateRoot).getValueWithProof(addressHash);
return accountProof
.getValue()
.map(RLP::input)
.map(StateTrieAccountValue::readFrom)
.map(
account -> {
final SortedMap<UInt256, Proof<BytesValue>> storageProofs =
getStorageProofs(account, accountStorageKeys);
return new WorldStateProof(account, accountProof, storageProofs);
});
}
}
private SortedMap<UInt256, Proof<BytesValue>> getStorageProofs(
final StateTrieAccountValue account, final List<UInt256> accountStorageKeys) {
final MerklePatriciaTrie<Bytes32, BytesValue> storageTrie =
newAccountStorageTrie(account.getStorageRoot());
final SortedMap<UInt256, Proof<BytesValue>> storageProofs = new TreeMap<>();
accountStorageKeys.forEach(
key -> storageProofs.put(key, storageTrie.getValueWithProof(Hash.hash(key.getBytes()))));
return storageProofs;
}
private MerklePatriciaTrie<Bytes32, BytesValue> newAccountStateTrie(final Bytes32 rootHash) {
return new StoredMerklePatriciaTrie<>(
worldStateStorage::getAccountStateTrieNode, rootHash, b -> b, b -> b);
}
private MerklePatriciaTrie<Bytes32, BytesValue> newAccountStorageTrie(final Bytes32 rootHash) {
return new StoredMerklePatriciaTrie<>(
worldStateStorage::getAccountStorageTrieNode, rootHash, b -> b, b -> b);
}
}

@ -12,23 +12,31 @@
*/ */
package tech.pegasys.pantheon.ethereum.worldstate; 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.Hash;
import tech.pegasys.pantheon.ethereum.core.MutableWorldState; import tech.pegasys.pantheon.ethereum.core.MutableWorldState;
import tech.pegasys.pantheon.ethereum.core.WorldState; 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.ethereum.trie.MerklePatriciaTrie;
import tech.pegasys.pantheon.util.bytes.BytesValue; 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.Optional;
public class WorldStateArchive { public class WorldStateArchive {
private final WorldStateStorage worldStateStorage; private final WorldStateStorage worldStateStorage;
private final WorldStatePreimageStorage preimageStorage; private final WorldStatePreimageStorage preimageStorage;
private final WorldStateProofProvider worldStateProof;
private static final Hash EMPTY_ROOT_HASH = Hash.wrap(MerklePatriciaTrie.EMPTY_TRIE_NODE_HASH); private static final Hash EMPTY_ROOT_HASH = Hash.wrap(MerklePatriciaTrie.EMPTY_TRIE_NODE_HASH);
public WorldStateArchive( public WorldStateArchive(
final WorldStateStorage worldStateStorage, final WorldStatePreimageStorage preimageStorage) { final WorldStateStorage worldStateStorage, final WorldStatePreimageStorage preimageStorage) {
this.worldStateStorage = worldStateStorage; this.worldStateStorage = worldStateStorage;
this.preimageStorage = preimageStorage; this.preimageStorage = preimageStorage;
this.worldStateProof = new WorldStateProofProvider(worldStateStorage);
} }
public Optional<WorldState> get(final Hash rootHash) { public Optional<WorldState> get(final Hash rootHash) {
@ -61,4 +69,11 @@ public class WorldStateArchive {
public WorldStateStorage getWorldStateStorage() { public WorldStateStorage getWorldStateStorage() {
return worldStateStorage; return worldStateStorage;
} }
public Optional<WorldStateProof> getAccountProof(
final Hash worldStateRoot,
final Address accountAddress,
final List<UInt256> accountStorageKeys) {
return worldStateProof.getAccountProof(worldStateRoot, accountAddress, accountStorageKeys);
}
} }

@ -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<WorldStateProof> accountProof =
worldStateProofProvider.getAccountProof(Hash.EMPTY, address, new ArrayList<>());
assertThat(accountProof).isEmpty();
}
@Test
public void getProofWhenWorldStateAvailable() {
final MerklePatriciaTrie<Bytes32, BytesValue> worldStateTrie = emptyWorldStateTrie();
final MerklePatriciaTrie<Bytes32, BytesValue> 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<UInt256> storageKeys = Arrays.asList(UInt256.of(1L), UInt256.of(3L), UInt256.of(6L));
final Optional<WorldStateProof> 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<Bytes32, BytesValue> worldStateTrie = emptyWorldStateTrie();
final Optional<WorldStateProof> accountProof =
worldStateProofProvider.getAccountProof(
Hash.wrap(worldStateTrie.getRootHash()), address, new ArrayList<>());
assertThat(accountProof).isEmpty();
}
private void writeStorageValue(
final MerklePatriciaTrie<Bytes32, BytesValue> 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<Bytes32, BytesValue> emptyStorageTrie() {
return new StoredMerklePatriciaTrie<>(
worldStateStorage::getAccountStateTrieNode, b -> b, b -> b);
}
private MerklePatriciaTrie<Bytes32, BytesValue> emptyWorldStateTrie() {
return new StoredMerklePatriciaTrie<>(
worldStateStorage::getAccountStorageTrieNode, b -> b, b -> b);
}
}

@ -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.EthGetFilterChanges;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.EthGetFilterLogs; 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.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.EthGetStorageAt;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.EthGetTransactionByBlockHashAndIndex; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.EthGetTransactionByBlockHashAndIndex;
import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.EthGetTransactionByBlockNumberAndIndex; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.EthGetTransactionByBlockNumberAndIndex;
@ -217,6 +218,7 @@ public class JsonRpcMethodsFactory {
parameter), parameter),
new EthGetCode(blockchainQueries, parameter), new EthGetCode(blockchainQueries, parameter),
new EthGetLogs(blockchainQueries, parameter), new EthGetLogs(blockchainQueries, parameter),
new EthGetProof(blockchainQueries, parameter),
new EthGetUncleCountByBlockHash(blockchainQueries, parameter), new EthGetUncleCountByBlockHash(blockchainQueries, parameter),
new EthGetUncleCountByBlockNumber(blockchainQueries, parameter), new EthGetUncleCountByBlockNumber(blockchainQueries, parameter),
new EthGetUncleByBlockNumberAndIndex(blockchainQueries, parameter), new EthGetUncleByBlockNumberAndIndex(blockchainQueries, parameter),

@ -57,6 +57,7 @@ public enum RpcMethod {
ETH_GET_FILTER_CHANGES("eth_getFilterChanges"), ETH_GET_FILTER_CHANGES("eth_getFilterChanges"),
ETH_GET_FILTER_LOGS("eth_getFilterLogs"), ETH_GET_FILTER_LOGS("eth_getFilterLogs"),
ETH_GET_LOGS("eth_getLogs"), ETH_GET_LOGS("eth_getLogs"),
ETH_GET_PROOF("eth_getProof"),
ETH_GET_STORAGE_AT("eth_getStorageAt"), ETH_GET_STORAGE_AT("eth_getStorageAt"),
ETH_GET_TRANSACTION_BY_BLOCK_HASH_AND_INDEX("eth_getTransactionByBlockHashAndIndex"), ETH_GET_TRANSACTION_BY_BLOCK_HASH_AND_INDEX("eth_getTransactionByBlockHashAndIndex"),
ETH_GET_TRANSACTION_BY_BLOCK_NUMBER_AND_INDEX("eth_getTransactionByBlockNumberAndIndex"), ETH_GET_TRANSACTION_BY_BLOCK_NUMBER_AND_INDEX("eth_getTransactionByBlockNumberAndIndex"),

@ -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<UInt256> 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<UInt256> storageKeys = getStorageKeys(request);
final Optional<MutableWorldState> worldState = blockchain.getWorldState(blockNumber);
if (worldState.isPresent()) {
Optional<WorldStateProof> 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();
}
}

@ -60,6 +60,12 @@ public enum JsonRpcError {
// Wallet errors // Wallet errors
COINBASE_NOT_SPECIFIED(-32000, "Coinbase must be explicitly specified"), 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 // Debug failures
PARENT_BLOCK_NOT_FOUND(-32000, "Parent block not found"), PARENT_BLOCK_NOT_FOUND(-32000, "Parent block not found"),

@ -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<BytesValue> accountProof;
private final Address address;
private final Wei balance;
private final Bytes32 codeHash;
private final long nonce;
private final Bytes32 storageHash;
private final List<StorageEntryProof> storageEntries;
public GetProofResult(
final Address address,
final Wei balance,
final Bytes32 codeHash,
final long nonce,
final Bytes32 storageHash,
final List<BytesValue> accountProof,
final List<StorageEntryProof> 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<StorageEntryProof> 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<String> getAccountProof() {
return accountProof.stream().map(BytesValue::toString).collect(Collectors.toList());
}
@JsonGetter(value = "storageProof")
public List<StorageEntryProof> getStorageProof() {
return storageEntries;
}
}

@ -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<BytesValue> storageProof;
public StorageEntryProof(
final UInt256 key, final UInt256 value, final List<BytesValue> 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<String> getStorageProof() {
return storageProof.stream().map(BytesValue::toString).collect(Collectors.toList());
}
}

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

@ -36,6 +36,14 @@ public interface MerklePatriciaTrie<K, V> {
*/ */
Optional<V> get(K key); Optional<V> 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<V> getValueWithProof(K key);
/** /**
* Updates the value mapped to the specified key, creating the mapping if one does not already * Updates the value mapped to the specified key, creating the mapping if one does not already
* exist. * exist.

@ -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<V> {
private final Optional<V> value;
private final List<BytesValue> proofRelatedNodes;
public Proof(final Optional<V> value, final List<BytesValue> proofRelatedNodes) {
this.value = value;
this.proofRelatedNodes = proofRelatedNodes;
}
public Optional<V> getValue() {
return value;
}
public List<BytesValue> getProofRelatedNodes() {
return proofRelatedNodes;
}
}

@ -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<V> extends GetVisitor<V> implements PathNodeVisitor<V> {
private final Node<V> rootNode;
private final List<Node<V>> proof = new ArrayList<>();
ProofVisitor(final Node<V> rootNode) {
this.rootNode = rootNode;
}
@Override
public Node<V> visit(final ExtensionNode<V> extensionNode, final BytesValue path) {
maybeTrackNode(extensionNode);
return super.visit(extensionNode, path);
}
@Override
public Node<V> visit(final BranchNode<V> branchNode, final BytesValue path) {
maybeTrackNode(branchNode);
return super.visit(branchNode, path);
}
@Override
public Node<V> visit(final LeafNode<V> leafNode, final BytesValue path) {
maybeTrackNode(leafNode);
return super.visit(leafNode, path);
}
@Override
public Node<V> visit(final NullNode<V> nullNode, final BytesValue path) {
return super.visit(nullNode, path);
}
public List<Node<V>> getProof() {
return proof;
}
private void maybeTrackNode(final Node<V> node) {
if (node.equals(rootNode) || node.isReferencedByHash()) {
proof.add(node);
}
}
}

@ -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.Bytes32;
import tech.pegasys.pantheon.util.bytes.BytesValue; import tech.pegasys.pantheon.util.bytes.BytesValue;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors;
/** /**
* An in-memory {@link MerklePatriciaTrie}. * An in-memory {@link MerklePatriciaTrie}.
@ -51,6 +53,16 @@ public class SimpleMerklePatriciaTrie<K extends BytesValue, V> implements Merkle
return root.accept(getVisitor, bytesToPath(key)).getValue(); return root.accept(getVisitor, bytesToPath(key)).getValue();
} }
@Override
public Proof<V> getValueWithProof(final K key) {
checkNotNull(key);
final ProofVisitor<V> proofVisitor = new ProofVisitor<>(root);
final Optional<V> value = root.accept(proofVisitor, bytesToPath(key)).getValue();
final List<BytesValue> proof =
proofVisitor.getProof().stream().map(Node::getRlp).collect(Collectors.toList());
return new Proof<>(value, proof);
}
@Override @Override
public void put(final K key, final V value) { public void put(final K key, final V value) {
checkNotNull(key); checkNotNull(key);

@ -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.Bytes32;
import tech.pegasys.pantheon.util.bytes.BytesValue; import tech.pegasys.pantheon.util.bytes.BytesValue;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors;
/** /**
* A {@link MerklePatriciaTrie} that persists trie nodes to a {@link MerkleStorage} key/value store. * A {@link MerklePatriciaTrie} that persists trie nodes to a {@link MerkleStorage} key/value store.
@ -76,6 +78,16 @@ public class StoredMerklePatriciaTrie<K extends BytesValue, V> implements Merkle
return root.accept(getVisitor, bytesToPath(key)).getValue(); return root.accept(getVisitor, bytesToPath(key)).getValue();
} }
@Override
public Proof<V> getValueWithProof(final K key) {
checkNotNull(key);
final ProofVisitor<V> proofVisitor = new ProofVisitor<>(root);
final Optional<V> value = root.accept(proofVisitor, bytesToPath(key)).getValue();
final List<BytesValue> proof =
proofVisitor.getProof().stream().map(Node::getRlp).collect(Collectors.toList());
return new Proof<>(value, proof);
}
@Override @Override
public void put(final K key, final V value) { public void put(final K key, final V value) {
checkNotNull(key); checkNotNull(key);

@ -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<BytesValue, String> trie;
@Before
public void setup() {
trie = createTrie();
}
protected abstract MerklePatriciaTrie<BytesValue, String> 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<BytesValue, BytesValue> 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<BytesValue, BytesValue> 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<BytesValue, BytesValue> 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<BytesValue, BytesValue> 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<String> 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<String> valueWithProof = trie.getValueWithProof(key1);
assertThat(valueWithProof.getProofRelatedNodes()).hasSize(2);
assertThat(valueWithProof.getValue()).contains(value1);
List<Node<BytesValue>> 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<String> 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<String> valueWithProof = trie.getValueWithProof(key1);
assertThat(valueWithProof.getValue()).contains(value1);
assertThat(valueWithProof.getProofRelatedNodes()).hasSize(1);
List<Node<BytesValue>> 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);
}
}

@ -12,277 +12,15 @@
*/ */
package tech.pegasys.pantheon.ethereum.trie; 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 tech.pegasys.pantheon.util.bytes.BytesValue;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.Optional;
import org.junit.Before;
import org.junit.Test;
public class SimpleMerklePatriciaTrieTest {
private SimpleMerklePatriciaTrie<BytesValue, String> trie;
@Before public class SimpleMerklePatriciaTrieTest extends AbstractMerklePatriciaTrieTest {
public void setup() { @Override
trie = protected MerklePatriciaTrie<BytesValue, String> createTrie() {
new SimpleMerklePatriciaTrie<>( return new SimpleMerklePatriciaTrie<>(
value -> value ->
(value != null) ? BytesValue.wrap(value.getBytes(Charset.forName("UTF-8"))) : null); (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);
}
} }

@ -12,7 +12,6 @@
*/ */
package tech.pegasys.pantheon.ethereum.trie; package tech.pegasys.pantheon.ethereum.trie;
import static junit.framework.TestCase.assertFalse;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import tech.pegasys.pantheon.services.kvstore.InMemoryKeyValueStorage; import tech.pegasys.pantheon.services.kvstore.InMemoryKeyValueStorage;
@ -24,275 +23,22 @@ import java.nio.charset.Charset;
import java.util.Optional; import java.util.Optional;
import java.util.function.Function; import java.util.function.Function;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
public class StoredMerklePatriciaTrieTest { public class StoredMerklePatriciaTrieTest extends AbstractMerklePatriciaTrieTest {
private KeyValueStorage keyValueStore; private KeyValueStorage keyValueStore;
private MerkleStorage merkleStorage; private MerkleStorage merkleStorage;
private Function<String, BytesValue> valueSerializer; private Function<String, BytesValue> valueSerializer;
private Function<BytesValue, String> valueDeserializer; private Function<BytesValue, String> valueDeserializer;
private StoredMerklePatriciaTrie<BytesValue, String> trie;
@Before @Override
public void setup() { protected MerklePatriciaTrie<BytesValue, String> createTrie() {
keyValueStore = new InMemoryKeyValueStorage(); keyValueStore = new InMemoryKeyValueStorage();
merkleStorage = new KeyValueMerkleStorage(keyValueStore); merkleStorage = new KeyValueMerkleStorage(keyValueStore);
valueSerializer = valueSerializer =
value -> (value != null) ? BytesValue.wrap(value.getBytes(Charset.forName("UTF-8"))) : null; value -> (value != null) ? BytesValue.wrap(value.getBytes(Charset.forName("UTF-8"))) : null;
valueDeserializer = bytes -> new String(bytes.getArrayUnsafe(), Charset.forName("UTF-8")); valueDeserializer = bytes -> new String(bytes.getArrayUnsafe(), Charset.forName("UTF-8"));
trie = new StoredMerklePatriciaTrie<>(merkleStorage::get, valueSerializer, valueDeserializer); return 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);
} }
@Test @Test
@ -372,49 +118,4 @@ public class StoredMerklePatriciaTrieTest {
assertThat(trie.get(key2)).isEqualTo(Optional.of("value2")); assertThat(trie.get(key2)).isEqualTo(Optional.of("value2"));
assertThat(trie.get(key3)).isEqualTo(Optional.of("value3")); 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<BytesValue, BytesValue> 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<BytesValue, BytesValue> 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<BytesValue, BytesValue> 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<BytesValue, BytesValue> 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"));
}
} }

Loading…
Cancel
Save