diff --git a/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/common/ContractCallContext.java b/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/common/ContractCallContext.java index 64a3d2c784..bb85e28c0f 100644 --- a/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/common/ContractCallContext.java +++ b/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/common/ContractCallContext.java @@ -16,6 +16,9 @@ package com.hedera.mirror.web3.common; +import com.hedera.hapi.node.contract.ContractCreateTransactionBody; +import com.hedera.hapi.node.file.FileCreateTransactionBody; +import com.hedera.hapi.node.state.file.File; import com.hedera.mirror.common.domain.contract.ContractAction; import com.hedera.mirror.common.domain.transaction.RecordFile; import com.hedera.mirror.web3.evm.contracts.execution.traceability.Opcode; @@ -23,6 +26,7 @@ import com.hedera.mirror.web3.evm.contracts.execution.traceability.OpcodeTracerOptions; import com.hedera.mirror.web3.evm.store.CachingStateFrame; import com.hedera.mirror.web3.evm.store.StackedStateFrames; +import com.hedera.mirror.web3.state.FileReadableKVState; import java.util.ArrayList; import java.util.EmptyStackException; import java.util.List; @@ -73,6 +77,24 @@ public class ContractCallContext { @Setter private Optional timestamp = Optional.empty(); + /** + * The TransactionExecutor from the modularized services integration deploys contracts in 2 steps: + * + * 1. The initcode is uploaded and saved as a file using a {@link FileCreateTransactionBody}. + * 2. The returned file id from step 1 is then passed to a {@link ContractCreateTransactionBody}. + * Each step performs a separate transaction. + * For step 2 even if we pass the correct file id, since the mirror node data is readonly, + * the {@link FileReadableKVState} is not able to populate the contract's bytecode from the DB + * since it was never explicitly persisted in the DB. + * + * This is the function of the field "file" to hold temporary the bytecode and the fileId + * during contract deploy. + */ + @Setter + private Optional file = Optional.empty(); + + ; + private ContractCallContext() {} public static ContractCallContext get() { @@ -86,6 +108,7 @@ public static T run(Function function) { public void reset() { recordFile = null; stack = stackBase; + file = Optional.empty(); } public int getStackHeight() { diff --git a/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/state/FileReadableKVState.java b/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/state/FileReadableKVState.java index d8ca1935ba..119b430d34 100644 --- a/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/state/FileReadableKVState.java +++ b/hedera-mirror-web3/src/main/java/com/hedera/mirror/web3/state/FileReadableKVState.java @@ -58,7 +58,15 @@ public FileReadableKVState(final FileDataRepository fileDataRepository, final En @Override protected File readFromDataSource(@Nonnull FileID key) { final var timestamp = ContractCallContext.get().getTimestamp(); - final var fileId = toEntityId(key).getId(); + final var fileEntityId = toEntityId(key); + final var fileId = fileEntityId.getId(); + + final var contextFile = ContractCallContext.get().getFile(); + // If we are in a contract create case, the fileID and the init bytecode are in the ContractCallContext. + if (contextFile.isPresent() && contextFile.get().fileId().equals(key)) { + return contextFile.get(); + } + return timestamp .map(t -> fileDataRepository.getFileAtTimestamp(fileId, t)) .orElseGet(() -> fileDataRepository.getFileAtTimestamp(fileId, getCurrentTimestamp())) diff --git a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/state/FileReadableKVStateTest.java b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/state/FileReadableKVStateTest.java index 100cf4aa93..68afe39d05 100644 --- a/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/state/FileReadableKVStateTest.java +++ b/hedera-mirror-web3/src/test/java/com/hedera/mirror/web3/state/FileReadableKVStateTest.java @@ -21,6 +21,8 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.hedera.hapi.node.base.FileID; @@ -52,6 +54,7 @@ class FileReadableKVStateTest { private static final long REALM = 1L; private static final long FILE_NUM = 123L; private static final FileID FILE_ID = toFileId(SHARD, REALM, FILE_NUM); + private static final File FILE = File.newBuilder().fileId(FILE_ID).build(); private static final long FILE_ID_LONG = toEntityId(FILE_ID).getId(); private static final long EXPIRATION_TIMESTAMP = 2_000_000_000L; private static final Optional TIMESTAMP = Optional.of(1234L); @@ -68,6 +71,9 @@ class FileReadableKVStateTest { @Mock private EntityRepository entityRepository; + @Mock + private Bytes initBytecode; + @Spy private ContractCallContext contractCallContext; @@ -173,6 +179,19 @@ void readFromDataSourceFileNotFound() { assertThat(result).isNull(); } + @Test + void readFromDataSourceWhenThereIsContext() { + when(ContractCallContext.get().getFile()).thenReturn(Optional.of(FILE)); + + File result = fileReadableKVState.readFromDataSource(FILE_ID); + + assertThat(result) + .isEqualTo( + File.newBuilder().fileId(FILE_ID).contents(initBytecode).build()); + verify(fileDataRepository, times(0)).findById(anyLong()); + verify(fileDataRepository, times(0)).getFileAtTimestamp(anyLong(), anyLong()); + } + @Test void sizeIsAlwaysZero() { assertThat(fileReadableKVState.size()).isZero();