mirror of https://github.com/hyperledger/besu
Implement eth_getproof JSON RPC API (#1824)
Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>pull/2/head
parent
8787a4c006
commit
a5daeba71d
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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(); |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue