From 1e1167a88517733ebda4aa895ce4dca7c9f3cb86 Mon Sep 17 00:00:00 2001 From: Asuka Date: Thu, 16 Apr 2026 15:13:47 +0800 Subject: [PATCH 1/5] feat(vm): serve historical block hashes from state (TIP-2935) Port EIP-2935 to TRON: store recent block hashes in a system contract's storage so smart contracts can access up to 8191 blocks of history beyond the BLOCKHASH opcode's 256-block window. - New proposal ALLOW_TVM_PRAGUE(95), fork-gated by VERSION_4_8_2. - On activation, deploy the EIP-2935 runtime bytecode + ContractCapsule (version=0) + AccountCapsule(type=CONTRACT) at the canonical address 0x0000F90827F1C53a10cb7A02335B175320002935 via direct CodeStore / ContractStore / AccountStore writes. - On every block, before the transaction loop, write the parent block hash to StorageRowStore at slot (blockNum - 1) % 8191. The storage key layout replicates Storage.compose() for contractVersion=0 (addrHash[0:16] || slotKey[16:32]) so the written rows are readable via VM SLOAD. - BLOCKHASH opcode semantics unchanged; new storage is accessed by user contracts via STATICCALL to the deployed bytecode. Tests: round-trip equivalence (direct-write key = VM compose key), deploy idempotency, ring buffer modulo, end-to-end activation flow, and full VM repository read-back. --- .../org/tron/core/utils/ProposalUtil.java | 16 ++ .../core/store/DynamicPropertiesStore.java | 17 ++ .../common/parameter/CommonParameter.java | 4 + .../src/main/java/org/tron/core/Wallet.java | 5 + .../java/org/tron/core/config/args/Args.java | 4 + .../org/tron/core/config/args/ConfigKey.java | 1 + .../tron/core/consensus/ProposalService.java | 8 + .../tron/core/db/HistoryBlockHashUtil.java | 114 +++++++++++++ .../main/java/org/tron/core/db/Manager.java | 3 + .../core/actuator/utils/ProposalUtilTest.java | 52 ++++++ .../db/HistoryBlockHashIntegrationTest.java | 144 ++++++++++++++++ .../core/db/HistoryBlockHashUtilTest.java | 158 ++++++++++++++++++ 12 files changed, 526 insertions(+) create mode 100644 framework/src/main/java/org/tron/core/db/HistoryBlockHashUtil.java create mode 100644 framework/src/test/java/org/tron/core/db/HistoryBlockHashIntegrationTest.java create mode 100644 framework/src/test/java/org/tron/core/db/HistoryBlockHashUtilTest.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 cd42d7a9010..97755117a68 100644 --- a/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java +++ b/actuator/src/main/java/org/tron/core/utils/ProposalUtil.java @@ -886,6 +886,21 @@ public static void validator(DynamicPropertiesStore dynamicPropertiesStore, } break; } + case ALLOW_TVM_PRAGUE: { + if (!forkController.pass(ForkBlockVersionEnum.VERSION_4_8_2)) { + throw new ContractValidateException( + "Bad chain parameter id [ALLOW_TVM_PRAGUE]"); + } + if (dynamicPropertiesStore.getAllowTvmPrague() == 1) { + throw new ContractValidateException( + "[ALLOW_TVM_PRAGUE] has been valid, no need to propose again"); + } + if (value != 1) { + throw new ContractValidateException( + "This value[ALLOW_TVM_PRAGUE] is only allowed to be 1"); + } + break; + } default: break; } @@ -971,6 +986,7 @@ public enum ProposalType { // current value, value range ALLOW_TVM_BLOB(89), // 0, 1 PROPOSAL_EXPIRE_TIME(92), // (0, 31536003000) ALLOW_TVM_SELFDESTRUCT_RESTRICTION(94), // 0, 1 + ALLOW_TVM_PRAGUE(95), // 0, 1 ALLOW_TVM_OSAKA(96); // 0, 1 private long code; 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 e0adb0d444a..d087158e656 100644 --- a/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java +++ b/chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java @@ -240,6 +240,8 @@ public class DynamicPropertiesStore extends TronStoreWithRevoking private static final byte[] ALLOW_TVM_OSAKA = "ALLOW_TVM_OSAKA".getBytes(); + private static final byte[] ALLOW_TVM_PRAGUE = "ALLOW_TVM_PRAGUE".getBytes(); + @Autowired private DynamicPropertiesStore(@Value("properties") String dbName) { super(dbName); @@ -2993,6 +2995,21 @@ public void saveAllowTvmOsaka(long value) { this.put(ALLOW_TVM_OSAKA, new BytesCapsule(ByteArray.fromLong(value))); } + public long getAllowTvmPrague() { + return Optional.ofNullable(getUnchecked(ALLOW_TVM_PRAGUE)) + .map(BytesCapsule::getData) + .map(ByteArray::toLong) + .orElse(CommonParameter.getInstance().getAllowTvmPrague()); + } + + public void saveAllowTvmPrague(long value) { + this.put(ALLOW_TVM_PRAGUE, new BytesCapsule(ByteArray.fromLong(value))); + } + + public boolean allowTvmPrague() { + return getAllowTvmPrague() == 1L; + } + 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/common/parameter/CommonParameter.java b/common/src/main/java/org/tron/common/parameter/CommonParameter.java index a73158a718a..81447536a29 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -637,6 +637,10 @@ public class CommonParameter { @Setter public long allowTvmOsaka; + @Getter + @Setter + public long allowTvmPrague; + private static double calcMaxTimeRatio() { return 5.0; } diff --git a/framework/src/main/java/org/tron/core/Wallet.java b/framework/src/main/java/org/tron/core/Wallet.java index 39e8f06c281..7fc3116ddaf 100755 --- a/framework/src/main/java/org/tron/core/Wallet.java +++ b/framework/src/main/java/org/tron/core/Wallet.java @@ -1514,6 +1514,11 @@ public Protocol.ChainParameters getChainParameters() { .setValue(dbManager.getDynamicPropertiesStore().getAllowTvmOsaka()) .build()); + builder.addChainParameter(Protocol.ChainParameters.ChainParameter.newBuilder() + .setKey("getAllowTvmPrague") + .setValue(dbManager.getDynamicPropertiesStore().getAllowTvmPrague()) + .build()); + return builder.build(); } diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index 83d7fd2c63d..070b8cbd04e 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -1041,6 +1041,10 @@ public static void applyConfigParams( config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_OSAKA) ? config .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_OSAKA) : 0; + PARAMETER.allowTvmPrague = + config.hasPath(ConfigKey.COMMITTEE_ALLOW_TVM_PRAGUE) ? config + .getInt(ConfigKey.COMMITTEE_ALLOW_TVM_PRAGUE) : 0; + logConfig(); } diff --git a/framework/src/main/java/org/tron/core/config/args/ConfigKey.java b/framework/src/main/java/org/tron/core/config/args/ConfigKey.java index b21c9c440a4..631c83a7c6b 100644 --- a/framework/src/main/java/org/tron/core/config/args/ConfigKey.java +++ b/framework/src/main/java/org/tron/core/config/args/ConfigKey.java @@ -248,6 +248,7 @@ private ConfigKey() { public static final String COMMITTEE_ALLOW_TVM_BLOB = "committee.allowTvmBlob"; public static final String COMMITTEE_PROPOSAL_EXPIRE_TIME = "committee.proposalExpireTime"; public static final String COMMITTEE_ALLOW_TVM_OSAKA = "committee.allowTvmOsaka"; + public static final String COMMITTEE_ALLOW_TVM_PRAGUE = "committee.allowTvmPrague"; public static final String ALLOW_ACCOUNT_ASSET_OPTIMIZATION = "committee.allowAccountAssetOptimization"; public static final String ALLOW_ASSET_OPTIMIZATION = "committee.allowAssetOptimization"; 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 1bec0c2bda3..c0b313b67f8 100644 --- a/framework/src/main/java/org/tron/core/consensus/ProposalService.java +++ b/framework/src/main/java/org/tron/core/consensus/ProposalService.java @@ -4,6 +4,7 @@ import lombok.extern.slf4j.Slf4j; import org.tron.core.capsule.ProposalCapsule; import org.tron.core.config.Parameter.ForkBlockVersionEnum; +import org.tron.core.db.HistoryBlockHashUtil; import org.tron.core.db.Manager; import org.tron.core.store.DynamicPropertiesStore; import org.tron.core.utils.ProposalUtil; @@ -396,6 +397,13 @@ public static boolean process(Manager manager, ProposalCapsule proposalCapsule) manager.getDynamicPropertiesStore().saveAllowTvmOsaka(entry.getValue()); break; } + case ALLOW_TVM_PRAGUE: { + manager.getDynamicPropertiesStore().saveAllowTvmPrague(entry.getValue()); + if (entry.getValue() == 1) { + HistoryBlockHashUtil.deploy(manager); + } + 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 new file mode 100644 index 00000000000..b085f32d2b1 --- /dev/null +++ b/framework/src/main/java/org/tron/core/db/HistoryBlockHashUtil.java @@ -0,0 +1,114 @@ +package org.tron.core.db; + +import static java.lang.System.arraycopy; + +import com.google.protobuf.ByteString; +import org.bouncycastle.util.encoders.Hex; +import org.tron.common.crypto.Hash; +import org.tron.common.runtime.vm.DataWord; +import org.tron.core.capsule.AccountCapsule; +import org.tron.core.capsule.BlockCapsule; +import org.tron.core.capsule.CodeCapsule; +import org.tron.core.capsule.ContractCapsule; +import org.tron.core.capsule.StorageRowCapsule; +import org.tron.protos.Protocol; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract; + +/** + * TIP-2935 (EIP-2935): serve historical block hashes from state. + * + *

Approach A1 — at proposal activation, deploy the EIP-2935 bytecode and + * minimal contract/account metadata via direct store writes; on every block + * (before the tx loop) write the parent block hash to slot + * {@code (blockNum - 1) % HISTORY_SERVE_WINDOW} via direct StorageRowStore write. + * No VM execution is needed for {@code set()}; user contracts read via normal + * STATICCALL which executes the deployed bytecode. + * + *

Storage key layout replicates {@code Storage.compose()} for + * {@code contractVersion=0}: first 16 bytes of {@code sha3(address)} followed by + * the last 16 bytes of the 32-byte slot key. + */ +public class HistoryBlockHashUtil { + + public static final long HISTORY_SERVE_WINDOW = 8191L; + + // 21-byte TRON address (0x41 prefix + 20-byte EVM address 0x0000F908...2935) + public static final byte[] HISTORY_STORAGE_ADDRESS = + Hex.decode("410000f90827f1c53a10cb7a02335b175320002935"); + + // EIP-2935 runtime bytecode (83 bytes, no constructor prefix). + public static final byte[] HISTORY_STORAGE_CODE = Hex.decode( + "3373fffffffffffffffffffffffffffffffffffffffe" + + "14604657602036036042575f35600143038111604257" + + "611fff81430311604257611fff9006545f5260205ff3" + + "5b5f5ffd5b5f35611fff60014303065500"); + + public static final String HISTORY_STORAGE_NAME = "HistoryStorage"; + + private static final int PREFIX_BYTES = 16; + + private HistoryBlockHashUtil() { + } + + /** + * Compose the raw StorageRowStore key for {@code (address, slot)} at + * {@code contractVersion=0}. Must match {@code Storage.compose()} byte-for-byte + * so that a subsequent VM SLOAD(slot) at this address reads back the written value. + */ + public static byte[] composeStorageKey(long slot, byte[] address) { + byte[] addrHash = Hash.sha3(address); + byte[] slotKey = new DataWord(slot).getData(); + byte[] result = new byte[32]; + arraycopy(addrHash, 0, result, 0, PREFIX_BYTES); + arraycopy(slotKey, PREFIX_BYTES, result, PREFIX_BYTES, PREFIX_BYTES); + return result; + } + + /** + * Deploy the EIP-2935 HistoryStorage contract at HISTORY_STORAGE_ADDRESS. + * Writes CodeStore, ContractStore and AccountStore. Idempotent — safe to call + * multiple times; only the missing entries are written. + * Called once from ProposalService when ALLOW_TVM_PRAGUE activates. + */ + public static void deploy(Manager manager) { + byte[] addr = HISTORY_STORAGE_ADDRESS; + + if (!manager.getChainBaseManager().getCodeStore().has(addr)) { + manager.getChainBaseManager().getCodeStore() + .put(addr, new CodeCapsule(HISTORY_STORAGE_CODE)); + } + + if (!manager.getChainBaseManager().getContractStore().has(addr)) { + SmartContract sc = SmartContract.newBuilder() + .setName(HISTORY_STORAGE_NAME) + .setContractAddress(ByteString.copyFrom(addr)) + .setOriginAddress(ByteString.copyFrom(addr)) + .setConsumeUserResourcePercent(100L) + .setOriginEnergyLimit(0L) + .build(); + manager.getChainBaseManager().getContractStore() + .put(addr, new ContractCapsule(sc)); + } + + if (!manager.getChainBaseManager().getAccountStore().has(addr)) { + AccountCapsule account = new AccountCapsule( + ByteString.copyFrom(addr), + Protocol.AccountType.Contract); + manager.getChainBaseManager().getAccountStore().put(addr, account); + } + } + + /** + * Write the parent block hash to storage at slot + * {@code (blockNum - 1) % HISTORY_SERVE_WINDOW}. Called from + * {@code Manager.processBlock} before the tx loop so transactions can SLOAD + * it via STATICCALL to the deployed bytecode. + */ + public static void write(Manager manager, BlockCapsule block) { + long slot = (block.getNum() - 1) % HISTORY_SERVE_WINDOW; + byte[] parentHash = block.getParentHash().getBytes(); + byte[] storageKey = composeStorageKey(slot, HISTORY_STORAGE_ADDRESS); + StorageRowCapsule row = new StorageRowCapsule(storageKey, parentHash); + manager.getChainBaseManager().getStorageRowStore().put(storageKey, row); + } +} diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index cd1a61c01fe..0a82f102fe7 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -1850,6 +1850,9 @@ private void processBlock(BlockCapsule block, List txs) TransactionRetCapsule transactionRetCapsule = new TransactionRetCapsule(block); + if (chainBaseManager.getDynamicPropertiesStore().allowTvmPrague()) { + HistoryBlockHashUtil.write(this, block); + } try { merkleContainer.resetCurrentMerkleTree(); accountStateCallBack.preExecute(block); 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 f8d8e6bdd9d..f43b3b03609 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 @@ -448,6 +448,8 @@ public void validateCheck() { testAllowTvmSelfdestructRestrictionProposal(); + testAllowTvmPragueProposal(); + forkUtils.getManager().getDynamicPropertiesStore() .statsByVersion(ForkBlockVersionEnum.ENERGY_LIMIT.getValue(), stats); forkUtils.reset(); @@ -719,6 +721,56 @@ private void testAllowTvmSelfdestructRestrictionProposal() { } } + private void testAllowTvmPragueProposal() { + byte[] stats = new byte[27]; + forkUtils.getManager().getDynamicPropertiesStore() + .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_2.getValue(), stats); + try { + ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_TVM_PRAGUE.getCode(), 1); + Assert.fail(); + } catch (ContractValidateException e) { + Assert.assertEquals( + "Bad chain parameter id [ALLOW_TVM_PRAGUE]", + e.getMessage()); + } + + long maintenanceTimeInterval = forkUtils.getManager().getDynamicPropertiesStore() + .getMaintenanceTimeInterval(); + + long hardForkTime = + ((ForkBlockVersionEnum.VERSION_4_8_2.getHardForkTime() - 1) / maintenanceTimeInterval + 1) + * maintenanceTimeInterval; + forkUtils.getManager().getDynamicPropertiesStore() + .saveLatestBlockHeaderTimestamp(hardForkTime + 1); + + stats = new byte[27]; + Arrays.fill(stats, (byte) 1); + forkUtils.getManager().getDynamicPropertiesStore() + .statsByVersion(ForkBlockVersionEnum.VERSION_4_8_2.getValue(), stats); + + try { + ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_TVM_PRAGUE.getCode(), 2); + Assert.fail(); + } catch (ContractValidateException e) { + Assert.assertEquals( + "This value[ALLOW_TVM_PRAGUE] is only allowed to be 1", + e.getMessage()); + } + + dynamicPropertiesStore.saveAllowTvmPrague(1); + try { + ProposalUtil.validator(dynamicPropertiesStore, forkUtils, + ProposalType.ALLOW_TVM_PRAGUE.getCode(), 1); + Assert.fail(); + } catch (ContractValidateException e) { + Assert.assertEquals( + "[ALLOW_TVM_PRAGUE] has been valid, no need to propose again", + e.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 new file mode 100644 index 00000000000..cfef4648a18 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db/HistoryBlockHashIntegrationTest.java @@ -0,0 +1,144 @@ +package org.tron.core.db; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.google.protobuf.ByteString; +import java.util.Arrays; +import org.junit.Before; +import org.junit.Test; +import org.tron.common.BaseTest; +import org.tron.common.TestConstants; +import org.tron.common.runtime.vm.DataWord; +import org.tron.common.utils.Sha256Hash; +import org.tron.core.capsule.BlockCapsule; +import org.tron.core.capsule.CodeCapsule; +import org.tron.core.capsule.StorageRowCapsule; +import org.tron.core.config.args.Args; +import org.tron.core.store.DynamicPropertiesStore; +import org.tron.core.store.StoreFactory; +import org.tron.core.vm.repository.RepositoryImpl; + +/** + * TIP-2935 end-to-end: activation deploys the contract, subsequent blocks + * populate the ring buffer via the pre-tx hook, and the VM repository reads + * back written hashes through the same {@code Storage.compose()} layer that + * production {@code SLOAD} uses. + */ +public class HistoryBlockHashIntegrationTest extends BaseTest { + + static { + Args.setParam(new String[]{"--output-directory", dbPath()}, TestConstants.TEST_CONF); + } + + @Before + public void resetState() { + byte[] addr = HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS; + chainBaseManager.getDynamicPropertiesStore().saveAllowTvmPrague(0L); + chainBaseManager.getCodeStore().delete(addr); + chainBaseManager.getContractStore().delete(addr); + chainBaseManager.getAccountStore().delete(addr); + for (long slot : new long[]{0L, 99L, 499L, 776L}) { + chainBaseManager.getStorageRowStore() + .delete(HistoryBlockHashUtil.composeStorageKey(slot, addr)); + } + } + + @Test + public void activationDeploysContractAndFlagIsSet() { + DynamicPropertiesStore dps = chainBaseManager.getDynamicPropertiesStore(); + byte[] addr = HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS; + + assertEquals(0L, dps.getAllowTvmPrague()); + assertFalse(chainBaseManager.getCodeStore().has(addr)); + + dps.saveAllowTvmPrague(1L); + HistoryBlockHashUtil.deploy(dbManager); + + assertEquals(1L, dps.getAllowTvmPrague()); + assertTrue(chainBaseManager.getCodeStore().has(addr)); + CodeCapsule code = chainBaseManager.getCodeStore().get(addr); + assertNotNull(code); + assertArrayEquals(HistoryBlockHashUtil.HISTORY_STORAGE_CODE, code.getData()); + } + + @Test + public void writeAfterActivationFillsStorageSlot() { + chainBaseManager.getDynamicPropertiesStore().saveAllowTvmPrague(1L); + HistoryBlockHashUtil.deploy(dbManager); + + long blockNum = 500L; + byte[] parentHash = new byte[32]; + Arrays.fill(parentHash, (byte) 0x5a); + BlockCapsule block = new BlockCapsule( + blockNum, + Sha256Hash.wrap(parentHash), + System.currentTimeMillis(), + ByteString.copyFrom(new byte[21])); + + HistoryBlockHashUtil.write(dbManager, block); + + byte[] storageKey = HistoryBlockHashUtil.composeStorageKey( + 499L, HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS); + StorageRowCapsule row = chainBaseManager.getStorageRowStore().get(storageKey); + assertNotNull(row); + assertArrayEquals(parentHash, row.getValue()); + } + + @Test + public void vmRepositoryReadsBackWrittenHash() { + // Full round-trip: direct-write -> VM Repository -> getStorageValue. + // Proves Storage.compose() on the read side agrees with + // HistoryBlockHashUtil.composeStorageKey() on the write side. + chainBaseManager.getDynamicPropertiesStore().saveAllowTvmPrague(1L); + HistoryBlockHashUtil.deploy(dbManager); + + long blockNum = 777L; + byte[] parentHash = new byte[32]; + Arrays.fill(parentHash, (byte) 0x77); + BlockCapsule block = new BlockCapsule( + blockNum, + Sha256Hash.wrap(parentHash), + System.currentTimeMillis(), + ByteString.copyFrom(new byte[21])); + HistoryBlockHashUtil.write(dbManager, block); + + RepositoryImpl repo = RepositoryImpl.createRoot(StoreFactory.getInstance()); + + // (777 - 1) % 8191 = 776 + DataWord slotKey = new DataWord(776L); + DataWord readBack = repo.getStorageValue( + HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS, slotKey); + + assertNotNull("VM repository failed to read stored hash", readBack); + assertArrayEquals("VM read-back != direct-written hash", + parentHash, readBack.getData()); + } + + @Test + public void noWriteBeforeActivation() { + assertEquals(0L, + chainBaseManager.getDynamicPropertiesStore().getAllowTvmPrague()); + + long blockNum = 100L; + byte[] parentHash = new byte[32]; + Arrays.fill(parentHash, (byte) 0xff); + BlockCapsule block = new BlockCapsule( + blockNum, + Sha256Hash.wrap(parentHash), + System.currentTimeMillis(), + ByteString.copyFrom(new byte[21])); + + // Mimic Manager's gated hook + if (chainBaseManager.getDynamicPropertiesStore().allowTvmPrague()) { + HistoryBlockHashUtil.write(dbManager, block); + } + + byte[] storageKey = HistoryBlockHashUtil.composeStorageKey( + 99L, HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS); + assertFalse(chainBaseManager.getStorageRowStore().has(storageKey)); + } +} diff --git a/framework/src/test/java/org/tron/core/db/HistoryBlockHashUtilTest.java b/framework/src/test/java/org/tron/core/db/HistoryBlockHashUtilTest.java new file mode 100644 index 00000000000..71e2182d3ab --- /dev/null +++ b/framework/src/test/java/org/tron/core/db/HistoryBlockHashUtilTest.java @@ -0,0 +1,158 @@ +package org.tron.core.db; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.google.protobuf.ByteString; +import java.util.Arrays; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.tron.common.BaseTest; +import org.tron.common.TestConstants; +import org.tron.common.runtime.vm.DataWord; +import org.tron.common.utils.Sha256Hash; +import org.tron.core.capsule.BlockCapsule; +import org.tron.core.capsule.CodeCapsule; +import org.tron.core.capsule.ContractCapsule; +import org.tron.core.capsule.StorageRowCapsule; +import org.tron.core.config.args.Args; +import org.tron.core.store.StorageRowStore; +import org.tron.core.vm.program.Storage; + +public class HistoryBlockHashUtilTest extends BaseTest { + + static { + Args.setParam(new String[]{"--output-directory", dbPath()}, TestConstants.TEST_CONF); + } + + @Before + public void resetState() { + byte[] addr = HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS; + chainBaseManager.getCodeStore().delete(addr); + chainBaseManager.getContractStore().delete(addr); + chainBaseManager.getAccountStore().delete(addr); + for (long slot : new long[]{0L, 99L, 499L, 776L}) { + chainBaseManager.getStorageRowStore() + .delete(HistoryBlockHashUtil.composeStorageKey(slot, addr)); + } + } + + /** + * Lock the storage key layout: {@code composeStorageKey} must produce the + * same raw key that {@code Storage.compose()} produces at contractVersion=0, + * so direct-written rows are readable via VM {@code SLOAD}. + */ + @Test + public void composeStorageKeyMatchesStorageCompose() { + byte[] addr = HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS; + long slot = 1234L; + + Storage storage = new Storage(addr, Mockito.mock(StorageRowStore.class)); + storage.setContractVersion(0); + + DataWord slotKey = new DataWord(slot); + DataWord value = new DataWord( + Hex.decode("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")); + storage.put(slotKey, value); + + StorageRowCapsule row = storage.getRowCache().get(slotKey); + byte[] vmKey = row.getRowKey(); + byte[] ourKey = HistoryBlockHashUtil.composeStorageKey(slot, addr); + + assertArrayEquals("direct-write key must equal VM SSTORE key (contractVersion=0)", + vmKey, ourKey); + } + + @Test + public void deployCreatesCodeContractAndAccount() { + HistoryBlockHashUtil.deploy(dbManager); + + byte[] addr = HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS; + + assertTrue(chainBaseManager.getCodeStore().has(addr)); + CodeCapsule code = chainBaseManager.getCodeStore().get(addr); + assertNotNull(code); + assertArrayEquals(HistoryBlockHashUtil.HISTORY_STORAGE_CODE, code.getData()); + + ContractCapsule contract = chainBaseManager.getContractStore().get(addr); + assertNotNull(contract); + assertEquals(HistoryBlockHashUtil.HISTORY_STORAGE_NAME, + contract.getInstance().getName()); + assertArrayEquals(addr, contract.getInstance().getContractAddress().toByteArray()); + assertEquals("version must be 0", 0, contract.getInstance().getVersion()); + assertEquals(100L, contract.getInstance().getConsumeUserResourcePercent()); + + assertTrue(chainBaseManager.getAccountStore().has(addr)); + } + + @Test + public void deployIsIdempotent() { + HistoryBlockHashUtil.deploy(dbManager); + HistoryBlockHashUtil.deploy(dbManager); + CodeCapsule code = + chainBaseManager.getCodeStore().get(HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS); + assertArrayEquals(HistoryBlockHashUtil.HISTORY_STORAGE_CODE, code.getData()); + } + + @Test + public void writeStoresParentHashAtCorrectSlot() { + HistoryBlockHashUtil.deploy(dbManager); + + long blockNum = 100L; + byte[] parentHash = new byte[32]; + Arrays.fill(parentHash, (byte) 0xab); + + BlockCapsule block = new BlockCapsule( + blockNum, + Sha256Hash.wrap(parentHash), + System.currentTimeMillis(), + ByteString.copyFrom(new byte[21])); + + HistoryBlockHashUtil.write(dbManager, block); + + byte[] key = HistoryBlockHashUtil.composeStorageKey( + 99L, HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS); + StorageRowCapsule row = chainBaseManager.getStorageRowStore().get(key); + assertNotNull(row); + assertArrayEquals(parentHash, row.getValue()); + } + + @Test + public void writeUsesRingBufferModulo() { + HistoryBlockHashUtil.deploy(dbManager); + + // (8192 - 1) % 8191 = 0 + long blockNum = 8192L; + byte[] parentHash = new byte[32]; + Arrays.fill(parentHash, (byte) 0xcd); + + BlockCapsule block = new BlockCapsule( + blockNum, + Sha256Hash.wrap(parentHash), + System.currentTimeMillis(), + ByteString.copyFrom(new byte[21])); + + HistoryBlockHashUtil.write(dbManager, block); + + byte[] key = HistoryBlockHashUtil.composeStorageKey( + 0L, HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS); + StorageRowCapsule row = chainBaseManager.getStorageRowStore().get(key); + assertNotNull(row); + assertArrayEquals(parentHash, row.getValue()); + } + + @Test + public void beforeDeployNothingIsWritten() { + assertFalse(chainBaseManager.getCodeStore() + .has(HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS)); + assertFalse(chainBaseManager.getContractStore() + .has(HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS)); + assertFalse(chainBaseManager.getAccountStore() + .has(HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS)); + } +} From 87359865643eb4c1247eff0841a62968674cfebd Mon Sep 17 00:00:00 2001 From: Asuka Date: Thu, 16 Apr 2026 17:02:36 +0800 Subject: [PATCH 2/5] fix(vm): deploy TIP-2935 history contract on config-driven activation Proposal activation deploys the HistoryStorage contract via ProposalService, but a node started with committee.allowTvmPrague=1 would flip the flag on through the CommonParameter fallback without the contract ever being written to CodeStore/ContractStore/AccountStore. External STATICCALLs would then hit an empty account. - Add HistoryBlockHashUtil.deployIfMissing(Manager), called from Manager.init right after updateDynamicStoreByConfig so the bytecode is guaranteed present before any block is applied. - Guard write() against blockNum <= 0 so (0 - 1) % 8191 = -1 in Java can never corrupt a slot if the hook is ever reached for genesis. - Add integration tests covering the proposal-persisted, config-fallback, and already-deployed paths, plus block-1 writes genesis hash to slot 0 and the genesis no-op. --- .../tron/core/db/HistoryBlockHashUtil.java | 17 +++ .../main/java/org/tron/core/db/Manager.java | 2 + .../db/HistoryBlockHashIntegrationTest.java | 135 ++++++++++++++++++ 3 files changed, 154 insertions(+) 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 b085f32d2b1..2108d5a8658 100644 --- a/framework/src/main/java/org/tron/core/db/HistoryBlockHashUtil.java +++ b/framework/src/main/java/org/tron/core/db/HistoryBlockHashUtil.java @@ -64,6 +64,18 @@ public static byte[] composeStorageKey(long slot, byte[] address) { return result; } + /** + * Deploy when the flag is on but bytecode is missing. Covers the config-boot + * path (committee.allowTvmPrague=1) where no proposal fires to trigger deploy. + * Run once from {@code Manager.init()} after config is loaded into the store. + */ + public static void deployIfMissing(Manager manager) { + if (manager.getChainBaseManager().getDynamicPropertiesStore().allowTvmPrague() + && !manager.getChainBaseManager().getCodeStore().has(HISTORY_STORAGE_ADDRESS)) { + deploy(manager); + } + } + /** * Deploy the EIP-2935 HistoryStorage contract at HISTORY_STORAGE_ADDRESS. * Writes CodeStore, ContractStore and AccountStore. Idempotent — safe to call @@ -105,6 +117,11 @@ public static void deploy(Manager manager) { * it via STATICCALL to the deployed bytecode. */ public static void write(Manager manager, BlockCapsule block) { + // Genesis has no parent; applyBlock never invokes this for block 0, but be + // explicit so (0-1) % 8191 = -1 in Java can never corrupt a slot. + if (block.getNum() <= 0) { + return; + } long slot = (block.getNum() - 1) % HISTORY_SERVE_WINDOW; byte[] parentHash = block.getParentHash().getBytes(); byte[] storageKey = composeStorageKey(slot, HISTORY_STORAGE_ADDRESS); diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index 0a82f102fe7..95a8dade18b 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -545,6 +545,8 @@ public void init() { //for test only chainBaseManager.getDynamicPropertiesStore().updateDynamicStoreByConfig(); + HistoryBlockHashUtil.deployIfMissing(this); + // init liteFullNode initLiteNode(); 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 cfef4648a18..c8b634ce2b2 100644 --- a/framework/src/test/java/org/tron/core/db/HistoryBlockHashIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db/HistoryBlockHashIntegrationTest.java @@ -7,11 +7,14 @@ import static org.junit.Assert.assertTrue; import com.google.protobuf.ByteString; +import java.lang.reflect.Field; import java.util.Arrays; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.tron.common.BaseTest; import org.tron.common.TestConstants; +import org.tron.common.parameter.CommonParameter; import org.tron.common.runtime.vm.DataWord; import org.tron.common.utils.Sha256Hash; import org.tron.core.capsule.BlockCapsule; @@ -38,6 +41,7 @@ public class HistoryBlockHashIntegrationTest extends BaseTest { public void resetState() { byte[] addr = HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS; chainBaseManager.getDynamicPropertiesStore().saveAllowTvmPrague(0L); + CommonParameter.getInstance().setAllowTvmPrague(0L); chainBaseManager.getCodeStore().delete(addr); chainBaseManager.getContractStore().delete(addr); chainBaseManager.getAccountStore().delete(addr); @@ -47,6 +51,11 @@ public void resetState() { } } + @After + public void resetCommonParameter() { + CommonParameter.getInstance().setAllowTvmPrague(0L); + } + @Test public void activationDeploysContractAndFlagIsSet() { DynamicPropertiesStore dps = chainBaseManager.getDynamicPropertiesStore(); @@ -141,4 +150,130 @@ public void noWriteBeforeActivation() { 99L, HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS); assertFalse(chainBaseManager.getStorageRowStore().has(storageKey)); } + + /** + * deployIfMissing deploys whenever the flag reads as 1 and bytecode is absent — + * covers the proposal-already-persisted restart case. + */ + @Test + public void deployIfMissingWithFlagOnDeploysContract() { + byte[] addr = HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS; + chainBaseManager.getDynamicPropertiesStore().saveAllowTvmPrague(1L); + + assertFalse(chainBaseManager.getCodeStore().has(addr)); + + HistoryBlockHashUtil.deployIfMissing(dbManager); + + assertTrue(chainBaseManager.getCodeStore().has(addr)); + assertArrayEquals(HistoryBlockHashUtil.HISTORY_STORAGE_CODE, + chainBaseManager.getCodeStore().get(addr).getData()); + assertTrue(chainBaseManager.getContractStore().has(addr)); + assertTrue(chainBaseManager.getAccountStore().has(addr)); + } + + /** + * Config-boot path: {@code committee.allowTvmPrague=1} populates + * {@link CommonParameter}, no proposal ever fires, and on first start the + * {@code DynamicPropertiesStore} has no ALLOW_TVM_PRAGUE entry so + * {@code getAllowTvmPrague()} falls back to CommonParameter. Proves the whole + * chain reaches deployIfMissing. + */ + @Test + public void deployIfMissingFromConfigFallbackDeploysContract() throws Exception { + byte[] addr = HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS; + clearAllowTvmPragueFromStore(); + CommonParameter.getInstance().setAllowTvmPrague(1L); + + assertFalse(chainBaseManager.getCodeStore().has(addr)); + assertTrue(chainBaseManager.getDynamicPropertiesStore().allowTvmPrague()); + + HistoryBlockHashUtil.deployIfMissing(dbManager); + + assertTrue(chainBaseManager.getCodeStore().has(addr)); + assertTrue(chainBaseManager.getContractStore().has(addr)); + assertTrue(chainBaseManager.getAccountStore().has(addr)); + } + + private void clearAllowTvmPragueFromStore() throws Exception { + Field keyField = DynamicPropertiesStore.class.getDeclaredField("ALLOW_TVM_PRAGUE"); + keyField.setAccessible(true); + byte[] key = (byte[]) keyField.get(null); + chainBaseManager.getDynamicPropertiesStore().delete(key); + } + + @Test + public void deployIfMissingWithFlagOffSkipsDeploy() { + byte[] addr = HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS; + // Flag defaults to 0 via resetState(); no proposal, no config. + + HistoryBlockHashUtil.deployIfMissing(dbManager); + + assertFalse(chainBaseManager.getCodeStore().has(addr)); + assertFalse(chainBaseManager.getContractStore().has(addr)); + assertFalse(chainBaseManager.getAccountStore().has(addr)); + } + + @Test + public void deployIfMissingIsIdempotentAfterProposal() { + byte[] addr = HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS; + // Simulate proposal already activated on a previous run: flag persisted and + // contract deployed. Restart with or without config; deployIfMissing is a no-op. + chainBaseManager.getDynamicPropertiesStore().saveAllowTvmPrague(1L); + HistoryBlockHashUtil.deploy(dbManager); + + CodeCapsule before = chainBaseManager.getCodeStore().get(addr); + HistoryBlockHashUtil.deployIfMissing(dbManager); + CodeCapsule after = chainBaseManager.getCodeStore().get(addr); + + assertArrayEquals(before.getData(), after.getData()); + } + + /** + * Block 1 is the first block to go through {@code applyBlock -> processBlock}. + * Its parent is the genesis block, so slot 0 must hold the genesis block hash. + */ + @Test + public void writeForBlock1StoresGenesisHashAtSlot0() { + chainBaseManager.getDynamicPropertiesStore().saveAllowTvmPrague(1L); + HistoryBlockHashUtil.deploy(dbManager); + + byte[] genesisHash = new byte[32]; + Arrays.fill(genesisHash, (byte) 0x01); + BlockCapsule block1 = new BlockCapsule( + 1L, + Sha256Hash.wrap(genesisHash), + System.currentTimeMillis(), + ByteString.copyFrom(new byte[21])); + + HistoryBlockHashUtil.write(dbManager, block1); + + byte[] key = HistoryBlockHashUtil.composeStorageKey( + 0L, HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS); + StorageRowCapsule row = chainBaseManager.getStorageRowStore().get(key); + assertNotNull(row); + assertArrayEquals(genesisHash, row.getValue()); + } + + /** + * Genesis never goes through {@code applyBlock}, but the guard keeps + * {@code (0 - 1) % 8191 = -1} from ever corrupting a slot if it ever did. + */ + @Test + public void writeIsNoOpForGenesisBlock() { + chainBaseManager.getDynamicPropertiesStore().saveAllowTvmPrague(1L); + HistoryBlockHashUtil.deploy(dbManager); + + byte[] zeroHash = new byte[32]; + BlockCapsule genesis = new BlockCapsule( + 0L, + Sha256Hash.wrap(zeroHash), + 0L, + ByteString.copyFrom(new byte[21])); + + HistoryBlockHashUtil.write(dbManager, genesis); + + byte[] slot0Key = HistoryBlockHashUtil.composeStorageKey( + 0L, HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS); + assertFalse(chainBaseManager.getStorageRowStore().has(slot0Key)); + } } From 967dd86f6b1dc8f6e33ff452fc31810a3e74e1df Mon Sep 17 00:00:00 2001 From: Asuka Date: Thu, 16 Apr 2026 17:16:53 +0800 Subject: [PATCH 3/5] refactor(vm): use Manager store accessors directly in HistoryBlockHashUtil MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapse the redundant getChainBaseManager() indirection — Manager already exposes getCodeStore/getContractStore/getAccountStore/getStorageRowStore/ getDynamicPropertiesStore pass-throughs. --- .../tron/core/db/HistoryBlockHashUtil.java | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) 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 2108d5a8658..099c9796f2e 100644 --- a/framework/src/main/java/org/tron/core/db/HistoryBlockHashUtil.java +++ b/framework/src/main/java/org/tron/core/db/HistoryBlockHashUtil.java @@ -70,8 +70,8 @@ public static byte[] composeStorageKey(long slot, byte[] address) { * Run once from {@code Manager.init()} after config is loaded into the store. */ public static void deployIfMissing(Manager manager) { - if (manager.getChainBaseManager().getDynamicPropertiesStore().allowTvmPrague() - && !manager.getChainBaseManager().getCodeStore().has(HISTORY_STORAGE_ADDRESS)) { + if (manager.getDynamicPropertiesStore().allowTvmPrague() + && !manager.getCodeStore().has(HISTORY_STORAGE_ADDRESS)) { deploy(manager); } } @@ -85,12 +85,11 @@ public static void deployIfMissing(Manager manager) { public static void deploy(Manager manager) { byte[] addr = HISTORY_STORAGE_ADDRESS; - if (!manager.getChainBaseManager().getCodeStore().has(addr)) { - manager.getChainBaseManager().getCodeStore() - .put(addr, new CodeCapsule(HISTORY_STORAGE_CODE)); + if (!manager.getCodeStore().has(addr)) { + manager.getCodeStore().put(addr, new CodeCapsule(HISTORY_STORAGE_CODE)); } - if (!manager.getChainBaseManager().getContractStore().has(addr)) { + if (!manager.getContractStore().has(addr)) { SmartContract sc = SmartContract.newBuilder() .setName(HISTORY_STORAGE_NAME) .setContractAddress(ByteString.copyFrom(addr)) @@ -98,15 +97,12 @@ public static void deploy(Manager manager) { .setConsumeUserResourcePercent(100L) .setOriginEnergyLimit(0L) .build(); - manager.getChainBaseManager().getContractStore() - .put(addr, new ContractCapsule(sc)); + manager.getContractStore().put(addr, new ContractCapsule(sc)); } - if (!manager.getChainBaseManager().getAccountStore().has(addr)) { - AccountCapsule account = new AccountCapsule( - ByteString.copyFrom(addr), - Protocol.AccountType.Contract); - manager.getChainBaseManager().getAccountStore().put(addr, account); + if (!manager.getAccountStore().has(addr)) { + manager.getAccountStore().put(addr, + new AccountCapsule(ByteString.copyFrom(addr), Protocol.AccountType.Contract)); } } @@ -123,9 +119,8 @@ public static void write(Manager manager, BlockCapsule block) { return; } long slot = (block.getNum() - 1) % HISTORY_SERVE_WINDOW; - byte[] parentHash = block.getParentHash().getBytes(); byte[] storageKey = composeStorageKey(slot, HISTORY_STORAGE_ADDRESS); - StorageRowCapsule row = new StorageRowCapsule(storageKey, parentHash); - manager.getChainBaseManager().getStorageRowStore().put(storageKey, row); + byte[] parentHash = block.getParentHash().getBytes(); + manager.getStorageRowStore().put(storageKey, new StorageRowCapsule(storageKey, parentHash)); } } From c4dde347deb6274181481bdac9c8803253597025 Mon Sep 17 00:00:00 2001 From: Asuka Date: Fri, 17 Apr 2026 16:49:25 +0800 Subject: [PATCH 4/5] fix(vm): validate existing state before TIP-2935 deploy and self-heal half-installs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous deploy() skipped any store whose entry already existed at the canonical address, which had two failure modes: - Silent merge with foreign state. If any capsule already sat at 0x…2935 (accidental transfer, pre-existing code), deploy would skip that store and proceed, leaving the canonical address with a mix of prior and new data. Nothing alerted operators, and the contract could be functionally broken (wrong code, wrong account type). - Unrecoverable half-install. init() runs with revokingStore disabled, so the three writes are not atomic. A crash between writes left the DB with a subset of entries; on next start deployIfMissing only checked CodeStore.has and would skip, leaving the address permanently broken (RepositoryImpl returns null storage whenever the account is missing). The rewritten deploy() validates any pre-existing code / contract / account against the expected HistoryStorage (bytecode equality, contract name + address, account type=Contract) and throws IllegalStateException on any mismatch — activation refuses rather than silently corrupting state. Whatever is missing after validation is filled in, so a half-install self-heals on the next start. deployIfMissing() now gates on the flag alone and delegates to deploy() so the recovery path is exercised regardless of which of the three stores was written before the crash. A logger.info fires inside the code-write branch specifically, so operators can grep for when fresh bytecode landed on a given node without noise on steady-state restarts. New tests: - deployRejectsPreExistingWrongCode - deployRejectsPreExistingForeignContract - deployRejectsPreExistingNormalAccount - deployIfMissingCompletesPartialInstall --- .../tron/core/db/HistoryBlockHashUtil.java | 58 +++++++++--- .../db/HistoryBlockHashIntegrationTest.java | 89 +++++++++++++++++++ 2 files changed, 136 insertions(+), 11 deletions(-) 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 099c9796f2e..902af1f39e6 100644 --- a/framework/src/main/java/org/tron/core/db/HistoryBlockHashUtil.java +++ b/framework/src/main/java/org/tron/core/db/HistoryBlockHashUtil.java @@ -3,6 +3,8 @@ import static java.lang.System.arraycopy; import com.google.protobuf.ByteString; +import java.util.Arrays; +import lombok.extern.slf4j.Slf4j; import org.bouncycastle.util.encoders.Hex; import org.tron.common.crypto.Hash; import org.tron.common.runtime.vm.DataWord; @@ -28,6 +30,7 @@ * {@code contractVersion=0}: first 16 bytes of {@code sha3(address)} followed by * the last 16 bytes of the 32-byte slot key. */ +@Slf4j(topic = "DB") public class HistoryBlockHashUtil { public static final long HISTORY_SERVE_WINDOW = 8191L; @@ -65,30 +68,38 @@ public static byte[] composeStorageKey(long slot, byte[] address) { } /** - * Deploy when the flag is on but bytecode is missing. Covers the config-boot - * path (committee.allowTvmPrague=1) where no proposal fires to trigger deploy. - * Run once from {@code Manager.init()} after config is loaded into the store. + * Ensure the HistoryStorage contract is fully deployed when the flag is on. + * Covers two recovery cases not handled by {@link ProposalService}: + *

+ * Run once from {@code Manager.init()} after config is loaded. */ public static void deployIfMissing(Manager manager) { - if (manager.getDynamicPropertiesStore().allowTvmPrague() - && !manager.getCodeStore().has(HISTORY_STORAGE_ADDRESS)) { + if (manager.getDynamicPropertiesStore().allowTvmPrague()) { deploy(manager); } } /** - * Deploy the EIP-2935 HistoryStorage contract at HISTORY_STORAGE_ADDRESS. - * Writes CodeStore, ContractStore and AccountStore. Idempotent — safe to call - * multiple times; only the missing entries are written. - * Called once from ProposalService when ALLOW_TVM_PRAGUE activates. + * Deploy the EIP-2935 HistoryStorage contract at {@code HISTORY_STORAGE_ADDRESS}. + * Validates first, writes second: any pre-existing code/contract/account at the + * canonical address must match the expected HistoryStorage; otherwise the call + * throws and activation halts rather than silently merging with foreign state. + * Whatever is missing after validation is filled in, so a half-installed state + * from a crashed prior run self-heals on the next start. */ public static void deploy(Manager manager) { byte[] addr = HISTORY_STORAGE_ADDRESS; + validateExistingOrThrow(manager, addr); if (!manager.getCodeStore().has(addr)) { manager.getCodeStore().put(addr, new CodeCapsule(HISTORY_STORAGE_CODE)); + logger.info("TIP-2935: wrote HistoryStorage bytecode at {}", Hex.toHexString(addr)); } - if (!manager.getContractStore().has(addr)) { SmartContract sc = SmartContract.newBuilder() .setName(HISTORY_STORAGE_NAME) @@ -99,13 +110,38 @@ public static void deploy(Manager manager) { .build(); manager.getContractStore().put(addr, new ContractCapsule(sc)); } - if (!manager.getAccountStore().has(addr)) { manager.getAccountStore().put(addr, new AccountCapsule(ByteString.copyFrom(addr), Protocol.AccountType.Contract)); } } + private static void validateExistingOrThrow(Manager manager, byte[] addr) { + if (manager.getCodeStore().has(addr)) { + byte[] existing = manager.getCodeStore().get(addr).getData(); + if (!Arrays.equals(HISTORY_STORAGE_CODE, existing)) { + throw new IllegalStateException( + "TIP-2935: code at " + Hex.toHexString(addr) + " differs from expected bytecode"); + } + } + if (manager.getContractStore().has(addr)) { + SmartContract existing = manager.getContractStore().get(addr).getInstance(); + if (!HISTORY_STORAGE_NAME.equals(existing.getName()) + || !Arrays.equals(addr, existing.getContractAddress().toByteArray())) { + throw new IllegalStateException( + "TIP-2935: contract at " + Hex.toHexString(addr) + + " is not the expected HistoryStorage"); + } + } + if (manager.getAccountStore().has(addr)) { + AccountCapsule existing = manager.getAccountStore().get(addr); + if (existing.getType() != Protocol.AccountType.Contract) { + throw new IllegalStateException( + "TIP-2935: account at " + Hex.toHexString(addr) + " exists but is not a contract"); + } + } + } + /** * Write the parent block hash to storage at slot * {@code (blockNum - 1) % HISTORY_SERVE_WINDOW}. Called from 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 c8b634ce2b2..c6455d3e055 100644 --- a/framework/src/test/java/org/tron/core/db/HistoryBlockHashIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db/HistoryBlockHashIntegrationTest.java @@ -5,6 +5,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import com.google.protobuf.ByteString; import java.lang.reflect.Field; @@ -17,13 +18,17 @@ import org.tron.common.parameter.CommonParameter; import org.tron.common.runtime.vm.DataWord; import org.tron.common.utils.Sha256Hash; +import org.tron.core.capsule.AccountCapsule; import org.tron.core.capsule.BlockCapsule; import org.tron.core.capsule.CodeCapsule; +import org.tron.core.capsule.ContractCapsule; import org.tron.core.capsule.StorageRowCapsule; import org.tron.core.config.args.Args; import org.tron.core.store.DynamicPropertiesStore; import org.tron.core.store.StoreFactory; import org.tron.core.vm.repository.RepositoryImpl; +import org.tron.protos.Protocol; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract; /** * TIP-2935 end-to-end: activation deploys the contract, subsequent blocks @@ -276,4 +281,88 @@ public void writeIsNoOpForGenesisBlock() { 0L, HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS); assertFalse(chainBaseManager.getStorageRowStore().has(slot0Key)); } + + /** + * Collision guard: if foreign bytecode already sits at the canonical address + * (theoretically impossible short of a hash pre-image, but we fail-fast rather + * than silently merging), activation must refuse instead of skipping the code + * write and proceeding with a broken contract. + */ + @Test + public void deployRejectsPreExistingWrongCode() { + byte[] addr = HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS; + chainBaseManager.getCodeStore().put(addr, new CodeCapsule(new byte[]{0x60, 0x00})); + + try { + HistoryBlockHashUtil.deploy(dbManager); + fail("expected deploy to refuse foreign bytecode at canonical address"); + } catch (IllegalStateException e) { + assertTrue(e.getMessage().contains("TIP-2935")); + } + + assertFalse(chainBaseManager.getContractStore().has(addr)); + assertFalse(chainBaseManager.getAccountStore().has(addr)); + } + + @Test + public void deployRejectsPreExistingForeignContract() { + byte[] addr = HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS; + SmartContract foreign = SmartContract.newBuilder() + .setName("NotHistoryStorage") + .setContractAddress(ByteString.copyFrom(addr)) + .setOriginAddress(ByteString.copyFrom(addr)) + .build(); + chainBaseManager.getContractStore().put(addr, new ContractCapsule(foreign)); + + try { + HistoryBlockHashUtil.deploy(dbManager); + fail("expected deploy to refuse foreign contract at canonical address"); + } catch (IllegalStateException e) { + assertTrue(e.getMessage().contains("TIP-2935")); + } + + assertFalse(chainBaseManager.getCodeStore().has(addr)); + assertFalse(chainBaseManager.getAccountStore().has(addr)); + } + + @Test + public void deployRejectsPreExistingNormalAccount() { + byte[] addr = HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS; + chainBaseManager.getAccountStore().put(addr, + new AccountCapsule(ByteString.copyFrom(addr), Protocol.AccountType.Normal)); + + try { + HistoryBlockHashUtil.deploy(dbManager); + fail("expected deploy to refuse a pre-existing non-contract account"); + } catch (IllegalStateException e) { + assertTrue(e.getMessage().contains("TIP-2935")); + } + + assertFalse(chainBaseManager.getCodeStore().has(addr)); + assertFalse(chainBaseManager.getContractStore().has(addr)); + } + + /** + * Half-installed state recovery: if a prior startup crashed between the three + * writes (init() runs with revokingStore disabled, so writes are not atomic), + * the DB is left with a subset of the three entries. deployIfMissing must + * complete the install on the next start — the previous gate (which only + * checked CodeStore.has) would skip and leave the state permanently broken. + */ + @Test + public void deployIfMissingCompletesPartialInstall() { + byte[] addr = HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS; + chainBaseManager.getDynamicPropertiesStore().saveAllowTvmPrague(1L); + + chainBaseManager.getCodeStore().put(addr, new CodeCapsule(HistoryBlockHashUtil.HISTORY_STORAGE_CODE)); + assertTrue(chainBaseManager.getCodeStore().has(addr)); + assertFalse(chainBaseManager.getContractStore().has(addr)); + assertFalse(chainBaseManager.getAccountStore().has(addr)); + + HistoryBlockHashUtil.deployIfMissing(dbManager); + + assertTrue(chainBaseManager.getCodeStore().has(addr)); + assertTrue(chainBaseManager.getContractStore().has(addr)); + assertTrue(chainBaseManager.getAccountStore().has(addr)); + } } From f97bcdc0d6bc469f43f8db5f6716425742d2697d Mon Sep 17 00:00:00 2001 From: Asuka Date: Fri, 17 Apr 2026 17:14:07 +0800 Subject: [PATCH 5/5] refactor(vm): rename HistoryStorage to BlockHashHistory and fix line length The on-chain contract name ships in ContractStore and surfaces in block explorers, so pick one that reads naturally: "history of block hashes". Also breaks a 106-char line in the new partial-install test. --- .../org/tron/core/db/HistoryBlockHashUtil.java | 16 ++++++++-------- .../core/db/HistoryBlockHashIntegrationTest.java | 5 +++-- .../tron/core/db/HistoryBlockHashUtilTest.java | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) 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 902af1f39e6..bc6f3b3ba98 100644 --- a/framework/src/main/java/org/tron/core/db/HistoryBlockHashUtil.java +++ b/framework/src/main/java/org/tron/core/db/HistoryBlockHashUtil.java @@ -46,7 +46,7 @@ public class HistoryBlockHashUtil { + "611fff81430311604257611fff9006545f5260205ff3" + "5b5f5ffd5b5f35611fff60014303065500"); - public static final String HISTORY_STORAGE_NAME = "HistoryStorage"; + public static final String BLOCK_HASH_HISTORY_NAME = "BlockHashHistory"; private static final int PREFIX_BYTES = 16; @@ -68,7 +68,7 @@ public static byte[] composeStorageKey(long slot, byte[] address) { } /** - * Ensure the HistoryStorage contract is fully deployed when the flag is on. + * Ensure the BlockHashHistory contract is fully deployed when the flag is on. * Covers two recovery cases not handled by {@link ProposalService}: *
    *
  • config-boot: {@code committee.allowTvmPrague=1} without a proposal; @@ -85,9 +85,9 @@ public static void deployIfMissing(Manager manager) { } /** - * Deploy the EIP-2935 HistoryStorage contract at {@code HISTORY_STORAGE_ADDRESS}. + * Deploy the EIP-2935 BlockHashHistory contract at {@code HISTORY_STORAGE_ADDRESS}. * Validates first, writes second: any pre-existing code/contract/account at the - * canonical address must match the expected HistoryStorage; otherwise the call + * canonical address must match the expected BlockHashHistory; otherwise the call * throws and activation halts rather than silently merging with foreign state. * Whatever is missing after validation is filled in, so a half-installed state * from a crashed prior run self-heals on the next start. @@ -98,11 +98,11 @@ public static void deploy(Manager manager) { if (!manager.getCodeStore().has(addr)) { manager.getCodeStore().put(addr, new CodeCapsule(HISTORY_STORAGE_CODE)); - logger.info("TIP-2935: wrote HistoryStorage bytecode at {}", Hex.toHexString(addr)); + logger.info("TIP-2935: wrote BlockHashHistory bytecode at {}", Hex.toHexString(addr)); } if (!manager.getContractStore().has(addr)) { SmartContract sc = SmartContract.newBuilder() - .setName(HISTORY_STORAGE_NAME) + .setName(BLOCK_HASH_HISTORY_NAME) .setContractAddress(ByteString.copyFrom(addr)) .setOriginAddress(ByteString.copyFrom(addr)) .setConsumeUserResourcePercent(100L) @@ -126,11 +126,11 @@ private static void validateExistingOrThrow(Manager manager, byte[] addr) { } if (manager.getContractStore().has(addr)) { SmartContract existing = manager.getContractStore().get(addr).getInstance(); - if (!HISTORY_STORAGE_NAME.equals(existing.getName()) + if (!BLOCK_HASH_HISTORY_NAME.equals(existing.getName()) || !Arrays.equals(addr, existing.getContractAddress().toByteArray())) { throw new IllegalStateException( "TIP-2935: contract at " + Hex.toHexString(addr) - + " is not the expected HistoryStorage"); + + " is not the expected BlockHashHistory"); } } if (manager.getAccountStore().has(addr)) { 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 c6455d3e055..c0f49f24156 100644 --- a/framework/src/test/java/org/tron/core/db/HistoryBlockHashIntegrationTest.java +++ b/framework/src/test/java/org/tron/core/db/HistoryBlockHashIntegrationTest.java @@ -308,7 +308,7 @@ public void deployRejectsPreExistingWrongCode() { public void deployRejectsPreExistingForeignContract() { byte[] addr = HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS; SmartContract foreign = SmartContract.newBuilder() - .setName("NotHistoryStorage") + .setName("NotBlockHashHistory") .setContractAddress(ByteString.copyFrom(addr)) .setOriginAddress(ByteString.copyFrom(addr)) .build(); @@ -354,7 +354,8 @@ public void deployIfMissingCompletesPartialInstall() { byte[] addr = HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS; chainBaseManager.getDynamicPropertiesStore().saveAllowTvmPrague(1L); - chainBaseManager.getCodeStore().put(addr, new CodeCapsule(HistoryBlockHashUtil.HISTORY_STORAGE_CODE)); + chainBaseManager.getCodeStore() + .put(addr, new CodeCapsule(HistoryBlockHashUtil.HISTORY_STORAGE_CODE)); assertTrue(chainBaseManager.getCodeStore().has(addr)); assertFalse(chainBaseManager.getContractStore().has(addr)); assertFalse(chainBaseManager.getAccountStore().has(addr)); diff --git a/framework/src/test/java/org/tron/core/db/HistoryBlockHashUtilTest.java b/framework/src/test/java/org/tron/core/db/HistoryBlockHashUtilTest.java index 71e2182d3ab..53d766b024b 100644 --- a/framework/src/test/java/org/tron/core/db/HistoryBlockHashUtilTest.java +++ b/framework/src/test/java/org/tron/core/db/HistoryBlockHashUtilTest.java @@ -81,7 +81,7 @@ public void deployCreatesCodeContractAndAccount() { ContractCapsule contract = chainBaseManager.getContractStore().get(addr); assertNotNull(contract); - assertEquals(HistoryBlockHashUtil.HISTORY_STORAGE_NAME, + assertEquals(HistoryBlockHashUtil.BLOCK_HASH_HISTORY_NAME, contract.getInstance().getName()); assertArrayEquals(addr, contract.getInstance().getContractAddress().toByteArray()); assertEquals("version must be 0", 0, contract.getInstance().getVersion());