From 0a6eb93425d940dc9db18a3e30ae529cc254cc9f Mon Sep 17 00:00:00 2001 From: Asuka Date: Wed, 21 Oct 2026 16:59:05 +0800 Subject: [PATCH 1/3] opt: gate write-only storage migration --- .../org/tron/core/utils/ProposalUtil.java | 18 +- .../org/tron/core/vm/config/ConfigLoader.java | 2 +- .../org/tron/core/vm/program/Storage.java | 17 +- .../core/vm/repository/RepositoryImpl.java | 3 +- .../core/vm/program/StorageAliasTest.java | 253 ++++++++-- .../core/store/DynamicPropertiesStore.java | 21 + .../java/org/tron/core/config/Parameter.java | 5 +- .../org/tron/core/vm/config/VMConfig.java | 10 +- .../src/main/java/org/tron/core/Wallet.java | 5 + .../tron/core/consensus/ProposalService.java | 5 + .../tron/core/db/HistoryBlockHashUtil.java | 2 +- .../services/jsonrpc/TronJsonRpcImpl.java | 2 +- .../main/java/org/tron/program/Version.java | 2 +- .../runtime/vm/OptimizeTvmStorageTest.java | 21 +- .../runtime/vm/StorageMigrationTest.java | 469 ++++++++++++++++++ .../runtime/vm/VMConfigIsolationTest.java | 2 +- .../core/actuator/utils/ProposalUtilTest.java | 75 +++ .../db/HistoryBlockHashIntegrationTest.java | 2 +- .../tron/core/db/HistoryBlockHashVmTest.java | 15 +- 19 files changed, 867 insertions(+), 62 deletions(-) create mode 100644 framework/src/test/java/org/tron/common/runtime/vm/StorageMigrationTest.java diff --git a/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java b/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java index 2c70a21ef3b..e03e22c4e98 100644 --- a/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java +++ b/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java @@ -956,6 +956,21 @@ public static void validator(DynamicPropertiesStore dynamicPropertiesStore, } break; } + case ALLOW_OPTIMIZE_TVM_STORAGE_KEY: { + if (!forkController.pass(ForkBlockVersionEnum.VERSION_4_8_2_4)) { + throw new ContractValidateException( + "Bad chain parameter id [ALLOW_OPTIMIZE_TVM_STORAGE_KEY]"); + } + if (dynamicPropertiesStore.getAllowOptimizeTvmStorageKey() == 1) { + throw new ContractValidateException( + "[ALLOW_OPTIMIZE_TVM_STORAGE_KEY] has been valid, no need to propose again"); + } + if (value != 1) { + throw new ContractValidateException( + "This value[ALLOW_OPTIMIZE_TVM_STORAGE_KEY] is only allowed to be 1"); + } + break; + } default: break; } @@ -1045,7 +1060,8 @@ public enum ProposalType { // current value, value range ALLOW_TVM_OSAKA(96), // 0, 1 ALLOW_HARDEN_RESOURCE_CALCULATION(97), // 0, 1 ALLOW_HARDEN_EXCHANGE_CALCULATION(98), // 0, 1 - ALLOW_OPTIMIZE_TVM_STORAGE(99); // 0, 1 + ALLOW_OPTIMIZE_TVM_STORAGE(99), // 0, 1 + ALLOW_OPTIMIZE_TVM_STORAGE_KEY(102); // 0, 1 private long code; ProposalType(long code) { diff --git a/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java b/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java index 9d927f7039b..91ffdfa88fa 100644 --- a/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java +++ b/actuator/src/main/java/org/tron/core/vm/config/ConfigLoader.java @@ -50,7 +50,7 @@ public static void load(StoreFactory storeFactory, boolean isolate) { snapshot.allowTvmSelfdestructRestriction = ds.getAllowTvmSelfdestructRestriction() == 1; snapshot.allowTvmOsaka = ds.getAllowTvmOsaka() == 1; snapshot.allowHardenResourceCalculation = ds.getAllowHardenResourceCalculation() == 1; - snapshot.allowOptimizeTvmStorage = ds.getAllowOptimizeTvmStorage() == 1; + snapshot.allowOptimizeTvmStorageKey = ds.allowOptimizeTvmStorageKey(); if (isolate) { VMConfig.setLocalSnapshot(snapshot); } else { diff --git a/actuator/src/main/java/org/tron/core/vm/program/Storage.java b/actuator/src/main/java/org/tron/core/vm/program/Storage.java index 1c2b15af863..e371f34488b 100644 --- a/actuator/src/main/java/org/tron/core/vm/program/Storage.java +++ b/actuator/src/main/java/org/tron/core/vm/program/Storage.java @@ -12,6 +12,7 @@ import org.tron.common.utils.ForkController; import org.tron.core.capsule.StorageRowCapsule; import org.tron.core.config.Parameter.ForkBlockVersionEnum; +import org.tron.core.store.DynamicPropertiesStore; import org.tron.core.store.StorageRowStore; public class Storage { @@ -28,6 +29,7 @@ private enum ReadKind { private final Map oldRowKeyOwners = new HashMap<>(); private final Map readKinds = new HashMap<>(); private final boolean optimizeTvmStorage; + private final boolean legacyReadProtection; private final boolean aliasCheckEnabled; @Getter @@ -39,11 +41,12 @@ private enum ReadKind { @Setter private int contractVersion; - public Storage(byte[] address, StorageRowStore store, boolean optimizeTvmStorage) { + public Storage(byte[] address, StorageRowStore store, DynamicPropertiesStore dynamicStore) { addrHash = addrHash(address); this.address = address; this.store = store; - this.optimizeTvmStorage = optimizeTvmStorage; + this.optimizeTvmStorage = dynamicStore.allowTvmStorageOptimization(); + this.legacyReadProtection = dynamicStore.allowOptimizeTvmStorageKey(); this.aliasCheckEnabled = ForkController.instance().pass(ForkBlockVersionEnum.VERSION_4_8_2_3); } @@ -53,6 +56,7 @@ public Storage(Storage storage) { this.store = storage.store; this.contractVersion = storage.contractVersion; this.optimizeTvmStorage = storage.optimizeTvmStorage; + this.legacyReadProtection = storage.legacyReadProtection; this.aliasCheckEnabled = storage.aliasCheckEnabled; storage.getRowCache().forEach((DataWord key, StorageRowCapsule row) -> { StorageRowCapsule newRow = new StorageRowCapsule(row); @@ -158,6 +162,12 @@ private DataWord getOptimized(DataWord key) { } if (oldKeyTaken(key)) { + if (legacyReadProtection) { + StorageRowCapsule oldRow = store.get(getOldRowKey(key)); + if (oldRow != null && oldRow.getValue() != null) { + throw new Program.OutOfTimeException("CPU timeout for storage check"); + } + } return null; } @@ -226,6 +236,9 @@ private void commitOptimized() { store.delete(getOldRowKey(key)); } }); + if (legacyReadProtection) { + return; + } readKinds.forEach((DataWord key, ReadKind kind) -> { StorageRowCapsule row = rowCache.get(key); if (row != null && row.isDirty()) { diff --git a/actuator/src/main/java/org/tron/core/vm/repository/RepositoryImpl.java b/actuator/src/main/java/org/tron/core/vm/repository/RepositoryImpl.java index 5c7691e6dd4..a3fe88d9c12 100644 --- a/actuator/src/main/java/org/tron/core/vm/repository/RepositoryImpl.java +++ b/actuator/src/main/java/org/tron/core/vm/repository/RepositoryImpl.java @@ -739,8 +739,7 @@ public Storage getStorage(byte[] address) { storage = parentStorage; } } else { - storage = new Storage(address, getStorageRowStore(), - VMConfig.allowOptimizeTvmStorage()); + storage = new Storage(address, getStorageRowStore(), getDynamicPropertiesStore()); } ContractCapsule contract = getContract(address); if (contract != null) { diff --git a/actuator/src/test/java/org/tron/core/vm/program/StorageAliasTest.java b/actuator/src/test/java/org/tron/core/vm/program/StorageAliasTest.java index e24f1a691f9..9f888ac78a0 100644 --- a/actuator/src/test/java/org/tron/core/vm/program/StorageAliasTest.java +++ b/actuator/src/test/java/org/tron/core/vm/program/StorageAliasTest.java @@ -23,6 +23,7 @@ import org.tron.common.utils.ForkController; import org.tron.core.capsule.StorageRowCapsule; import org.tron.core.config.Parameter.ForkBlockVersionEnum; +import org.tron.core.store.DynamicPropertiesStore; import org.tron.core.store.StorageRowStore; import org.tron.core.vm.config.VMConfig; import org.tron.core.vm.program.Program.OutOfTimeException; @@ -44,7 +45,7 @@ public void setUp() { when(controller.pass(ForkBlockVersionEnum.VERSION_4_8_2_3)).thenReturn(true); VMConfig.setLocalSnapshot(new VMConfig.Snapshot()); store = mock(StorageRowStore.class); - storage = new Storage(new byte[21], store, false); + storage = createStorage(0, 0); } @After @@ -86,7 +87,7 @@ public void readOfAliasAfterWriteThrows() { @Test public void aliasedReadsRemainAllowedBeforeFork() { when(controller.pass(ForkBlockVersionEnum.VERSION_4_8_2_3)).thenReturn(false); - Storage before = new Storage(new byte[21], store, false); + Storage before = createStorage(0, 0); when(store.get(any(byte[].class))).thenAnswer( invocation -> new StorageRowCapsule(new DataWord(5).getData())); assertEquals(new DataWord(5), before.getValue(slot)); @@ -99,7 +100,7 @@ public void aliasedReadsRemainAllowedBeforeFork() { public void forkFlagIsCapturedOnceAtConstruction() { clearInvocations(controller); when(controller.pass(ForkBlockVersionEnum.VERSION_4_8_2_3)).thenReturn(false); - Storage before = new Storage(new byte[21], store, false); + Storage before = createStorage(0, 0); before.put(slot, new DataWord(5)); when(controller.pass(ForkBlockVersionEnum.VERSION_4_8_2_3)).thenReturn(true); @@ -109,24 +110,28 @@ public void forkFlagIsCapturedOnceAtConstruction() { child.put(slot, new DataWord(1)); child.put(alias, new DataWord(2)); verify(controller, times(1)).pass(ForkBlockVersionEnum.VERSION_4_8_2_3); + verify(controller, never()).pass(ForkBlockVersionEnum.VERSION_4_8_2_4); - Storage after = new Storage(new byte[21], store, false); + Storage after = createStorage(0, 0); after.put(alias, new DataWord(8)); assertThrows(OutOfTimeException.class, () -> after.put(slot, new DataWord(7))); verify(controller, times(2)).pass(ForkBlockVersionEnum.VERSION_4_8_2_3); + verify(controller, never()).pass(ForkBlockVersionEnum.VERSION_4_8_2_4); clearInvocations(controller); - Storage optimized = new Storage(new byte[21], store, true); + Storage optimized = optimizedStorage(); optimized.put(slot, new DataWord(1)); optimized.put(alias, new DataWord(2)); assertEquals(new DataWord(2), optimized.getValue(alias)); + new Storage(optimized).put(slot, new DataWord(3)); verify(controller, times(1)).pass(ForkBlockVersionEnum.VERSION_4_8_2_3); + verify(controller, never()).pass(ForkBlockVersionEnum.VERSION_4_8_2_4); } @Test public void aliasedWritesRemainAllowedBeforeFork() { when(controller.pass(ForkBlockVersionEnum.VERSION_4_8_2_3)).thenReturn(false); - Storage before = new Storage(new byte[21], store, false); + Storage before = createStorage(0, 0); before.put(slot, new DataWord(5)); before.put(alias, new DataWord(8)); assertEquals(new DataWord(5), before.getValue(slot)); @@ -165,7 +170,7 @@ public void versionOneUsesHashedLegacyRows() { @Test public void migratedNewKeysDoNotConflict() { - Storage optimized = new Storage(new byte[21], store, true); + Storage optimized = optimizedStorage(); when(store.get(argThat(key -> key != null && key.length == 48))) .thenReturn(new StorageRowCapsule(new DataWord(4).getData())); assertEquals(new DataWord(4), optimized.getValue(slot)); @@ -177,37 +182,35 @@ public void migratedNewKeysDoNotConflict() { } @Test - public void optimizedWritesDoNotTimeoutAndCollidingReadIsEmpty() { - Storage optimized = new Storage(new byte[21], store, true); + public void optimizedWritesDoNotTimeout() { + Storage optimized = optimizedStorage(); when(store.get(argThat(key -> key != null && key.length == 32))) .thenReturn(new StorageRowCapsule(new DataWord(7).getData())); optimized.put(slot, new DataWord(5)); - assertNull(optimized.getValue(alias)); optimized.put(alias, new DataWord(8)); assertEquals(48, optimized.getRowCache().get(slot).getRowKey().length); assertEquals(48, optimized.getRowCache().get(alias).getRowKey().length); assertEquals(new DataWord(5), optimized.getValue(slot)); assertEquals(new DataWord(8), optimized.getValue(alias)); - assertNull(optimized.getValue(anotherAlias())); + assertThrows(OutOfTimeException.class, () -> optimized.getValue(anotherAlias())); } @Test public void oldKeyIsReturnedOnlyToTheFirstCachedSlot() { - Storage optimized = new Storage(new byte[21], store, true); + Storage optimized = optimizedStorage(); when(store.get(argThat(key -> key != null && key.length == 32))) .thenReturn(new StorageRowCapsule(new DataWord(7).getData())); assertEquals(new DataWord(7), optimized.getValue(slot)); - assertNull(optimized.getValue(alias)); + assertThrows(OutOfTimeException.class, () -> optimized.getValue(alias)); assertEquals(new DataWord(7), optimized.getValue(slot.clone())); optimized.commit(); - verify(store, times(1)).put(argThat(key -> key != null && key.length == 48), - argThat(row -> new DataWord(row.getValue()).longValue() == 7L)); - verify(store).delete(argThat(key -> key != null && key.length == 32)); + verify(store, never()).put(any(), any()); + verify(store, never()).delete(any()); } @Test public void newKeyWinsOverACachedOldKeyCollision() { - Storage optimized = new Storage(new byte[21], store, true); + Storage optimized = optimizedStorage(); boolean[] serveNewKey = {true}; when(store.get(any(byte[].class))).thenAnswer(invocation -> { byte[] key = invocation.getArgument(0); @@ -221,14 +224,14 @@ public void newKeyWinsOverACachedOldKeyCollision() { }); assertEquals(new DataWord(4), optimized.getValue(slot)); serveNewKey[0] = false; - assertNull(optimized.getValue(alias)); + assertThrows(OutOfTimeException.class, () -> optimized.getValue(alias)); serveNewKey[0] = true; assertEquals(new DataWord(4), optimized.getValue(alias)); } @Test public void optimizedCommitDeletesLegacyKey() { - Storage optimized = new Storage(new byte[21], store, true); + Storage optimized = optimizedStorage(); optimized.put(slot, new DataWord(5)); optimized.commit(); verify(store).put(argThat(key -> key != null && key.length == 48), any()); @@ -236,20 +239,45 @@ public void optimizedCommitDeletesLegacyKey() { } @Test - public void readOfOldKeyMigratesOnCommit() { - Storage optimized = new Storage(new byte[21], store, true); + public void readOfOldKeyDoesNotMigrateOnCommit() { + Storage optimized = optimizedStorage(); when(store.get(argThat(key -> key != null && key.length == 32))) .thenReturn(new StorageRowCapsule(new DataWord(7).getData())); assertEquals(new DataWord(7), optimized.getValue(slot)); optimized.commit(); + verify(store, never()).put(any(), any()); + verify(store, never()).delete(any()); + } + + @Test + public void proposal99SecondReadIsEmptyAndPureReadMigrates() { + Storage proposal99 = createStorage(1, 0); + when(store.get(argThat(key -> key != null && key.length == 32))) + .thenReturn(new StorageRowCapsule(new DataWord(7).getData())); + assertEquals(new DataWord(7), proposal99.getValue(slot)); + assertNull(proposal99.getValue(alias)); + proposal99.commit(); verify(store).put(argThat(key -> key != null && key.length == 48), argThat(row -> new DataWord(row.getValue()).longValue() == 7L)); verify(store).delete(argThat(key -> key != null && key.length == 32)); } + @Test + public void proposal99ChildCopyDoesNotTimeout() { + Storage parent = createStorage(1, 0); + when(store.get(argThat(key -> key != null && key.length == 32))) + .thenReturn(new StorageRowCapsule(new DataWord(7).getData())); + assertEquals(new DataWord(7), parent.getValue(slot)); + Storage child = new Storage(parent); + assertNull(child.getValue(alias)); + child.commit(); + verify(store).put(argThat(key -> key != null && key.length == 48), any()); + verify(store).delete(argThat(key -> key != null && key.length == 32)); + } + @Test public void readOfMissingKeyDoesNotWriteAndRepeatReadSkipsStore() { - Storage optimized = new Storage(new byte[21], store, true); + Storage optimized = optimizedStorage(); assertNull(optimized.getValue(slot)); assertNull(optimized.getValue(slot)); optimized.commit(); @@ -260,7 +288,7 @@ public void readOfMissingKeyDoesNotWriteAndRepeatReadSkipsStore() { @Test public void readOfNewKeyDoesNotRewrite() { - Storage optimized = new Storage(new byte[21], store, true); + Storage optimized = optimizedStorage(); when(store.get(argThat(key -> key != null && key.length == 48))) .thenReturn(new StorageRowCapsule(new DataWord(4).getData())); assertEquals(new DataWord(4), optimized.getValue(slot)); @@ -271,7 +299,7 @@ public void readOfNewKeyDoesNotRewrite() { @Test public void writeAfterReadKeepsWrittenValueAndDeletesOldKey() { - Storage optimized = new Storage(new byte[21], store, true); + Storage optimized = optimizedStorage(); when(store.get(argThat(key -> key != null && key.length == 32))) .thenReturn(new StorageRowCapsule(new DataWord(7).getData())); assertEquals(new DataWord(7), optimized.getValue(slot)); @@ -284,7 +312,7 @@ public void writeAfterReadKeepsWrittenValueAndDeletesOldKey() { @Test public void readThenWriteOfExistingNewKeyDoesNotDeleteOld() { - Storage optimized = new Storage(new byte[21], store, true); + Storage optimized = optimizedStorage(); when(store.get(argThat(key -> key != null && key.length == 48))) .thenReturn(new StorageRowCapsule(new DataWord(4).getData())); assertEquals(new DataWord(4), optimized.getValue(slot)); @@ -297,7 +325,7 @@ public void readThenWriteOfExistingNewKeyDoesNotDeleteOld() { @Test public void writeThenCacheHitDeletesOldKey() { - Storage optimized = new Storage(new byte[21], store, true); + Storage optimized = optimizedStorage(); when(store.get(argThat(key -> key != null && key.length == 48))) .thenReturn(new StorageRowCapsule(new DataWord(4).getData())); optimized.put(slot, new DataWord(9)); @@ -309,7 +337,7 @@ public void writeThenCacheHitDeletesOldKey() { @Test public void storedZeroOnNewKeyReadsAsEmptyAndSkipsOldKey() { - Storage optimized = new Storage(new byte[21], store, true); + Storage optimized = optimizedStorage(); when(store.get(any(byte[].class))).thenAnswer(invocation -> { byte[] key = invocation.getArgument(0); if (key.length == 48) { @@ -333,7 +361,7 @@ public void storedZeroOnNewKeyReadsAsEmptyAndSkipsOldKey() { @Test public void writeAfterStoredZeroHitsCacheAndDoesNotDeleteOldKey() { - Storage optimized = new Storage(new byte[21], store, true); + Storage optimized = optimizedStorage(); when(store.get(argThat(key -> key != null && key.length == 48))) .thenReturn(new StorageRowCapsule(DataWord.ZERO().getData())); assertNull(optimized.getValue(slot)); @@ -348,7 +376,7 @@ public void writeAfterStoredZeroHitsCacheAndDoesNotDeleteOldKey() { @Test public void putZeroOverStoredZeroStaysInCache() { - Storage optimized = new Storage(new byte[21], store, true); + Storage optimized = optimizedStorage(); when(store.get(argThat(key -> key != null && key.length == 48))) .thenReturn(new StorageRowCapsule(DataWord.ZERO().getData())); assertNull(optimized.getValue(slot)); @@ -358,6 +386,173 @@ public void putZeroOverStoredZeroStaysInCache() { verify(store, times(1)).get(any()); } + @Test + public void collidingReadTimesOutAndDoesNotMigrate() { + Storage optimized = optimizedStorage(); + when(store.get(argThat(key -> key != null && key.length == 32))) + .thenReturn(new StorageRowCapsule(new DataWord(7).getData())); + assertEquals(new DataWord(7), optimized.getValue(slot)); + OutOfTimeException ex = assertThrows(OutOfTimeException.class, + () -> optimized.getValue(alias)); + assertEquals("CPU timeout for storage check", ex.getMessage()); + optimized.commit(); + verify(store, never()).put(any(), any()); + verify(store, never()).delete(any()); + } + + @Test + public void pureReadDoesNotMigrateAndLegacyValueStays() { + Storage optimized = optimizedStorage(); + when(store.get(argThat(key -> key != null && key.length == 32))) + .thenReturn(new StorageRowCapsule(new DataWord(7).getData())); + assertEquals(new DataWord(7), optimized.getValue(alias)); + assertEquals(new DataWord(7), optimized.getValue(alias.clone())); + optimized.commit(); + verify(store, never()).put(any(), any()); + verify(store, never()).delete(any()); + Storage later = optimizedStorage(); + assertEquals(new DataWord(7), later.getValue(slot)); + } + + @Test + public void unrelatedWriteDoesNotMigrateAPureRead() { + Storage optimized = optimizedStorage(); + when(store.get(argThat(key -> key != null && key.length == 32))) + .thenReturn(new StorageRowCapsule(new DataWord(7).getData())); + assertEquals(new DataWord(7), optimized.getValue(slot)); + optimized.put(new DataWord(2), new DataWord(3)); + optimized.commit(); + verify(store, times(1)).put(argThat(key -> key != null && key.length == 48), any()); + verify(store, times(1)).delete(argThat(key -> key != null && key.length == 32)); + Storage later = optimizedStorage(); + assertEquals(new DataWord(7), later.getValue(slot)); + } + + @Test + public void writeAfterReadStillMigrates() { + Storage optimized = optimizedStorage(); + when(store.get(argThat(key -> key != null && key.length == 32))) + .thenReturn(new StorageRowCapsule(new DataWord(7).getData())); + assertEquals(new DataWord(7), optimized.getValue(slot)); + optimized.put(slot, new DataWord(9)); + optimized.commit(); + verify(store).put(argThat(key -> key != null && key.length == 48), + argThat(row -> new DataWord(row.getValue()).longValue() == 9L)); + verify(store).delete(argThat(key -> key != null && key.length == 32)); + } + + @Test + public void directPutDoesNotThrowAndStillDeletesOldKey() { + Storage optimized = optimizedStorage(); + optimized.put(slot, new DataWord(5)); + optimized.put(alias, new DataWord(8)); + optimized.commit(); + verify(store, times(2)).put(argThat(key -> key != null && key.length == 48), any()); + verify(store, times(2)).delete(argThat(key -> key != null && key.length == 32)); + } + + @Test + public void readAfterAliasPutTimesOutWhileLegacyRowExists() { + Storage optimized = optimizedStorage(); + when(store.get(argThat(key -> key != null && key.length == 32))) + .thenReturn(new StorageRowCapsule(new DataWord(7).getData())); + optimized.put(slot, new DataWord(5)); + OutOfTimeException ex = assertThrows(OutOfTimeException.class, + () -> optimized.getValue(alias)); + assertEquals("CPU timeout for storage check", ex.getMessage()); + } + + @Test + public void aliasReadOfAbsentLegacyRowStaysEmpty() { + Storage optimized = optimizedStorage(); + optimized.put(slot, new DataWord(5)); + assertNull(optimized.getValue(alias)); + } + + @Test + public void aliasReadAfterMigrationDoesNotThrow() { + Storage optimized = optimizedStorage(); + int[] newKeyReads = {0}; + when(store.get(any(byte[].class))).thenAnswer(invocation -> { + byte[] key = invocation.getArgument(0); + if (key.length == 48 && newKeyReads[0]++ == 0) { + return new StorageRowCapsule(new DataWord(4).getData()); + } + return null; + }); + assertEquals(new DataWord(4), optimized.getValue(slot)); + assertNull(optimized.getValue(alias)); + optimized.commit(); + verify(store, never()).put(any(), any()); + verify(store, never()).delete(any()); + } + + @Test + public void liveLegacyRowStillTimesOutAfterANewKeyHit() { + Storage optimized = optimizedStorage(); + int[] newKeyReads = {0}; + when(store.get(any(byte[].class))).thenAnswer(invocation -> { + byte[] key = invocation.getArgument(0); + if (key.length == 48 && newKeyReads[0]++ == 0) { + return new StorageRowCapsule(new DataWord(4).getData()); + } + if (key.length == 32) { + return new StorageRowCapsule(new DataWord(7).getData()); + } + return null; + }); + assertEquals(new DataWord(4), optimized.getValue(slot)); + assertThrows(OutOfTimeException.class, () -> optimized.getValue(alias)); + } + + @Test + public void migratedNewKeysDoNotTimeout() { + Storage optimized = optimizedStorage(); + when(store.get(argThat(key -> key != null && key.length == 48))) + .thenReturn(new StorageRowCapsule(new DataWord(4).getData())); + assertEquals(new DataWord(4), optimized.getValue(slot)); + assertEquals(new DataWord(4), optimized.getValue(alias)); + optimized.commit(); + verify(store, never()).delete(any()); + } + + @Test + public void childCopyInheritsOwner() { + Storage parent = optimizedStorage(); + when(store.get(argThat(key -> key != null && key.length == 32))) + .thenReturn(new StorageRowCapsule(new DataWord(7).getData())); + assertEquals(new DataWord(7), parent.getValue(slot)); + Storage child = new Storage(parent); + assertThrows(OutOfTimeException.class, () -> child.getValue(alias)); + child.commit(); + verify(store, never()).put(any(), any()); + verify(store, never()).delete(any()); + } + + @Test + public void versionOneAliasesDoNotTimeout() { + Storage optimized = optimizedStorage(); + optimized.setContractVersion(1); + when(store.get(argThat(key -> key != null && key.length == 32))) + .thenReturn(new StorageRowCapsule(new DataWord(7).getData())); + assertEquals(new DataWord(7), optimized.getValue(slot)); + assertEquals(new DataWord(7), optimized.getValue(alias)); + optimized.commit(); + verify(store, never()).put(any(), any()); + verify(store, never()).delete(any()); + } + + private Storage optimizedStorage() { + return createStorage(0, 1); + } + + private Storage createStorage(int proposal99, int proposal102) { + DynamicPropertiesStore dynamicStore = mock(DynamicPropertiesStore.class); + when(dynamicStore.allowTvmStorageOptimization()).thenReturn(proposal99 == 1 || proposal102 == 1); + when(dynamicStore.allowOptimizeTvmStorageKey()).thenReturn(proposal102 == 1); + return new Storage(new byte[21], store, dynamicStore); + } + private static DataWord aliasedSlot(int value) { byte[] bytes = new DataWord(value).getData().clone(); bytes[15] = 1; diff --git a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java index 99dbbe71824..4d79f23be2b 100644 --- a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java +++ b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java @@ -259,6 +259,8 @@ public class DynamicPropertiesStore extends TronStoreWithRevoking "TURKISH_KEY_MIGRATION_DONE".getBytes(); private static final byte[] ALLOW_OPTIMIZE_TVM_STORAGE = "ALLOW_OPTIMIZE_TVM_STORAGE".getBytes(); + private static final byte[] ALLOW_OPTIMIZE_TVM_STORAGE_KEY = + "ALLOW_OPTIMIZE_TVM_STORAGE_KEY".getBytes(); @Autowired private DynamicPropertiesStore(@Value("properties") String dbName) { @@ -3100,6 +3102,25 @@ public boolean allowOptimizeTvmStorage() { return getAllowOptimizeTvmStorage() == 1L; } + public long getAllowOptimizeTvmStorageKey() { + return Optional.ofNullable(getUnchecked(ALLOW_OPTIMIZE_TVM_STORAGE_KEY)) + .map(BytesCapsule::getData) + .map(ByteArray::toLong) + .orElse(0L); + } + + public void saveAllowOptimizeTvmStorageKey(long value) { + this.put(ALLOW_OPTIMIZE_TVM_STORAGE_KEY, new BytesCapsule(ByteArray.fromLong(value))); + } + + public boolean allowOptimizeTvmStorageKey() { + return getAllowOptimizeTvmStorageKey() == 1L; + } + + public boolean allowTvmStorageOptimization() { + return allowOptimizeTvmStorage() || allowOptimizeTvmStorageKey(); + } + private static class DynamicResourceProperties { private static final byte[] ONE_DAY_NET_LIMIT = "ONE_DAY_NET_LIMIT".getBytes(); diff --git a/common/src/main/java/org/tron/core/config/Parameter.java b/common/src/main/java/org/tron/core/config/Parameter.java index 57954f2add4..60df4727a79 100644 --- a/common/src/main/java/org/tron/core/config/Parameter.java +++ b/common/src/main/java/org/tron/core/config/Parameter.java @@ -32,7 +32,8 @@ public enum ForkBlockVersionEnum { VERSION_4_8_1_1(35, 1596780000000L, 70), VERSION_4_8_2(36, 1596780000000L, 80), VERSION_4_8_2_2(37, 1596780000000L, 70), - VERSION_4_8_2_3(38, 1596780000000L, 70); + VERSION_4_8_2_3(38, 1596780000000L, 70), + VERSION_4_8_2_4(39, 1596780000000L, 70); // if add a version, modify BLOCK_VERSION simultaneously @Getter @@ -81,7 +82,7 @@ public class ChainConstant { public static final int SINGLE_REPEAT = 1; public static final int BLOCK_FILLED_SLOTS_NUMBER = 128; public static final int MAX_FROZEN_NUMBER = 1; - public static final int BLOCK_VERSION = 38; + public static final int BLOCK_VERSION = 39; public static final long FROZEN_PERIOD = 86_400_000L; public static final long DELEGATE_PERIOD = 3 * 86_400_000L; public static final long TRX_PRECISION = 1000_000L; diff --git a/common/src/main/java/org/tron/core/vm/config/VMConfig.java b/common/src/main/java/org/tron/core/vm/config/VMConfig.java index 6e77caa0ede..da9fedade0b 100644 --- a/common/src/main/java/org/tron/core/vm/config/VMConfig.java +++ b/common/src/main/java/org/tron/core/vm/config/VMConfig.java @@ -46,7 +46,7 @@ public static class Snapshot { public boolean allowTvmSelfdestructRestriction; public boolean allowTvmOsaka; public boolean allowHardenResourceCalculation; - public boolean allowOptimizeTvmStorage; + public boolean allowOptimizeTvmStorageKey; } // HEAD / block-processing config, written by the consensus path; read by everyone with no @@ -205,8 +205,8 @@ public static void initAllowHardenResourceCalculation(long allow) { globalSnapshot.allowHardenResourceCalculation = allow == 1; } - public static void initAllowOptimizeTvmStorage(long allow) { - globalSnapshot.allowOptimizeTvmStorage = allow == 1; + public static void initAllowOptimizeTvmStorageKey(long allow) { + globalSnapshot.allowOptimizeTvmStorageKey = allow == 1; } public static boolean getEnergyLimitHardFork() { @@ -317,7 +317,7 @@ public static boolean allowHardenResourceCalculation() { return current().allowHardenResourceCalculation; } - public static boolean allowOptimizeTvmStorage() { - return current().allowOptimizeTvmStorage; + public static boolean allowOptimizeTvmStorageKey() { + return current().allowOptimizeTvmStorageKey; } } diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 7d0b0634c7e..b7fa128c701 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -1529,6 +1529,11 @@ public Protocol.ChainParameters getChainParameters() { .setValue(dbManager.getDynamicPropertiesStore().getAllowOptimizeTvmStorage()) .build()); + builder.addChainParameter(Protocol.ChainParameters.ChainParameter.newBuilder() + .setKey("getAllowOptimizeTvmStorageKey") + .setValue(dbManager.getDynamicPropertiesStore().getAllowOptimizeTvmStorageKey()) + .build()); + return builder.build(); } diff --git a/framework/src/main/java/org/tron/core/consensus/ProposalService.java b/framework/src/main/java/org/tron/core/consensus/ProposalService.java index 0442078fdec..bfe51059af1 100644 --- a/framework/src/main/java/org/tron/core/consensus/ProposalService.java +++ b/framework/src/main/java/org/tron/core/consensus/ProposalService.java @@ -416,6 +416,11 @@ public static boolean process(Manager manager, ProposalCapsule proposalCapsule) manager.getDynamicPropertiesStore().saveAllowOptimizeTvmStorage(entry.getValue()); break; } + case ALLOW_OPTIMIZE_TVM_STORAGE_KEY: { + manager.getDynamicPropertiesStore() + .saveAllowOptimizeTvmStorageKey(entry.getValue()); + break; + } default: find = false; break; diff --git a/framework/src/main/java/org/tron/core/db/HistoryBlockHashUtil.java b/framework/src/main/java/org/tron/core/db/HistoryBlockHashUtil.java index cfcf573992f..088ee4acdf5 100644 --- a/framework/src/main/java/org/tron/core/db/HistoryBlockHashUtil.java +++ b/framework/src/main/java/org/tron/core/db/HistoryBlockHashUtil.java @@ -151,7 +151,7 @@ public static void write(Manager manager, BlockCapsule block) { } long slot = (block.getNum() - 1) % HISTORY_SERVE_WINDOW; Storage storage = new Storage(HISTORY_STORAGE_ADDRESS, manager.getStorageRowStore(), - manager.getDynamicPropertiesStore().allowOptimizeTvmStorage()); + manager.getDynamicPropertiesStore()); storage.put(new DataWord(slot), new DataWord(block.getParentHash().getBytes())); storage.commit(); } diff --git a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java index bda7be2b648..b779738101d 100644 --- a/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java +++ b/framework/src/main/java/org/tron/core/services/jsonrpc/TronJsonRpcImpl.java @@ -628,7 +628,7 @@ public String getStorageAt(String address, String storageIdx, String blockNumOrT StorageRowStore store = manager.getStorageRowStore(); Storage storage = new Storage(addressByte, store, - manager.getDynamicPropertiesStore().allowOptimizeTvmStorage()); + manager.getDynamicPropertiesStore()); storage.setContractVersion(smartContract.getVersion()); storage.generateAddrHash(smartContract.getTrxHash().toByteArray()); diff --git a/framework/src/main/java/org/tron/program/Version.java b/framework/src/main/java/org/tron/program/Version.java index d7f322f0525..ec0a357e721 100644 --- a/framework/src/main/java/org/tron/program/Version.java +++ b/framework/src/main/java/org/tron/program/Version.java @@ -4,7 +4,7 @@ public class Version { public static final String VERSION_NAME = "GreatVoyage-v4.8.2.2-1-gf3e81404fe"; public static final String VERSION_CODE = "18830"; - private static final String VERSION = "4.8.2.3"; + private static final String VERSION = "4.8.2.4"; public static String getVersion() { return VERSION; diff --git a/framework/src/test/java/org/tron/common/runtime/vm/OptimizeTvmStorageTest.java b/framework/src/test/java/org/tron/common/runtime/vm/OptimizeTvmStorageTest.java index c0120059b84..201584acc79 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/OptimizeTvmStorageTest.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/OptimizeTvmStorageTest.java @@ -31,13 +31,15 @@ public class OptimizeTvmStorageTest extends VMTestBase { @Override protected void beforeDestroy() { - VMConfig.initAllowOptimizeTvmStorage(0); + VMConfig.initAllowOptimizeTvmStorageKey(0); + dbManager.getDynamicPropertiesStore().saveAllowOptimizeTvmStorageKey(0); } @Test public void optimizedKeyKeepsFullSlotAndTombstone() { activateFork(); - VMConfig.initAllowOptimizeTvmStorage(1); + dbManager.getDynamicPropertiesStore().saveAllowOptimizeTvmStorageKey(1); + VMConfig.initAllowOptimizeTvmStorageKey(1); byte[] address = account("00000000000000000000000000000000000000c1"); rootRepository.createAccount(address, AccountType.Normal); rootRepository.addBalance(address, 1_000_000L); @@ -77,12 +79,14 @@ public void optimizedKeyKeepsFullSlotAndTombstone() { DataWord after = RepositoryImpl.createRoot(StoreFactory.getInstance()) .getStorageValue(address, new DataWord(legacyOnly)); Assert.assertEquals(9L, after.longValue()); + dbManager.getDynamicPropertiesStore().saveAllowOptimizeTvmStorageKey(0); } @Test public void migratedWriteClearsSharedLegacyRow() { activateFork(); - VMConfig.initAllowOptimizeTvmStorage(1); + dbManager.getDynamicPropertiesStore().saveAllowOptimizeTvmStorageKey(1); + VMConfig.initAllowOptimizeTvmStorageKey(1); byte[] address = account("00000000000000000000000000000000000000c3"); rootRepository.createAccount(address, AccountType.Normal); rootRepository.commit(); @@ -106,6 +110,7 @@ public void migratedWriteClearsSharedLegacyRow() { byte[] stored = dbManager.getStorageRowStore() .get(optimizedKey(address, slotA)).getValue(); Assert.assertTrue(new DataWord(stored).isZero()); + dbManager.getDynamicPropertiesStore().saveAllowOptimizeTvmStorageKey(0); } @Test @@ -122,7 +127,7 @@ public void zeroThenNonZeroKeepsLegacyChargeBeforeProposal() throws Exception { @Test public void zeroThenNonZeroKeepsResetChargeWhenOptimized() throws Exception { activateFork(); - dbManager.getDynamicPropertiesStore().saveAllowOptimizeTvmStorage(1); + dbManager.getDynamicPropertiesStore().saveAllowOptimizeTvmStorageKey(1); try { long fresh = deploy("FreshSet", "600160005500").getResult().getEnergyUsed(); long zeroThenSet = deploy("ZeroThenSet", "6000600055600160005500") @@ -131,7 +136,7 @@ public void zeroThenNonZeroKeepsResetChargeWhenOptimized() throws Exception { // following non-zero write is RESET (5000), same as before the proposal. Assert.assertEquals(-9994L, zeroThenSet - fresh); } finally { - dbManager.getDynamicPropertiesStore().saveAllowOptimizeTvmStorage(0); + dbManager.getDynamicPropertiesStore().saveAllowOptimizeTvmStorageKey(0); } } @@ -139,8 +144,8 @@ public void zeroThenNonZeroKeepsResetChargeWhenOptimized() throws Exception { public void tombstoneThenNonZeroChargesAsFirstWrite() throws Exception { activateFork(); dbManager.getDynamicPropertiesStore().saveAllowTvmConstantinople(1); - dbManager.getDynamicPropertiesStore().saveAllowOptimizeTvmStorage(1); - VMConfig.initAllowOptimizeTvmStorage(1); + dbManager.getDynamicPropertiesStore().saveAllowOptimizeTvmStorageKey(1); + VMConfig.initAllowOptimizeTvmStorageKey(1); try { String init = "6006600c60003960066000f3600160005500"; Runtime seededRuntime = deploy("Seeded", init); @@ -174,7 +179,7 @@ public void tombstoneThenNonZeroChargesAsFirstWrite() throws Exception { .getStorageValue(seeded, new DataWord(0)); Assert.assertEquals(1L, written.longValue()); } finally { - dbManager.getDynamicPropertiesStore().saveAllowOptimizeTvmStorage(0); + dbManager.getDynamicPropertiesStore().saveAllowOptimizeTvmStorageKey(0); dbManager.getDynamicPropertiesStore().saveAllowTvmConstantinople(0); } } diff --git a/framework/src/test/java/org/tron/common/runtime/vm/StorageMigrationTest.java b/framework/src/test/java/org/tron/common/runtime/vm/StorageMigrationTest.java new file mode 100644 index 00000000000..a23244c23e9 --- /dev/null +++ b/framework/src/test/java/org/tron/common/runtime/vm/StorageMigrationTest.java @@ -0,0 +1,469 @@ +package org.tron.common.runtime.vm; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.tron.protos.Protocol.Transaction.Result.contractResult.ILLEGAL_OPERATION; +import static org.tron.protos.Protocol.Transaction.Result.contractResult.OUT_OF_TIME; +import static org.tron.protos.Protocol.Transaction.Result.contractResult.REVERT; +import static org.tron.protos.Protocol.Transaction.Result.contractResult.SUCCESS; + +import com.google.protobuf.ByteString; +import java.util.Arrays; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Test; +import org.springframework.test.util.ReflectionTestUtils; +import org.tron.common.crypto.Hash; +import org.tron.common.runtime.TvmTestUtils; +import org.tron.common.utils.ByteUtil; +import org.tron.common.utils.ForkController; +import org.tron.core.actuator.VMActuator; +import org.tron.core.capsule.ContractCapsule; +import org.tron.core.capsule.StorageRowCapsule; +import org.tron.core.capsule.TransactionCapsule; +import org.tron.core.config.Parameter.ForkBlockVersionEnum; +import org.tron.core.db.TransactionContext; +import org.tron.core.db2.ISession; +import org.tron.core.store.DynamicPropertiesStore; +import org.tron.core.store.StoreFactory; +import org.tron.core.vm.config.ConfigLoader; +import org.tron.core.vm.config.VMConfig; +import org.tron.core.vm.program.Storage; +import org.tron.core.vm.repository.Repository; +import org.tron.core.vm.repository.RepositoryImpl; +import org.tron.protos.Protocol.AccountType; +import org.tron.protos.Protocol.Transaction; +import org.tron.protos.Protocol.Transaction.Result.contractResult; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract; + +/** + * Exercises storage migration through real VM transactions and revocable database sessions. + * Contract version 0 slots with identical low 128 bits share a legacy physical row. + */ +public class StorageMigrationTest extends VMTestBase { + + private static final String STOP = "00"; + private static final String REVERT_CODE = "60006000fd"; + private static final String INVALID = "fe"; + private static final String READ = "60005450"; // SLOAD(0), POP + private static final String WRITE = "6009600055"; // SSTORE(0, 9) + private static final String WRITE_ZERO = "6000600055"; + private static final String WRITE_OTHER = "6003600155"; // SSTORE(1, 3) + private static final String ALIAS = "00000000000000000000000000000001" + + "00000000000000000000000000000000"; + private static final String READ_ALIAS = "7f" + ALIAS + "5450"; + private static final String WRITE_ALIAS = "60087f" + ALIAS + "55"; // SSTORE(2^128, 8) + + private int addressSequence = 0x1000; + private VMConfig.Snapshot savedGlobalConfig; + private boolean savedConfigLoaderDisabled; + private boolean savedEnergyLimitHardFork; + private boolean savedVmTrace; + + @Override + protected void beforeContext() { + savedGlobalConfig = (VMConfig.Snapshot) ReflectionTestUtils.getField(VMConfig.class, + "globalSnapshot"); + // Detach the saved object before any init* setter mutates the global snapshot in place. + VMConfig.setGlobalSnapshot(new VMConfig.Snapshot()); + savedConfigLoaderDisabled = ConfigLoader.disable; + savedEnergyLimitHardFork = VMConfig.getEnergyLimitHardFork(); + savedVmTrace = VMConfig.vmTrace(); + ConfigLoader.disable = false; + } + + @Override + protected void afterInit() { + super.afterInit(); + ForkController.instance().init(chainBaseManager); + byte[] stats = new byte[27]; + Arrays.fill(stats, (byte) 1); + for (ForkBlockVersionEnum fork : new ForkBlockVersionEnum[]{ + ForkBlockVersionEnum.VERSION_4_8_2_3, ForkBlockVersionEnum.VERSION_4_8_2_4}) { + properties().statsByVersion(fork.getValue(), stats); + } + long interval = properties().getMaintenanceTimeInterval(); + long forkTime = ((ForkBlockVersionEnum.VERSION_4_8_2_4.getHardForkTime() - 1) + / interval + 1) * interval; + properties().saveLatestBlockHeaderTimestamp(forkTime + 1); + properties().saveLatestBlockHeaderNumber(100_000_000L); + properties().saveAllowTvmConstantinople(1); + properties().saveAllowTvmSolidity059(1); + setMode(0, 1); + VMConfig.initVmHardFork(true); + } + + @Override + protected void beforeDestroy() { + VMConfig.setGlobalSnapshot(savedGlobalConfig); + VMConfig.initVmHardFork(savedEnergyLimitHardFork); + VMConfig.setVmTrace(savedVmTrace); + ConfigLoader.disable = savedConfigLoaderDisabled; + } + + @Test + public void neitherProposalUsesLegacyLayout() throws Exception { + assertProposalMode(0, 0); + } + + @Test + public void proposal99AloneMigratesReads() throws Exception { + assertProposalMode(1, 0); + } + + @Test + public void proposal102AloneMigratesOnlyWrites() throws Exception { + assertProposalMode(0, 1); + } + + @Test + public void bothProposalsUse102ReadProtection() throws Exception { + assertProposalMode(1, 1); + } + + private void assertProposalMode(int proposal99, int proposal102) throws Exception { + setMode(proposal99, proposal102); + String scenario = "99=" + proposal99 + ", 102=" + proposal102; + boolean readMigrates = proposal99 == 1 && proposal102 == 0; + boolean newLayout = proposal99 == 1 || proposal102 == 1; + + byte[] reader = install(READ + STOP); + seedLegacyRow(reader); + assertResult(scenario + " read", SUCCESS, reader); + assertLayout(scenario + " read", reader, readMigrates, 7); + + byte[] aliasReader = install(READ + READ_ALIAS + STOP); + seedLegacyRow(aliasReader); + assertResult(scenario + " alias", readMigrates ? SUCCESS : OUT_OF_TIME, aliasReader); + assertLayout(scenario + " alias", aliasReader, readMigrates, 7); + assertFalse(scenario, has(newKey(aliasReader, aliasSlot()))); + + for (String end : new String[]{STOP, REVERT_CODE, INVALID}) { + byte[] writer = install(WRITE + end); + seedLegacyRow(writer); + assertResult(scenario + " write, end=" + end, resultFor(end), writer); + boolean success = STOP.equals(end); + assertLayout(scenario + " write, end=" + end, writer, success && newLayout, + success ? 9 : 7); + } + } + + @Test + public void topLevelReadsDoNotMigrateAndWritesMigrateOnlyOnSuccess() throws Exception { + for (String operations : new String[]{READ, READ + WRITE_OTHER, READ + WRITE, + WRITE, WRITE_ZERO}) { + for (String end : new String[]{STOP, REVERT_CODE, INVALID}) { + String scenario = "operations=" + operations + ", end=" + end; + byte[] address = install(operations + end); + seedLegacyRow(address); + assertResult(scenario, resultFor(end), address); + boolean dirty = operations.equals(READ + WRITE) || operations.equals(WRITE) + || operations.equals(WRITE_ZERO); + boolean migrated = STOP.equals(end) && dirty; + assertLayout(scenario, address, migrated, + migrated ? (operations.equals(WRITE_ZERO) ? 0 : 9) : 7); + if (operations.equals(READ + WRITE_OTHER)) { + byte[] otherKey = newKey(address, new DataWord(1)); + assertEquals(scenario, STOP.equals(end), has(otherKey)); + if (STOP.equals(end)) { + assertStored(scenario, otherKey, 3); + } + } + } + } + } + + @Test + public void nestedCallsCommitOnlyDirtySuccessfulScopes() throws Exception { + // CALL, CALLCODE, DELEGATECALL, STATICCALL; the middle two use the caller's storage. + for (String opcode : new String[]{"f1", "f2", "f4", "fa"}) { + for (boolean write : new boolean[]{false, true}) { + for (String childEnd : new String[]{STOP, REVERT_CODE, INVALID}) { + for (boolean parentSuccess : new boolean[]{false, true}) { + String scenario = "opcode=" + opcode + ", write=" + write + ", childEnd=" + + childEnd + ", parentSuccess=" + parentSuccess; + byte[] child = install(READ + (write ? WRITE : "") + childEnd); + byte[] parent = install(call(child, opcode) + + (parentSuccess ? STOP : REVERT_CODE)); + byte[] storageOwner = opcode.equals("f2") || opcode.equals("f4") ? parent : child; + seedLegacyRow(storageOwner); + assertResult(scenario, parentSuccess ? SUCCESS : REVERT, parent); + boolean migrated = parentSuccess && write && childEnd.equals(STOP) + && !opcode.equals("fa"); + assertLayout(scenario, storageOwner, migrated, migrated ? 9 : 7); + } + } + } + } + } + + @Test + public void aliasReadWriteCombinationsAbortWholeTransactionWithLegacyRow() throws Exception { + for (String first : new String[]{READ, WRITE}) { + for (String second : new String[]{READ_ALIAS, WRITE_ALIAS}) { + String scenario = "first=" + first + ", second=" + second; + byte[] address = install(first + second + STOP); + seedLegacyRow(address); + assertResult(scenario, OUT_OF_TIME, address); + assertLayout(scenario, address, false, 7); + assertFalse(scenario, has(newKey(address, aliasSlot()))); + } + } + byte[] fresh = install(WRITE + WRITE_ALIAS + STOP); + assertResult("no legacy row", SUCCESS, fresh); + assertLayout("no legacy row", fresh, true, 9); + assertStored("independent alias slot", newKey(fresh, aliasSlot()), 8); + } + + @Test + public void committedReadOwnershipPropagatesButRevertedReadOwnershipDoesNot() throws Exception { + for (boolean childSuccess : new boolean[]{false, true}) { + String scenario = "childSuccess=" + childSuccess; + byte[] child = install(READ + (childSuccess ? STOP : REVERT_CODE)); + byte[] parent = install(call(child, "f4") + READ_ALIAS + STOP); + seedLegacyRow(parent); + assertResult(scenario, childSuccess ? OUT_OF_TIME : SUCCESS, parent); + assertLayout(scenario, parent, false, 7); + assertFalse(scenario, has(newKey(parent, aliasSlot()))); + } + byte[] child = install(READ_ALIAS + STOP); + byte[] parent = install(READ + call(child, "f4") + STOP); + seedLegacyRow(parent); + assertResult("child inherits parent's read ownership", OUT_OF_TIME, parent); + assertLayout("child failure aborts parent", parent, false, 7); + assertFalse(has(newKey(parent, aliasSlot()))); + } + + @Test + public void crossTransactionReadsKeepOldRowUntilWriteAndAllowAliasesAfterMigration() + throws Exception { + byte[] address = install(READ + STOP); + seedLegacyRow(address); + assertResult("first read", SUCCESS, address); + assertLayout("first read", address, false, 7); + + replaceCode(address, READ_ALIAS + STOP); + assertResult("alias read in next transaction", SUCCESS, address); + assertLayout("alias read in next transaction", address, false, 7); + assertFalse(has(newKey(address, aliasSlot()))); + + replaceCode(address, WRITE + STOP); + assertResult("write migrates", SUCCESS, address); + assertLayout("write migrates", address, true, 9); + + replaceCode(address, READ + WRITE_ALIAS + STOP); + assertResult("alias after migration", SUCCESS, address); + assertLayout("alias after migration", address, true, 9); + assertStored("alias after migration", newKey(address, aliasSlot()), 8); + } + + @Test + public void constantCallDoesNotPersistMigration() throws Exception { + byte[] address = install(WRITE + STOP); + seedLegacyRow(address); + TransactionContext context = new TransactionContext(null, + new TransactionCapsule(transaction(address)), StoreFactory.getInstance(), false, true); + VMActuator actuator = new VMActuator(true); + actuator.validate(context); + actuator.execute(context); + assertNull(context.getProgramResult().getException()); + assertFalse(context.getProgramResult().isRevert()); + assertLayout("constant call", address, false, 7); + } + + @Test + public void revokingSuccessfulTransactionRestoresLegacyRow() throws Exception { + byte[] address = install(WRITE + STOP); + seedLegacyRow(address); + try (ISession ignored = dbManager.getRevokingStore().buildSession()) { + assertResult("before session rollback", SUCCESS, address); + assertLayout("before session rollback", address, true, 9); + } + assertLayout("after session rollback", address, false, 7); + } + + @Test + public void proposalActivationAndRollbackRestoreFlagsAndRows() throws Exception { + // Activate either proposal first, then activate the other. Rollback is the only disabling path. + for (int[] transition : new int[][]{{0, 0, 1, 0}, {0, 0, 0, 1}, + {1, 0, 1, 1}, {0, 1, 1, 1}}) { + int before99 = transition[0]; + int before102 = transition[1]; + int after99 = transition[2]; + int after102 = transition[3]; + String scenario = Arrays.toString(transition); + setMode(before99, before102); + boolean readMigratesBefore = before99 == 1 && before102 == 0; + boolean readMigratesAfter = after99 == 1 && after102 == 0; + + byte[] existing = install(READ + STOP); + seedLegacyRow(existing); + assertResult(scenario + " existing read", SUCCESS, existing); + assertLayout(scenario + " existing read", existing, readMigratesBefore, 7); + byte[] reader = install(READ + STOP); + seedLegacyRow(reader); + byte[] writer = install(WRITE + STOP); + seedLegacyRow(writer); + + try (ISession ignored = dbManager.getRevokingStore().buildSession()) { + setMode(after99, after102); + assertResult(scenario + " existing row after activation", SUCCESS, existing); + assertLayout(scenario + " existing row after activation", existing, + readMigratesBefore || readMigratesAfter, 7); + assertEquals(scenario, new DataWord(7), + repository().getStorageValue(existing, DataWord.ZERO())); + assertResult(scenario + " read after activation", SUCCESS, reader); + assertLayout(scenario + " read after activation", reader, readMigratesAfter, 7); + assertResult(scenario + " write after activation", SUCCESS, writer); + assertLayout(scenario + " write after activation", writer, true, 9); + } + + assertEquals(scenario, before99, properties().getAllowOptimizeTvmStorage()); + assertEquals(scenario, before102, properties().getAllowOptimizeTvmStorageKey()); + assertLayout(scenario + " existing row after rollback", existing, readMigratesBefore, 7); + assertLayout(scenario + " reader after rollback", reader, false, 7); + assertLayout(scenario + " writer after rollback", writer, false, 7); + // ConfigLoader must reload the rolled-back flags even though the last VM run used new flags. + assertResult(scenario + " read after rollback", SUCCESS, reader); + assertLayout(scenario + " read after rollback", reader, readMigratesBefore, 7); + assertResult(scenario + " write after rollback", SUCCESS, writer); + assertLayout(scenario + " write after rollback", writer, + before99 == 1 || before102 == 1, 9); + } + } + + @Test + public void databaseConstructorAndRepositoryIgnoreStaleVmFlags() { + for (int proposal99 : new int[]{0, 1}) { + for (int proposal102 : new int[]{0, 1}) { + setMode(proposal99, proposal102); + boolean readMigrates = proposal99 == 1 && proposal102 == 0; + // Exercise stale flags in both directions, including a leftover constant-call snapshot. + for (boolean staleFlag : new boolean[]{false, true}) { + String scenario = "99=" + proposal99 + ", 102=" + proposal102 + ", stale=" + staleFlag; + VMConfig.initAllowOptimizeTvmStorageKey(staleFlag ? 1 : 0); + VMConfig.Snapshot local = new VMConfig.Snapshot(); + local.allowOptimizeTvmStorageKey = staleFlag; + VMConfig.setLocalSnapshot(local); + try { + byte[] directAddress = install(STOP); + seedLegacyRow(directAddress); + Storage direct = new Storage(directAddress, dbManager.getStorageRowStore(), + properties()); + assertEquals(scenario, new DataWord(7), direct.getValue(DataWord.ZERO())); + direct.commit(); + assertLayout(scenario + " direct Storage", directAddress, readMigrates, 7); + + byte[] repositoryAddress = install(STOP); + seedLegacyRow(repositoryAddress); + Repository repository = repository(); + assertEquals(scenario, new DataWord(7), + repository.getStorageValue(repositoryAddress, DataWord.ZERO())); + repository.commit(); + assertLayout(scenario + " repository", repositoryAddress, readMigrates, 7); + } finally { + VMConfig.clearLocalSnapshot(); + } + } + } + } + } + + private DynamicPropertiesStore properties() { + return dbManager.getDynamicPropertiesStore(); + } + + private void setMode(int proposal99, int proposal102) { + properties().saveAllowOptimizeTvmStorage(proposal99); + properties().saveAllowOptimizeTvmStorageKey(proposal102); + } + + private Repository repository() { + return RepositoryImpl.createRoot(StoreFactory.getInstance()); + } + + private byte[] install(String bytecode) { + byte[] address = Hex.decode(String.format("41%040x", addressSequence++)); + Repository repository = repository(); + repository.createAccount(address, AccountType.Contract); + repository.createContract(address, new ContractCapsule(SmartContract.newBuilder() + .setContractAddress(ByteString.copyFrom(address)) + .setOriginAddress(ByteString.copyFrom(Hex.decode(OWNER_ADDRESS))) + .setConsumeUserResourcePercent(100).build())); + repository.saveCode(address, Hex.decode(bytecode)); + repository.commit(); + return address; + } + + private void replaceCode(byte[] address, String bytecode) { + Repository repository = repository(); + repository.saveCode(address, Hex.decode(bytecode)); + repository.commit(); + } + + private Transaction transaction(byte[] address) { + return TvmTestUtils.generateTriggerSmartContractAndGetTransaction( + Hex.decode(OWNER_ADDRESS), address, new byte[0], 0, 1_000_000_000L); + } + + private void assertResult(String scenario, contractResult expected, byte[] address) + throws Exception { + assertEquals(scenario, expected, TvmTestUtils.processTransactionAndReturnRuntime( + transaction(address), dbManager, null).getResult().getResultCode()); + } + + private contractResult resultFor(String end) { + return STOP.equals(end) ? SUCCESS : (REVERT_CODE.equals(end) ? REVERT : ILLEGAL_OPERATION); + } + + private DataWord aliasSlot() { + return new DataWord(Hex.decode(ALIAS)); + } + + // Derive the physical keys independently of Storage, so a wrong layout cannot validate itself. + private byte[] oldKey(byte[] address) { + byte[] key = new byte[32]; + System.arraycopy(Hash.sha3(address), 0, key, 0, 16); + return key; + } + + private byte[] newKey(byte[] address, DataWord slot) { + byte[] hash = Hash.sha3(address); + byte[] key = new byte[48]; + System.arraycopy(hash, 0, key, 0, 16); + System.arraycopy(Hash.sha3(ByteUtil.merge(hash, slot.getData())), 0, key, 16, 32); + return key; + } + + private void seedLegacyRow(byte[] address) { + dbManager.getStorageRowStore().put(oldKey(address), + new StorageRowCapsule(new DataWord(7).getData())); + } + + private boolean has(byte[] key) { + return dbManager.getStorageRowStore().has(key); + } + + private void assertStored(String scenario, byte[] key, long expected) { + StorageRowCapsule row = dbManager.getStorageRowStore().get(key); + assertNotNull(scenario + " row", row); + assertNotNull(scenario + " value", row.getValue()); + assertEquals(scenario, new DataWord(expected), new DataWord(row.getValue())); + } + + private void assertLayout(String scenario, byte[] address, boolean migrated, long value) { + assertEquals(scenario + " legacy row", !migrated, has(oldKey(address))); + assertEquals(scenario + " new row", migrated, has(newKey(address, DataWord.ZERO()))); + assertStored(scenario, migrated ? newKey(address, DataWord.ZERO()) : oldKey(address), value); + } + + private String call(byte[] target, String opcode) { + String arguments = "6000600060006000"; // Empty input and output ranges. + if (opcode.equals("f1") || opcode.equals("f2")) { + arguments += "6000"; // CALL and CALLCODE also pop a value argument. + } + return arguments + "73" + Hex.toHexString(Arrays.copyOfRange(target, 1, 21)) + + "620186a0" + opcode + "50"; // Forward 100,000 energy; discard the child status. + } +} diff --git a/framework/src/test/java/org/tron/common/runtime/vm/VMConfigIsolationTest.java b/framework/src/test/java/org/tron/common/runtime/vm/VMConfigIsolationTest.java index 6dc561034db..ff4fb450d23 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/VMConfigIsolationTest.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/VMConfigIsolationTest.java @@ -103,7 +103,7 @@ private static VMConfig.Snapshot snapshotGlobal() { snapshot.allowTvmSelfdestructRestriction = VMConfig.allowTvmSelfdestructRestriction(); snapshot.allowTvmOsaka = VMConfig.allowTvmOsaka(); snapshot.allowHardenResourceCalculation = VMConfig.allowHardenResourceCalculation(); - snapshot.allowOptimizeTvmStorage = VMConfig.allowOptimizeTvmStorage(); + snapshot.allowOptimizeTvmStorageKey = VMConfig.allowOptimizeTvmStorageKey(); return snapshot; } } diff --git a/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java b/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java index 16a3cb3a5bb..fabd0c45f25 100644 --- a/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java +++ b/framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java @@ -349,6 +349,8 @@ public void validateCheck() { testAllowHardenExchangeCalculationProposal(); + testAllowOptimizeTvmStorageKeyProposal(); + forkUtils.getManager().getDynamicPropertiesStore() .statsByVersion(ForkBlockVersionEnum.ENERGY_LIMIT.getValue(), stats); forkUtils.reset(); @@ -744,6 +746,79 @@ private void testAllowHardenExchangeCalculationProposal() { } } + private void testAllowOptimizeTvmStorageKeyProposal() { + long legacy = ProposalType.ALLOW_OPTIMIZE_TVM_STORAGE.getCode(); + long code = ProposalType.ALLOW_OPTIMIZE_TVM_STORAGE_KEY.getCode(); + byte[] cleared = new byte[27]; + forkUtils.getManager().getDynamicPropertiesStore() + .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_2_3.getValue(), cleared); + forkUtils.getManager().getDynamicPropertiesStore() + .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_2_4.getValue(), cleared); + dynamicPropertiesStore.saveAllowOptimizeTvmStorage(0); + + ContractValidateException bannedEx = assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, legacy, 1)); + assertEquals("Bad chain parameter id [ALLOW_OPTIMIZE_TVM_STORAGE]", bannedEx.getMessage()); + + activateFork(ForkBlockVersionEnum.VERSION_4_8_2_3); + ContractValidateException newBeforeFork = assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, code, 1)); + assertEquals("Bad chain parameter id [ALLOW_OPTIMIZE_TVM_STORAGE_KEY]", + newBeforeFork.getMessage()); + ContractValidateException legacyValue = assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, legacy, 0)); + assertEquals("This value[ALLOW_OPTIMIZE_TVM_STORAGE] is only allowed to be 1", + legacyValue.getMessage()); + try { + ProposalUtil.validator(dynamicPropertiesStore, forkUtils, legacy, 1); + } catch (ContractValidateException e) { + Assert.fail(e.getMessage()); + } + Protocol.Proposal legacyProposal = Protocol.Proposal.newBuilder() + .putParameters(legacy, 1).build(); + Assert.assertTrue(ProposalService.process(dbManager, new ProposalCapsule(legacyProposal))); + Assert.assertEquals(1L, dynamicPropertiesStore.getAllowOptimizeTvmStorage()); + Assert.assertTrue(dynamicPropertiesStore.allowTvmStorageOptimization()); + Assert.assertFalse(dynamicPropertiesStore.allowOptimizeTvmStorageKey()); + ContractValidateException legacyAgain = assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, legacy, 1)); + assertEquals("[ALLOW_OPTIMIZE_TVM_STORAGE] has been valid, no need to propose again", + legacyAgain.getMessage()); + + dynamicPropertiesStore.saveAllowOptimizeTvmStorage(0); + activateFork(ForkBlockVersionEnum.VERSION_4_8_2_4); + + try { + ProposalUtil.validator(dynamicPropertiesStore, forkUtils, legacy, 1); + } catch (ContractValidateException e) { + Assert.fail(e.getMessage()); + } + Assert.assertTrue(ProposalService.process(dbManager, new ProposalCapsule(legacyProposal))); + Assert.assertEquals(1L, dynamicPropertiesStore.getAllowOptimizeTvmStorage()); + + ContractValidateException badValue = assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, code, 0)); + assertEquals("This value[ALLOW_OPTIMIZE_TVM_STORAGE_KEY] is only allowed to be 1", + badValue.getMessage()); + + try { + ProposalUtil.validator(dynamicPropertiesStore, forkUtils, code, 1); + } catch (ContractValidateException e) { + Assert.fail(e.getMessage()); + } + + Protocol.Proposal proposal = Protocol.Proposal.newBuilder().putParameters(code, 1).build(); + ProposalService.process(dbManager, new ProposalCapsule(proposal)); + Assert.assertEquals(1L, dynamicPropertiesStore.getAllowOptimizeTvmStorageKey()); + Assert.assertTrue(dynamicPropertiesStore.allowTvmStorageOptimization()); + + ContractValidateException again = assertThrows(ContractValidateException.class, + () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, code, 1)); + assertEquals( + "[ALLOW_OPTIMIZE_TVM_STORAGE_KEY] has been valid, no need to propose again", + again.getMessage()); + } + private void testAllowMarketTransaction() { ThrowingRunnable off = () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils, ProposalType.ALLOW_MARKET_TRANSACTION.getCode(), 0); diff --git a/framework/src/test/java/org/tron/core/db/HistoryBlockHashIntegrationTest.java b/framework/src/test/java/org/tron/core/db/HistoryBlockHashIntegrationTest.java index ff2eb5163ae..25eea18b00f 100644 --- a/framework/src/test/java/org/tron/core/db/HistoryBlockHashIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db/HistoryBlockHashIntegrationTest.java @@ -67,7 +67,7 @@ public void resetState() { private Storage openStorage() { return new Storage(HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS, dbManager.getStorageRowStore(), - dbManager.getDynamicPropertiesStore().allowOptimizeTvmStorage()); + dbManager.getDynamicPropertiesStore()); } private DataWord readSlot(long slot) { diff --git a/framework/src/test/java/org/tron/core/db/HistoryBlockHashVmTest.java b/framework/src/test/java/org/tron/core/db/HistoryBlockHashVmTest.java index 442b7c397f3..d312db54d87 100644 --- a/framework/src/test/java/org/tron/core/db/HistoryBlockHashVmTest.java +++ b/framework/src/test/java/org/tron/core/db/HistoryBlockHashVmTest.java @@ -118,7 +118,7 @@ private BlockCapsule blockAt(long num) { private Storage openStorage() { return new Storage(HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS, dbManager.getStorageRowStore(), - dbManager.getDynamicPropertiesStore().allowOptimizeTvmStorage()); + dbManager.getDynamicPropertiesStore()); } private static byte[] uint256(long n) { @@ -249,8 +249,9 @@ public void vmExecutionDoesNotInvalidOpcodeUnderShanghai() throws Exception { @Test public void writeUsesDatabaseFlagWhenVmConfigIsOff() { - VMConfig.initAllowOptimizeTvmStorage(0); - chainBaseManager.getDynamicPropertiesStore().saveAllowOptimizeTvmStorage(1); + VMConfig.initAllowOptimizeTvmStorageKey(0); + DynamicPropertiesStore dynamicStore = chainBaseManager.getDynamicPropertiesStore(); + dynamicStore.saveAllowOptimizeTvmStorageKey(1); byte[] parent = new byte[32]; Arrays.fill(parent, (byte) 0x11); BlockCapsule block = new BlockCapsule( @@ -265,16 +266,16 @@ public void writeUsesDatabaseFlagWhenVmConfigIsOff() { Storage optimized = new Storage( HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS, chainBaseManager.getStorageRowStore(), - true); + dynamicStore); + dynamicStore.saveAllowOptimizeTvmStorageKey(0); Storage legacy = new Storage( HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS, chainBaseManager.getStorageRowStore(), - false); + dynamicStore); assertEquals(expected, optimized.getValue(new DataWord(slot))); assertNull(legacy.getValue(new DataWord(slot))); - assertFalse(VMConfig.allowOptimizeTvmStorage()); + assertFalse(VMConfig.allowOptimizeTvmStorageKey()); - chainBaseManager.getDynamicPropertiesStore().saveAllowOptimizeTvmStorage(0); optimized.put(new DataWord(slot), DataWord.ZERO()); optimized.commit(); } From 4786eb09ef622f3f59db1a8dbd43b1cd15d19201 Mon Sep 17 00:00:00 2001 From: Asuka Date: Wed, 21 Oct 2026 16:59:06 +0800 Subject: [PATCH 2/3] opt: skip EXTCODEHASH metadata writes only in special cases --- .../org/tron/core/vm/program/Program.java | 4 + .../runtime/vm/StorageMigrationTest.java | 77 +++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/actuator/src/main/java/org/tron/core/vm/program/Program.java b/actuator/src/main/java/org/tron/core/vm/program/Program.java index 2be1eb80703..2effc82b68d 100644 --- a/actuator/src/main/java/org/tron/core/vm/program/Program.java +++ b/actuator/src/main/java/org/tron/core/vm/program/Program.java @@ -1315,6 +1315,10 @@ public byte[] getCodeHashAt(DataWord address) { if (ByteUtil.isNullOrZeroArray(codeHash)) { byte[] code = getCodeAt(address); codeHash = Hash.sha3(code); + if (VMConfig.allowOptimizeTvmStorageKey() + && ByteUtil.isNullOrZeroArray(code)) { + return codeHash; + } contract.setCodeHash(codeHash); getContractState().updateContract(tronAddr, contract); } diff --git a/framework/src/test/java/org/tron/common/runtime/vm/StorageMigrationTest.java b/framework/src/test/java/org/tron/common/runtime/vm/StorageMigrationTest.java index a23244c23e9..a2a0c1a7e10 100644 --- a/framework/src/test/java/org/tron/common/runtime/vm/StorageMigrationTest.java +++ b/framework/src/test/java/org/tron/common/runtime/vm/StorageMigrationTest.java @@ -1,5 +1,6 @@ package org.tron.common.runtime.vm; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -15,6 +16,7 @@ import org.junit.Test; import org.springframework.test.util.ReflectionTestUtils; import org.tron.common.crypto.Hash; +import org.tron.common.runtime.Runtime; import org.tron.common.runtime.TvmTestUtils; import org.tron.common.utils.ByteUtil; import org.tron.common.utils.ForkController; @@ -122,6 +124,69 @@ public void bothProposalsUse102ReadProtection() throws Exception { assertProposalMode(1, 1); } + @Test + public void extCodeHashSkipsEmptyCodeWriteWhenProposal102() throws Exception { + // Non-empty code is written back even when 102 is on. Only empty code skips the write. + for (boolean emptyCode : new boolean[]{false, true}) { + byte[] targetCode = emptyCode ? new byte[0] : Hex.decode("600160005500"); + for (int proposal99 : new int[]{0, 1}) { + for (int proposal102 : new int[]{0, 1}) { + setMode(proposal99, proposal102); + boolean skipWrite = proposal102 == 1 && emptyCode; + for (byte[] initialHash : new byte[][]{new byte[0], new byte[32], + new DataWord(123).getData()}) { + String scenario = "empty=" + emptyCode + ", 99=" + proposal99 + ", 102=" + proposal102 + + ", initialHash=" + Hex.toHexString(initialHash); + byte[] target = install(emptyCode ? "00" : Hex.toHexString(targetCode)); + if (emptyCode) { + dbManager.getCodeStore().delete(target); + } + setCodeHash(target, initialHash); + byte[] reader = install(readCodeHash(target) + "60005260206000f3"); + boolean missing = ByteUtil.isNullOrZeroArray(initialHash); + byte[] returned = missing ? Hash.sha3(targetCode) : initialHash; + byte[] stored = missing && !skipWrite ? Hash.sha3(targetCode) : initialHash; + + for (int read = 0; read < 2; read++) { + Runtime result = TvmTestUtils.processTransactionAndReturnRuntime( + transaction(reader), dbManager, null); + assertEquals(scenario, SUCCESS, result.getResult().getResultCode()); + assertArrayEquals(scenario, returned, result.getResult().getHReturn()); + assertArrayEquals(scenario, stored, + repository().getContract(target).getCodeHash()); + } + } + } + } + } + } + + @Test + public void extCodeHashMetadataRespectsTransactionAndSessionRollback() throws Exception { + byte[] targetCode = Hex.decode("600160005500"); + for (int proposal99 : new int[]{0, 1}) { + for (int proposal102 : new int[]{0, 1}) { + setMode(proposal99, proposal102); + String scenario = "99=" + proposal99 + ", 102=" + proposal102; + byte[] target = install(Hex.toHexString(targetCode)); + setCodeHash(target, new byte[0]); + for (String end : new String[]{REVERT_CODE, INVALID}) { + byte[] reader = install(readCodeHash(target) + "50" + end); + assertResult(scenario + ", end=" + end, resultFor(end), reader); + assertArrayEquals(scenario, new byte[0], repository().getContract(target).getCodeHash()); + } + + byte[] reader = install(readCodeHash(target) + "50" + STOP); + try (ISession ignored = dbManager.getRevokingStore().buildSession()) { + assertResult(scenario + " before session rollback", SUCCESS, reader); + assertArrayEquals(scenario, Hash.sha3(targetCode), + repository().getContract(target).getCodeHash()); + } + assertArrayEquals(scenario, new byte[0], repository().getContract(target).getCodeHash()); + } + } + } + private void assertProposalMode(int proposal99, int proposal102) throws Exception { setMode(proposal99, proposal102); String scenario = "99=" + proposal99 + ", 102=" + proposal102; @@ -402,6 +467,18 @@ private void replaceCode(byte[] address, String bytecode) { repository.commit(); } + private void setCodeHash(byte[] address, byte[] codeHash) { + Repository repository = repository(); + ContractCapsule contract = repository.getContract(address); + contract.setCodeHash(codeHash); + repository.updateContract(address, contract); + repository.commit(); + } + + private String readCodeHash(byte[] address) { + return "73" + Hex.toHexString(Arrays.copyOfRange(address, 1, 21)) + "3f"; + } + private Transaction transaction(byte[] address) { return TvmTestUtils.generateTriggerSmartContractAndGetTransaction( Hex.decode(OWNER_ADDRESS), address, new byte[0], 0, 1_000_000_000L); From 8ce1de112c135807a59358694719d296240a85fa Mon Sep 17 00:00:00 2001 From: Asuka Date: Thu, 22 Oct 2026 16:29:04 +0800 Subject: [PATCH 3/3] update a new version. version name:GreatVoyage-v4.8.2.3-4-gfa4dd52346,version code:18835 --- framework/src/main/java/org/tron/program/Version.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/src/main/java/org/tron/program/Version.java b/framework/src/main/java/org/tron/program/Version.java index ec0a357e721..002efd16b05 100644 --- a/framework/src/main/java/org/tron/program/Version.java +++ b/framework/src/main/java/org/tron/program/Version.java @@ -2,8 +2,8 @@ public class Version { - public static final String VERSION_NAME = "GreatVoyage-v4.8.2.2-1-gf3e81404fe"; - public static final String VERSION_CODE = "18830"; + public static final String VERSION_NAME = "GreatVoyage-v4.8.2.3-4-gfa4dd52346"; + public static final String VERSION_CODE = "18835"; private static final String VERSION = "4.8.2.4"; public static String getVersion() {