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