Skip to content

Commit

Permalink
fix: storage link mgmt with multiple inserts (#13055)
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Tinker <michael.tinker@swirldslabs.com>
  • Loading branch information
tinker-michaelj committed Apr 30, 2024
1 parent 917faaa commit 890ca1c
Show file tree
Hide file tree
Showing 6 changed files with 469 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,11 @@ public void persistChanges(
// Adjust the storage linked lists for each contract
allAccesses.forEach(contractAccesses -> contractAccesses.accesses().forEach(access -> {
if (access.isUpdate()) {
final var firstContractKey = contractFirstKeyOf(enhancement, contractAccesses.contractID());
final var contractId = contractAccesses.contractID();
// If we have already changed the head pointer for this contract,
// use that; otherwise, get the contract's head pointer from state
final var firstContractKey =
firstKeys.computeIfAbsent(contractId, cid -> contractFirstKeyOf(enhancement, contractId));

// Only certain access types can change the head slot in a contract's storage linked list
final var newFirstContractKey =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,53 @@ void insertSlotIntoEmptyStorage() {
verifyNoMoreInteractions(hederaOperations);
}

@Test
void multipleInsertsUseLatestHeadPointer() {
final var accesses = List.of(new StorageAccesses(
CONTRACT_1,
List.of(
StorageAccess.newWrite(UInt256.valueOf(2L), UInt256.ZERO, UInt256.MAX_VALUE),
StorageAccess.newWrite(UInt256.valueOf(3L), UInt256.ZERO, UInt256.MAX_VALUE))));

final var sizeChanges = List.of(new StorageSizeChange(CONTRACT_1, 0, 2));

given(enhancement.nativeOperations()).willReturn(hederaNativeOperations);
given(hederaNativeOperations.getAccount(CONTRACT_1)).willReturn(account);
given(account.firstContractStorageKey()).willReturn(BYTES_1);
given(enhancement.operations()).willReturn(hederaOperations);
given(store.getSlotValueForModify(new SlotKey(CONTRACT_1, BYTES_1)))
.willReturn(new SlotValue(tuweniToPbjBytes(UInt256.ONE), Bytes.EMPTY, Bytes.EMPTY));
given(store.getSlotValueForModify(new SlotKey(CONTRACT_1, BYTES_2)))
.willReturn(new SlotValue(tuweniToPbjBytes(UInt256.ONE), Bytes.EMPTY, BYTES_1));

// Should insert into the head of the existing storage list
subject.persistChanges(enhancement, accesses, sizeChanges, store);

// The first insert (BYTES_2)
verify(store)
.putSlot(
new SlotKey(CONTRACT_1, BYTES_2),
new SlotValue(tuweniToPbjBytes(UInt256.MAX_VALUE), Bytes.EMPTY, BYTES_1));
verify(store)
.putSlot(
new SlotKey(CONTRACT_1, BYTES_1),
new SlotValue(tuweniToPbjBytes(UInt256.ONE), BYTES_2, Bytes.EMPTY));
// The second insert (BYTES_3)
verify(store)
.putSlot(
new SlotKey(CONTRACT_1, BYTES_3),
new SlotValue(tuweniToPbjBytes(UInt256.MAX_VALUE), Bytes.EMPTY, BYTES_2));
verify(store)
.putSlot(
new SlotKey(CONTRACT_1, BYTES_2),
new SlotValue(tuweniToPbjBytes(UInt256.ONE), BYTES_3, BYTES_1));

// The new first key is BYTES_3
verify(hederaOperations).updateStorageMetadata(CONTRACT_1, BYTES_3, 2);
verifyNoMoreInteractions(store);
verifyNoMoreInteractions(hederaOperations);
}

@Test
void insertSlotIntoExistingStorage() {
final var accesses = List.of(new StorageAccesses(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
* Copyright (C) 2024 Hedera Hashgraph, LLC
*
* 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 com.hedera.services.bdd.suites.contract.hapi;

import static com.hedera.services.bdd.junit.TestTags.SMART_CONTRACT;
import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec;
import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall;
import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate;
import static com.hedera.services.bdd.spec.transactions.TxnVerbs.uploadInitCode;
import static com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil.asHeadlongAddress;
import static java.lang.Integer.MAX_VALUE;

import com.esaulpaugh.headlong.abi.Address;
import com.esaulpaugh.headlong.abi.Tuple;
import com.hedera.services.bdd.junit.HapiTest;
import com.hedera.services.bdd.junit.HapiTestSuite;
import com.hedera.services.bdd.spec.HapiSpec;
import com.hedera.services.bdd.spec.HapiSpecOperation;
import com.hedera.services.bdd.suites.HapiSuite;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.SplittableRandom;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.jupiter.api.Tag;

@HapiTestSuite(fuzzyMatch = true)
@Tag(SMART_CONTRACT)
public class ContractStateSuite extends HapiSuite {
private static final String CONTRACT = "StateContract";
private static final SplittableRandom RANDOM = new SplittableRandom(1_234_567L);
private static final Logger LOG = LogManager.getLogger(ContractStateSuite.class);

@Override
protected Logger getResultsLogger() {
return LOG;
}

@Override
public List<HapiSpec> getSpecsInSuite() {
return List.of(stateChangesSpec());
}

@HapiTest
HapiSpec stateChangesSpec() {
final var iterations = 2;
final var integralTypes = Map.ofEntries(
Map.entry("Uint8", 0x01),
Map.entry("Uint16", 0x02),
Map.entry("Uint32", (long) 0x03),
Map.entry("Uint64", BigInteger.valueOf(4)),
Map.entry("Uint128", BigInteger.valueOf(5)),
Map.entry("Uint256", BigInteger.valueOf(6)),
Map.entry("Int8", 0x01),
Map.entry("Int16", 0x02),
Map.entry("Int32", 0x03),
Map.entry("Int64", 4L),
Map.entry("Int128", BigInteger.valueOf(5)),
Map.entry("Int256", BigInteger.valueOf(6)));

return defaultHapiSpec("stateChangesSpec")
.given(uploadInitCode(CONTRACT), contractCreate(CONTRACT))
.when(IntStream.range(0, iterations)
.boxed()
.flatMap(i -> Stream.of(
Stream.of(contractCall(CONTRACT, "setVarBool", RANDOM.nextBoolean())),
Arrays.stream(integralTypes.keySet().toArray(new String[0]))
.map(type -> contractCall(
CONTRACT, "setVar" + type, integralTypes.get(type))),
Stream.of(contractCall(CONTRACT, "setVarAddress", randomHeadlongAddress())),
Stream.of(contractCall(CONTRACT, "setVarContractType")),
Stream.of(contractCall(CONTRACT, "setVarBytes32", randomBytes32())),
Stream.of(contractCall(CONTRACT, "setVarString", randomString())),
Stream.of(contractCall(CONTRACT, "setVarEnum", randomEnum())),
randomSetAndDeleteVarInt(),
randomSetAndDeleteString(),
randomSetAndDeleteStruct())
.flatMap(s -> s))
.toArray(HapiSpecOperation[]::new))
.then();
}

private Stream<HapiSpecOperation> randomSetAndDeleteVarInt() {
final var numSetsAndDeletes = 2;
return IntStream.range(0, numSetsAndDeletes)
.boxed()
.flatMap(i -> Stream.of(
contractCall(CONTRACT, "setVarIntArrDataAlloc", new Object[] {randomInts()})
.gas(5_000_000),
contractCall(CONTRACT, "deleteVarIntArrDataAlloc")));
}

private Stream<HapiSpecOperation> randomSetAndDeleteString() {
final var numCycles = 2;
return IntStream.range(0, numCycles)
.boxed()
.flatMap(i -> Stream.of(
contractCall(CONTRACT, "setVarStringConcat", randomString())
.gas(5_000_000),
contractCall(CONTRACT, "setVarStringConcat", randomString())
.gas(5_000_000),
contractCall(CONTRACT, "deleteVarStringConcat").gas(5_000_000)));
}

private Stream<HapiSpecOperation> randomSetAndDeleteStruct() {
final var numCycles = 4;
return IntStream.range(0, numCycles)
.boxed()
.flatMap(i -> Stream.of(
contractCall(CONTRACT, "setVarContractStruct", randomContractStruct())
.gas(5_000_000),
contractCall(CONTRACT, "deleteVarContractStruct").gas(5_000_000)));
}

private Tuple randomContractStruct() {
return Tuple.of(
BigInteger.valueOf(RANDOM.nextInt(MAX_VALUE)),
randomHeadlongAddress(),
randomBytes32(),
randomString(),
RANDOM.nextInt(3),
randomInts(),
randomString());
}

private Address randomHeadlongAddress() {
final var bytes = new byte[20];
RANDOM.nextBytes(bytes);
return asHeadlongAddress(bytes);
}

private byte[] randomBytes32() {
final var bytes = new byte[32];
RANDOM.nextBytes(bytes);
return bytes;
}

private String randomString() {
return new String(randomBytes32());
}

private int randomEnum() {
return RANDOM.nextInt(3);
}

private BigInteger[] randomInts() {
return new BigInteger[] {
BigInteger.valueOf(RANDOM.nextInt(MAX_VALUE)),
BigInteger.valueOf(RANDOM.nextInt(MAX_VALUE)),
BigInteger.valueOf(RANDOM.nextInt(MAX_VALUE)),
BigInteger.valueOf(RANDOM.nextInt(MAX_VALUE)),
};
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"deleteVarContractStruct","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"deleteVarIntArrDataAlloc","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"deleteVarStringConcat","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"getVarAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getVarBool","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getVarBytes32","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getVarContractStruct","outputs":[{"components":[{"internalType":"uint256","name":"varUint256","type":"uint256"},{"internalType":"address","name":"varAddress","type":"address"},{"internalType":"bytes32","name":"varBytes32","type":"bytes32"},{"internalType":"string","name":"varString","type":"string"},{"internalType":"enum Choices","name":"varContractType","type":"uint8"},{"internalType":"uint256[]","name":"varUint256Arr","type":"uint256[]"},{"internalType":"string","name":"varStringConcat","type":"string"}],"internalType":"struct ContractStruct","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getVarContractType","outputs":[{"internalType":"contract ContractType","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getVarEnum","outputs":[{"internalType":"enum Choices","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getVarInt128","outputs":[{"internalType":"int128","name":"","type":"int128"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getVarInt16","outputs":[{"internalType":"int16","name":"","type":"int16"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getVarInt256","outputs":[{"internalType":"int256","name":"","type":"int256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getVarInt32","outputs":[{"internalType":"int32","name":"","type":"int32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getVarInt64","outputs":[{"internalType":"int64","name":"","type":"int64"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getVarInt8","outputs":[{"internalType":"int8","name":"","type":"int8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getVarIntArrDataAlloc","outputs":[{"internalType":"uint256[]","name":"","type":"uint256[]"},{"internalType":"uint256[]","name":"","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getVarString","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getVarStringConcat","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getVarUint128","outputs":[{"internalType":"uint128","name":"","type":"uint128"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getVarUint16","outputs":[{"internalType":"uint16","name":"","type":"uint16"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getVarUint256","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getVarUint32","outputs":[{"internalType":"uint32","name":"","type":"uint32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getVarUint64","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getVarUint8","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"newVar","type":"address"}],"name":"setVarAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bool","name":"newVar","type":"bool"}],"name":"setVarBool","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"newVar","type":"bytes32"}],"name":"setVarBytes32","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"uint256","name":"varUint256","type":"uint256"},{"internalType":"address","name":"varAddress","type":"address"},{"internalType":"bytes32","name":"varBytes32","type":"bytes32"},{"internalType":"string","name":"varString","type":"string"},{"internalType":"enum Choices","name":"varContractType","type":"uint8"},{"internalType":"uint256[]","name":"varUint256Arr","type":"uint256[]"},{"internalType":"string","name":"varStringConcat","type":"string"}],"internalType":"struct ContractStruct","name":"newVar","type":"tuple"}],"name":"setVarContractStruct","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"setVarContractType","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"enum Choices","name":"newVar","type":"uint8"}],"name":"setVarEnum","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"int128","name":"newVar","type":"int128"}],"name":"setVarInt128","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"int16","name":"newVar","type":"int16"}],"name":"setVarInt16","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"int256","name":"newVar","type":"int256"}],"name":"setVarInt256","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"int32","name":"newVar","type":"int32"}],"name":"setVarInt32","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"int64","name":"newVar","type":"int64"}],"name":"setVarInt64","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"int8","name":"newVar","type":"int8"}],"name":"setVarInt8","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256[]","name":"newVar","type":"uint256[]"}],"name":"setVarIntArrDataAlloc","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"newVar","type":"string"}],"name":"setVarString","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"newVar","type":"string"}],"name":"setVarStringConcat","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint128","name":"newVar","type":"uint128"}],"name":"setVarUint128","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint16","name":"newVar","type":"uint16"}],"name":"setVarUint16","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"newVar","type":"uint256"}],"name":"setVarUint256","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint32","name":"newVar","type":"uint32"}],"name":"setVarUint32","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint64","name":"newVar","type":"uint64"}],"name":"setVarUint64","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint8","name":"newVar","type":"uint8"}],"name":"setVarUint8","outputs":[],"stateMutability":"nonpayable","type":"function"}]

0 comments on commit 890ca1c

Please sign in to comment.