Skip to content

Commit 208c698

Browse files
committed
add lambdasstore transaction
Signed-off-by: Jesse Nelson <jesse@hashgraph.com>
1 parent 1991e40 commit 208c698

12 files changed

Lines changed: 526 additions & 1 deletion

File tree

common/src/main/java/org/hiero/mirror/common/domain/hook/HookStorage.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package org.hiero.mirror.common.domain.hook;
44

55
import com.fasterxml.jackson.annotation.JsonIgnore;
6+
import jakarta.persistence.Column;
67
import jakarta.persistence.Entity;
78
import jakarta.persistence.IdClass;
89
import java.io.Serial;
@@ -27,6 +28,7 @@ public class HookStorage {
2728

2829
private static final int KEY_BYTE_LENGTH = 32;
2930

31+
@Column(updatable = false)
3032
private long createdTimestamp;
3133

3234
@jakarta.persistence.Id

common/src/main/java/org/hiero/mirror/common/domain/transaction/TransactionType.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ public enum TransactionType {
6767
TOKENAIRDROP(58, EntityOperation.NONE),
6868
TOKENCANCELAIRDROP(59, EntityOperation.NONE),
6969
TOKENCLAIMAIRDROP(60, EntityOperation.NONE),
70-
ATOMIC_BATCH(74, EntityOperation.NONE);
70+
ATOMIC_BATCH(74, EntityOperation.NONE),
71+
LAMBDA_SSTORE(75, EntityOperation.NONE);
7172

7273
private static final Map<Integer, TransactionType> idMap =
7374
Arrays.stream(values()).collect(Collectors.toMap(TransactionType::getProtoId, Function.identity()));

importer/src/main/java/org/hiero/mirror/importer/parser/record/entity/EntityListener.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.hiero.mirror.common.domain.entity.NftAllowance;
1818
import org.hiero.mirror.common.domain.entity.TokenAllowance;
1919
import org.hiero.mirror.common.domain.file.FileData;
20+
import org.hiero.mirror.common.domain.hook.HookStorageChange;
2021
import org.hiero.mirror.common.domain.node.Node;
2122
import org.hiero.mirror.common.domain.schedule.Schedule;
2223
import org.hiero.mirror.common.domain.token.CustomFee;
@@ -75,6 +76,8 @@ default void onEthereumTransaction(EthereumTransaction ethereumTransaction) {}
7576

7677
default void onFileData(FileData fileData) throws ImporterException {}
7778

79+
default void onHookStorageChange(HookStorageChange storageChange) throws ImporterException {}
80+
7881
default void onLiveHash(LiveHash liveHash) throws ImporterException {}
7982

8083
default void onNetworkFreeze(NetworkFreeze networkFreeze) {}

importer/src/main/java/org/hiero/mirror/importer/parser/record/entity/sql/SqlEntityListener.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import org.hiero.mirror.common.domain.entity.NftAllowance;
2828
import org.hiero.mirror.common.domain.entity.TokenAllowance;
2929
import org.hiero.mirror.common.domain.file.FileData;
30+
import org.hiero.mirror.common.domain.hook.HookStorage;
31+
import org.hiero.mirror.common.domain.hook.HookStorageChange;
3032
import org.hiero.mirror.common.domain.node.Node;
3133
import org.hiero.mirror.common.domain.schedule.Schedule;
3234
import org.hiero.mirror.common.domain.token.AbstractNft;
@@ -60,6 +62,7 @@
6062
import org.hiero.mirror.importer.parser.record.entity.EntityListener;
6163
import org.hiero.mirror.importer.parser.record.entity.EntityProperties;
6264
import org.hiero.mirror.importer.parser.record.entity.ParserContext;
65+
import org.hiero.mirror.importer.repository.HookStorageRepository;
6366
import org.hiero.mirror.importer.repository.NftRepository;
6467
import org.hiero.mirror.importer.repository.TokenAccountRepository;
6568
import org.hiero.mirror.importer.util.Utility;
@@ -81,6 +84,7 @@ public class SqlEntityListener implements EntityListener, RecordStreamFileListen
8184
private final EntityProperties entityProperties;
8285
private final NftRepository nftRepository;
8386
private final TokenAccountRepository tokenAccountRepository;
87+
private final HookStorageRepository hookStorageRepository;
8488
private final SqlProperties sqlProperties;
8589

8690
@Override
@@ -206,6 +210,22 @@ public void onFileData(FileData fileData) {
206210
context.add(fileData);
207211
}
208212

213+
@Override
214+
public void onHookStorageChange(HookStorageChange storageChange) throws ImporterException {
215+
context.add(storageChange);
216+
217+
final var hookStorage = HookStorage.builder()
218+
.createdTimestamp(storageChange.getConsensusTimestamp())
219+
.hookId(storageChange.getHookId())
220+
.ownerId(storageChange.getOwnerId())
221+
.key(storageChange.getKey())
222+
.modifiedTimestamp(storageChange.getConsensusTimestamp())
223+
.value(storageChange.getValueWritten())
224+
.build();
225+
226+
context.merge(hookStorage.getId(), hookStorage, this::mergeHookStorage);
227+
}
228+
209229
@Override
210230
public void onLiveHash(LiveHash liveHash) throws ImporterException {
211231
context.add(liveHash);
@@ -852,6 +872,12 @@ private Topic mergeTopic(Topic previous, Topic current) {
852872
return current;
853873
}
854874

875+
private HookStorage mergeHookStorage(HookStorage previous, HookStorage current) {
876+
previous.setValue(current.getValue());
877+
previous.setModifiedTimestamp(current.getModifiedTimestamp());
878+
return previous;
879+
}
880+
855881
private void onNftTransferList(Transaction transaction) {
856882
var nftTransferList = transaction.getNftTransfer();
857883
if (CollectionUtils.isEmpty(nftTransferList)) {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package org.hiero.mirror.importer.parser.record.transactionhandler;
4+
5+
import com.hedera.services.stream.proto.TransactionSidecarRecord;
6+
import jakarta.inject.Named;
7+
import java.util.List;
8+
import lombok.CustomLog;
9+
import lombok.RequiredArgsConstructor;
10+
import org.hiero.mirror.common.domain.hook.HookStorageChange;
11+
import org.hiero.mirror.common.util.DomainUtils;
12+
import org.hiero.mirror.importer.parser.record.entity.EntityListener;
13+
import org.jspecify.annotations.NullMarked;
14+
15+
@Named
16+
@NullMarked
17+
@CustomLog
18+
@RequiredArgsConstructor
19+
public class EVMHookStorageHandler {
20+
private final EntityListener entityListener;
21+
22+
void processStorageUpdates(
23+
long consensusTimestamp, long hookId, long ownerId, List<TransactionSidecarRecord> sidecarRecords) {
24+
for (final var record : sidecarRecords) {
25+
if (!record.hasStateChanges()) {
26+
log.warn(
27+
"Ignoring storage update as sidecar record without state changes at consensusTimestamp={} for owner={} hook={}",
28+
consensusTimestamp,
29+
ownerId,
30+
hookId);
31+
continue;
32+
}
33+
final var stateChanges = record.getStateChanges();
34+
for (final var stateChange : stateChanges.getContractStateChangesList()) {
35+
for (final var storageChange : stateChange.getStorageChangesList()) {
36+
final var hookStorageUpdate = HookStorageChange.builder()
37+
.consensusTimestamp(consensusTimestamp)
38+
.hookId(hookId)
39+
.ownerId(ownerId)
40+
.key(DomainUtils.toBytes(storageChange.getSlot()))
41+
.valueRead(DomainUtils.toBytes(
42+
storageChange.getValueWritten().getValue()));
43+
44+
if (storageChange.hasValueWritten()) {
45+
hookStorageUpdate.valueWritten(DomainUtils.toBytes(
46+
storageChange.getValueWritten().getValue()));
47+
}
48+
49+
entityListener.onHookStorageChange(hookStorageUpdate.build());
50+
}
51+
}
52+
}
53+
}
54+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package org.hiero.mirror.importer.parser.record.transactionhandler;
4+
5+
import jakarta.inject.Named;
6+
import lombok.CustomLog;
7+
import lombok.RequiredArgsConstructor;
8+
import org.hiero.mirror.common.domain.entity.EntityId;
9+
import org.hiero.mirror.common.domain.transaction.RecordItem;
10+
import org.hiero.mirror.common.domain.transaction.Transaction;
11+
import org.hiero.mirror.common.domain.transaction.TransactionType;
12+
import org.jspecify.annotations.NullMarked;
13+
14+
@Named
15+
@NullMarked
16+
@RequiredArgsConstructor
17+
@CustomLog
18+
final class LambdaSStoreTransactionHandler extends AbstractTransactionHandler {
19+
20+
private final EVMHookStorageHandler storageHandler;
21+
22+
@Override
23+
public TransactionType getType() {
24+
return TransactionType.LAMBDA_SSTORE;
25+
}
26+
27+
@Override
28+
public EntityId getEntity(RecordItem recordItem) {
29+
if (!recordItem.getTransactionBody().hasLambdaSstore()) {
30+
return EntityId.EMPTY;
31+
}
32+
return EntityId.of(recordItem
33+
.getTransactionBody()
34+
.getLambdaSstore()
35+
.getHookId()
36+
.getEntityId()
37+
.getAccountId());
38+
}
39+
40+
@Override
41+
protected void doUpdateTransaction(Transaction txn, RecordItem recordItem) {
42+
final var transactionBody = recordItem.getTransactionBody();
43+
44+
if (!transactionBody.hasLambdaSstore()) {
45+
log.warn("no lambda sstore in transaction body consensus_timestamp={}", recordItem.getConsensusTimestamp());
46+
return;
47+
}
48+
49+
final var sstore = transactionBody.getLambdaSstore();
50+
final var owner = getEntity(recordItem);
51+
final var ownerId = owner.getId();
52+
final var hookId = sstore.getHookId().getHookId();
53+
final var consensusTimestamp = recordItem.getConsensusTimestamp();
54+
55+
storageHandler.processStorageUpdates(consensusTimestamp, hookId, ownerId, recordItem.getSidecarRecords());
56+
}
57+
}

importer/src/main/resources/db/migration/common/R__01_temp_tables.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ call create_temp_table_safe('crypto_allowance', 'owner', 'spender');
2525
call create_temp_table_safe('custom_fee', 'entity_id');
2626
call create_temp_table_safe('entity', 'id');
2727
call create_temp_table_safe('entity_stake', variadic array[]::text[]);
28+
call create_temp_table_safe('hook_storage', 'hook_id', 'owner_id', 'key');
2829
call create_temp_table_safe('nft_allowance', 'owner', 'spender', 'token_id');
2930
call create_temp_table_safe('nft', 'token_id', 'serial_number');
3031
call create_temp_table_safe('node', 'node_id');

importer/src/test/java/org/hiero/mirror/importer/parser/domain/RecordItemBuilder.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.google.protobuf.Int32Value;
2222
import com.google.protobuf.Int64Value;
2323
import com.google.protobuf.StringValue;
24+
import com.hedera.hapi.node.hooks.legacy.LambdaSStoreTransactionBody;
2425
import com.hedera.services.stream.proto.CallOperationType;
2526
import com.hedera.services.stream.proto.ContractAction;
2627
import com.hedera.services.stream.proto.ContractActionType;
@@ -74,6 +75,8 @@
7475
import com.hederahashgraph.api.proto.java.FractionalFee;
7576
import com.hederahashgraph.api.proto.java.FreezeTransactionBody;
7677
import com.hederahashgraph.api.proto.java.FreezeType;
78+
import com.hederahashgraph.api.proto.java.HookEntityId;
79+
import com.hederahashgraph.api.proto.java.HookId;
7780
import com.hederahashgraph.api.proto.java.Key;
7881
import com.hederahashgraph.api.proto.java.KeyList;
7982
import com.hederahashgraph.api.proto.java.LiveHash;
@@ -1235,6 +1238,22 @@ private Builder<?> toCreateTransaction(Map.Entry<GeneratedMessage, EntityState>
12351238
};
12361239
}
12371240

1241+
public Builder<LambdaSStoreTransactionBody.Builder> lambdaSstore() {
1242+
1243+
var contractId = contractId();
1244+
var body = LambdaSStoreTransactionBody.newBuilder()
1245+
.setHookId(HookId.newBuilder()
1246+
.setHookId(id())
1247+
.setEntityId(HookEntityId.newBuilder()
1248+
.setAccountId(accountId())
1249+
.setContractId(contractId)
1250+
.build())
1251+
.build());
1252+
1253+
return new Builder<>(TransactionType.LAMBDA_SSTORE, body)
1254+
.sidecarRecords(r -> r.add(contractStateChanges(contractId)));
1255+
}
1256+
12381257
public ByteString bytes(int length) {
12391258
byte[] bytes = randomBytes(length);
12401259
return ByteString.copyFrom(bytes);

importer/src/test/java/org/hiero/mirror/importer/parser/record/entity/sql/SqlEntityListenerTest.java

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.hiero.mirror.common.domain.entity.EntityType;
3636
import org.hiero.mirror.common.domain.entity.NftAllowance;
3737
import org.hiero.mirror.common.domain.entity.TokenAllowance;
38+
import org.hiero.mirror.common.domain.hook.HookStorage;
3839
import org.hiero.mirror.common.domain.node.Node;
3940
import org.hiero.mirror.common.domain.node.ServiceEndpoint;
4041
import org.hiero.mirror.common.domain.schedule.Schedule;
@@ -77,6 +78,8 @@
7778
import org.hiero.mirror.importer.repository.EntityTransactionRepository;
7879
import org.hiero.mirror.importer.repository.EthereumTransactionRepository;
7980
import org.hiero.mirror.importer.repository.FileDataRepository;
81+
import org.hiero.mirror.importer.repository.HookStorageChangeRepository;
82+
import org.hiero.mirror.importer.repository.HookStorageRepository;
8083
import org.hiero.mirror.importer.repository.LiveHashRepository;
8184
import org.hiero.mirror.importer.repository.NetworkFreezeRepository;
8285
import org.hiero.mirror.importer.repository.NetworkStakeRepository;
@@ -131,6 +134,8 @@ final class SqlEntityListenerTest extends ImporterIntegrationTest {
131134
private final EntityTransactionRepository entityTransactionRepository;
132135
private final EthereumTransactionRepository ethereumTransactionRepository;
133136
private final FileDataRepository fileDataRepository;
137+
private final HookStorageChangeRepository hookStorageChangeRepository;
138+
private final HookStorageRepository hookStorageRepository;
134139
private final LiveHashRepository liveHashRepository;
135140
private final NetworkFreezeRepository networkFreezeRepository;
136141
private final NetworkStakeRepository networkStakeRepository;
@@ -3486,6 +3491,94 @@ void onEthereumTransactionWFileId() {
34863491
assertThat(ethereumTransactionRepository.findAll()).containsExactly(ethereumTransaction);
34873492
}
34883493

3494+
@Test
3495+
void onHookStorageChange() {
3496+
final var hookStorageChange = domainBuilder.hookStorageChange().get();
3497+
3498+
sqlEntityListener.onHookStorageChange(hookStorageChange);
3499+
completeFileAndCommit();
3500+
3501+
final var expectedHookStorage = HookStorage.builder()
3502+
.createdTimestamp(hookStorageChange.getConsensusTimestamp())
3503+
.hookId(hookStorageChange.getHookId())
3504+
.key(hookStorageChange.getKey())
3505+
.modifiedTimestamp(hookStorageChange.getConsensusTimestamp())
3506+
.ownerId(hookStorageChange.getOwnerId())
3507+
.value(hookStorageChange.getValueWritten())
3508+
.build();
3509+
3510+
assertThat(hookStorageChangeRepository.findAll()).containsExactly(hookStorageChange);
3511+
assertThat(hookStorageRepository.findAll()).containsExactly(expectedHookStorage);
3512+
}
3513+
3514+
@Test
3515+
void onHookStorageChangeMerge() {
3516+
final var hookStorageChange = domainBuilder.hookStorageChange().get();
3517+
3518+
sqlEntityListener.onHookStorageChange(hookStorageChange);
3519+
3520+
final var hookStorageChange2 = domainBuilder
3521+
.hookStorageChange()
3522+
.customize(b -> {
3523+
b.consensusTimestamp(domainBuilder.timestamp())
3524+
.hookId(hookStorageChange.getHookId())
3525+
.ownerId(hookStorageChange.getOwnerId())
3526+
.key(hookStorageChange.getKey())
3527+
.valueRead(hookStorageChange.getValueWritten())
3528+
.valueWritten(domainBuilder.bytes(32));
3529+
})
3530+
.get();
3531+
3532+
sqlEntityListener.onHookStorageChange(hookStorageChange2);
3533+
completeFileAndCommit();
3534+
3535+
final var expectedHookStorage = HookStorage.builder()
3536+
.createdTimestamp(hookStorageChange.getConsensusTimestamp())
3537+
.hookId(hookStorageChange.getHookId())
3538+
.key(hookStorageChange.getKey())
3539+
.modifiedTimestamp(hookStorageChange2.getConsensusTimestamp())
3540+
.ownerId(hookStorageChange.getOwnerId())
3541+
.value(hookStorageChange2.getValueWritten())
3542+
.build();
3543+
3544+
assertThat(hookStorageChangeRepository.findAll())
3545+
.containsExactlyInAnyOrder(hookStorageChange, hookStorageChange2);
3546+
assertThat(hookStorageRepository.findAll()).containsExactly(expectedHookStorage);
3547+
}
3548+
3549+
@Test
3550+
void onHookStorageChangeHistory() {
3551+
final var changeBuilder = domainBuilder.hookStorageChange();
3552+
final var hookStorageChange = changeBuilder.get();
3553+
3554+
sqlEntityListener.onHookStorageChange(hookStorageChange);
3555+
completeFileAndCommit();
3556+
3557+
final var hookStorageChange2 = changeBuilder
3558+
.customize(b -> {
3559+
b.consensusTimestamp(domainBuilder.timestamp())
3560+
.valueRead(hookStorageChange.getValueWritten())
3561+
.valueWritten(domainBuilder.bytes(32));
3562+
})
3563+
.get();
3564+
3565+
sqlEntityListener.onHookStorageChange(hookStorageChange2);
3566+
completeFileAndCommit();
3567+
3568+
final var expectedHookStorage = HookStorage.builder()
3569+
.createdTimestamp(hookStorageChange.getConsensusTimestamp())
3570+
.hookId(hookStorageChange.getHookId())
3571+
.key(hookStorageChange.getKey())
3572+
.modifiedTimestamp(hookStorageChange2.getConsensusTimestamp())
3573+
.ownerId(hookStorageChange.getOwnerId())
3574+
.value(hookStorageChange2.getValueWritten())
3575+
.build();
3576+
3577+
assertThat(hookStorageChangeRepository.findAll())
3578+
.containsExactlyInAnyOrder(hookStorageChange, hookStorageChange2);
3579+
assertThat(hookStorageRepository.findAll()).containsExactly(expectedHookStorage);
3580+
}
3581+
34893582
private void completeFileAndCommit() {
34903583
RecordFile recordFile =
34913584
domainBuilder.recordFile().customize(r -> r.sidecars(List.of())).get();

importer/src/test/java/org/hiero/mirror/importer/parser/record/transactionhandler/AbstractTransactionHandlerTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ abstract class AbstractTransactionHandlerTest {
123123
@Mock
124124
protected SyntheticContractLogService syntheticContractLogService;
125125

126+
@Mock
127+
protected EVMHookStorageHandler storageHandler;
128+
126129
@Captor
127130
protected ArgumentCaptor<Entity> entityCaptor;
128131

0 commit comments

Comments
 (0)