Skip to content

Commit

Permalink
initial test coverage for snap server account range
Browse files Browse the repository at this point in the history
Signed-off-by: garyschulte <[email protected]>
  • Loading branch information
garyschulte committed Sep 22, 2023
1 parent fc46556 commit ba392d5
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.hyperledger.besu.ethereum.trie.MerkleTrie;
import org.hyperledger.besu.ethereum.worldstate.DataStorageFormat;
import org.hyperledger.besu.ethereum.worldstate.FlatDbMode;
import org.hyperledger.besu.ethereum.worldstate.FlatWorldStateStorage;
import org.hyperledger.besu.ethereum.worldstate.StateTrieAccountValue;
import org.hyperledger.besu.ethereum.worldstate.WorldStateStorage;
import org.hyperledger.besu.evm.account.AccountStorageEntry;
Expand All @@ -53,7 +54,7 @@
import org.slf4j.LoggerFactory;

@SuppressWarnings("unused")
public class BonsaiWorldStateKeyValueStorage implements WorldStateStorage, AutoCloseable {
public class BonsaiWorldStateKeyValueStorage implements WorldStateStorage, FlatWorldStateStorage, AutoCloseable {
private static final Logger LOG = LoggerFactory.getLogger(BonsaiWorldStateKeyValueStorage.class);

// 0x776f726c64526f6f74
Expand Down Expand Up @@ -131,6 +132,7 @@ public FlatDbMode deriveFlatDbStrategy() {
return flatDbMode;
}

@Override
public FlatDbStrategy getFlatDbStrategy() {
if (flatDbStrategy == null) {
loadFlatDbStrategy();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright Hyperledger Besu Contributors.
*
* 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.worldstate;

import org.hyperledger.besu.ethereum.bonsai.storage.flat.FlatDbStrategy;

public interface FlatWorldStateStorage {

FlatDbMode getFlatDbMode();

FlatDbStrategy getFlatDbStrategy();

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@
*/
package org.hyperledger.besu.ethereum.eth.manager.snap;

import kotlin.collections.ArrayDeque;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import org.apache.tuweni.units.bigints.UInt256;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.ethereum.bonsai.storage.flat.FlatDbReaderStrategy;
import org.hyperledger.besu.ethereum.eth.manager.EthMessages;
import org.hyperledger.besu.ethereum.eth.messages.snap.AccountRangeMessage;
import org.hyperledger.besu.ethereum.eth.messages.snap.ByteCodesMessage;
Expand All @@ -27,33 +30,39 @@
import org.hyperledger.besu.ethereum.proof.WorldStateProofProvider;
import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive;
import org.hyperledger.besu.ethereum.worldstate.WorldStateStorage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.NavigableMap;
import java.util.Optional;
import java.util.function.Function;

import kotlin.collections.ArrayDeque;
import org.apache.tuweni.bytes.Bytes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** TODO: See https://github.com/ethereum/devp2p/blob/master/caps/snap.md */
/** See https://github.com/ethereum/devp2p/blob/master/caps/snap.md */
@SuppressWarnings("unused")
class SnapServer {
private static final Logger LOGGER = LoggerFactory.getLogger(SnapServer.class);
private static final int MAX_ENTRIES_PER_REQUEST = 100000;

private static final int MAX_RESPONSE_SIZE = 2 * 1024 * 1024;
private static final AccountRangeMessage EMPTY_ACCOUNT_RANGE = AccountRangeMessage.create(
new HashMap<>(), new ArrayDeque<>());

private final EthMessages snapMessages;
private final FlatDbReaderStrategy flatDbStrategy;
private final Function<Hash, WorldStateStorage> worldStateStorageProvider;
private final Function<Hash, Optional<WorldStateStorage>> worldStateStorageProvider;

SnapServer(final EthMessages snapMessages, final WorldStateArchive archive) {

Check notice

Code scanning / CodeQL

Useless parameter Note

The parameter 'archive' is never used.
this.snapMessages = snapMessages;
// TODO get these from worldstate archive:
this.flatDbStrategy = null;
// TODO implement worldstate storage retrieval by root hash in WorldStateArchive:
this.worldStateStorageProvider = __ -> null;
}

SnapServer(
final EthMessages snapMessages,
final Function<Hash, Optional<WorldStateStorage>> worldStateStorageProvider) {
this.snapMessages = snapMessages;
this.worldStateStorageProvider = worldStateStorageProvider;
}

private void registerResponseConstructors() {
snapMessages.registerResponseConstructor(
SnapV1.GET_ACCOUNT_RANGE, messageData -> constructGetAccountRangeResponse(messageData));
Expand All @@ -65,42 +74,41 @@ private void registerResponseConstructors() {
SnapV1.GET_TRIE_NODES, messageData -> constructGetTrieNodesResponse(messageData));
}

private MessageData constructGetAccountRangeResponse(final MessageData message) {
if (flatDbStrategy == null) {
// TODO, remove this empty fallback
return AccountRangeMessage.create(new HashMap<>(), new ArrayDeque<>());
}

MessageData constructGetAccountRangeResponse(final MessageData message) {
final GetAccountRangeMessage getAccountRangeMessage = GetAccountRangeMessage.readFrom(message);
final GetAccountRangeMessage.Range range = getAccountRangeMessage.range(true);

final int maxResponseBytes = Math.min(range.responseBytes().intValue(), MAX_RESPONSE_SIZE);

Check notice

Code scanning / CodeQL

Unread local variable Note

Variable 'int maxResponseBytes' is never read.

LOGGER.info(
"Receive get account range message from {} to {}",
range.startKeyHash().toHexString(),
range.endKeyHash().toHexString());

final var storage =
worldStateStorageProvider.apply(getAccountRangeMessage.getRootHash().orElseThrow());

final var accounts =
storage.streamFlatAccounts(
range.startKeyHash(), range.endKeyHash(), MAX_ENTRIES_PER_REQUEST);
// TODO if accounts is empty we need to return the first hash after the requested endHash

final var worldStateProof = new WorldStateProofProvider(storage);
final ArrayDeque<Bytes> proof =
new ArrayDeque<>(
worldStateProof.getAccountProofRelatedNodes(
range.worldStateRootHash(), Hash.wrap(range.startKeyHash())));
if (!accounts.isEmpty()) {
proof.addAll(
worldStateProof.getAccountProofRelatedNodes(
range.worldStateRootHash(), Hash.wrap(accounts.lastKey())));
}

return AccountRangeMessage.create(accounts, proof);
LOGGER.info("Receive get account range message from {} to {}",
range.startKeyHash().toHexString(), range.endKeyHash().toHexString());

var worldStateHash = getAccountRangeMessage
.range(true).worldStateRootHash();

return worldStateStorageProvider.apply(worldStateHash)
.map(storage -> {
NavigableMap<Bytes32, Bytes>
accounts = storage.streamFlatAccounts(range.startKeyHash(), range.endKeyHash(),
MAX_ENTRIES_PER_REQUEST);

if (accounts.isEmpty()) {
// fetch next account after range, if it exists
accounts = storage.streamFlatAccounts(range.endKeyHash(), UInt256.MAX_VALUE, 1L);
}

final var worldStateProof = new WorldStateProofProvider(storage);
final ArrayDeque<Bytes> proof = new ArrayDeque<>(
worldStateProof.getAccountProofRelatedNodes(range.worldStateRootHash(),
Hash.wrap(range.startKeyHash())));
if (!accounts.isEmpty()) {
proof.addAll(worldStateProof.getAccountProofRelatedNodes(range.worldStateRootHash(),
Hash.wrap(accounts.lastKey())));
}
return AccountRangeMessage.create(accounts, proof);

})
.orElse(EMPTY_ACCOUNT_RANGE);
}

private MessageData constructGetStorageRangeResponse(final MessageData message) {

Check notice

Code scanning / CodeQL

Useless parameter Note

The parameter 'message' is never used.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.hyperledger.besu.ethereum.rlp.RLPInput;

import java.math.BigInteger;
import java.util.Optional;

import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
Expand Down Expand Up @@ -95,6 +96,8 @@ public Range range(final boolean withRequestId) {
@Value.Immutable
public interface Range {

Optional<BigInteger> requestId();

Hash worldStateRootHash();

Hash startKeyHash();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*
* Copyright Hyperledger Besu Contributors
*
* 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.eth.manager.snap;

import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.datatypes.Wei;
import org.hyperledger.besu.ethereum.bonsai.storage.BonsaiWorldStateKeyValueStorage;
import org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider;
import org.hyperledger.besu.ethereum.eth.manager.EthMessages;
import org.hyperledger.besu.ethereum.eth.messages.snap.AccountRangeMessage;
import org.hyperledger.besu.ethereum.eth.messages.snap.GetAccountRangeMessage;
import org.hyperledger.besu.ethereum.proof.WorldStateProofProvider;
import org.hyperledger.besu.ethereum.rlp.RLP;
import org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueStorageProvider;
import org.hyperledger.besu.ethereum.trie.patricia.StoredMerklePatriciaTrie;
import org.hyperledger.besu.ethereum.worldstate.StateTrieAccountValue;
import org.hyperledger.besu.metrics.ObservableMetricsSystem;
import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem;
import org.junit.jupiter.api.Test;

import java.math.BigInteger;
import java.util.Optional;
import java.util.Random;
import java.util.function.Function;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

public class SnapServerTest {
static Random rand = new Random();

record SnapTestAccount(Hash addressHash, StateTrieAccountValue accountValue){
Bytes asRLP() {
return RLP.encode(accountValue::writeTo);
}
}

static final ObservableMetricsSystem noopMetrics = new NoOpMetricsSystem();


final static SnapTestAccount acct1 = testAcct("10");
final static SnapTestAccount acct2 = testAcct("20");
final static SnapTestAccount acct3 = testAcct("30");
final static SnapTestAccount acct4 = testAcct("40");

final KeyValueStorageProvider storageProvider = new InMemoryKeyValueStorageProvider();
final BonsaiWorldStateKeyValueStorage inMemoryStorage =
new BonsaiWorldStateKeyValueStorage(storageProvider, noopMetrics);

final StoredMerklePatriciaTrie<Bytes, Bytes> storageTrie =
new StoredMerklePatriciaTrie<>(
inMemoryStorage::getAccountStateTrieNode,
Function.identity(),
Function.identity());
final WorldStateProofProvider proofProvider = new WorldStateProofProvider(inMemoryStorage);

final SnapServer snapServer = new SnapServer(new EthMessages(), __ -> Optional.of(inMemoryStorage));

@Test
public void assertEmptyRangeLeftProofOfExclusionAndNextAccount() {
// for a range request that returns empty, we should return just a proof of exclusion on the left
// and the next account after the limit hash
insertTestAccounts(acct1, acct4);

var rangeData = getAndVerifyAcountRangeData(
requestAccountRange(acct2.addressHash, acct3.addressHash), 1);

// expect to find only one value acct4, outside the requested range
var outOfRangeVal = rangeData.accounts().entrySet().stream().findFirst();
assertThat(outOfRangeVal).isPresent();
assertThat(outOfRangeVal.get().getKey()).isEqualTo(acct4.addressHash());

// assert proofs are valid for the requested range
assertThat(assertIsValidRangeProof(acct2.addressHash, rangeData)).isTrue();
}

@Test
public void assertLastEmptyRange() {
// When our final range request is empty, no next account is possible,
// and we should return just a proof of exclusion of the right
insertTestAccounts(acct1, acct2);
var rangeData = getAndVerifyAcountRangeData(
requestAccountRange(acct3.addressHash, acct4.addressHash), 0);

// assert proofs are valid for the requested range
assertThat(assertIsValidRangeProof(acct3.addressHash, rangeData)).isTrue();
}

@Test
public void assertAccountFoundAtStartHashProof() {
// account found at startHash
insertTestAccounts(acct1, acct2, acct3, acct4);
var rangeData = getAndVerifyAcountRangeData(
requestAccountRange(acct1.addressHash, acct4.addressHash), 4);

// assert proofs are valid for requested range
assertThat(assertIsValidRangeProof(acct1.addressHash, rangeData)).isTrue();
}




static SnapTestAccount testAcct(final String hexAddr) {
return new SnapTestAccount(
Hash.wrap(Bytes32.rightPad(Bytes.fromHexString(hexAddr))),
new StateTrieAccountValue(
rand.nextInt(0,1),
Wei.of(rand.nextLong(0L, 1L)),
Hash.EMPTY_TRIE_HASH, Hash.EMPTY));
}

void insertTestAccounts(final SnapTestAccount ... accounts) {
final var updater = inMemoryStorage.updater();
for(SnapTestAccount account : accounts) {
updater.putAccountInfoState(account.addressHash(), account.asRLP());
storageTrie.put(account.addressHash(), account.asRLP());
}
storageTrie.commit(updater::putAccountStateTrieNode);
updater.commit();

}

boolean assertIsValidRangeProof(final Hash startHash, final AccountRangeMessage.AccountRangeData accountRange) {
Bytes32 lastKey = accountRange.accounts()
.keySet()
.stream()
.reduce((first, second) -> second)
.orElse(startHash);

return proofProvider
.isValidRangeProof(
acct2.addressHash,
lastKey,
storageTrie.getRootHash(),
accountRange.proofs(),
accountRange.accounts());
}

AccountRangeMessage requestAccountRange(final Hash startHash, final Hash limitHash) {
return (AccountRangeMessage) snapServer.constructGetAccountRangeResponse(
GetAccountRangeMessage.create(
Hash.wrap(storageTrie.getRootHash()),
startHash,
limitHash)
.wrapMessageData(BigInteger.ONE));
}

AccountRangeMessage.AccountRangeData getAndVerifyAcountRangeData(
final AccountRangeMessage range, final int expectedSize) {
assertThat(range).isNotNull();
var accountData = range.accountData(false);
assertThat(accountData).isNotNull();
assertThat(accountData.accounts().size()).isEqualTo(expectedSize);
return accountData;
}
}

0 comments on commit ba392d5

Please sign in to comment.