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..bc6f3b3ba98 --- /dev/null +++ b/framework/src/main/java/org/tron/core/db/HistoryBlockHashUtil.java @@ -0,0 +1,162 @@ +package org.tron.core.db; + +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; +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. + */ +@Slf4j(topic = "DB") +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 BLOCK_HASH_HISTORY_NAME = "BlockHashHistory"; + + 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; + } + + /** + * Ensure the BlockHashHistory 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()) { + deploy(manager); + } + } + + /** + * 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 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. + */ + 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 BlockHashHistory bytecode at {}", Hex.toHexString(addr)); + } + if (!manager.getContractStore().has(addr)) { + SmartContract sc = SmartContract.newBuilder() + .setName(BLOCK_HASH_HISTORY_NAME) + .setContractAddress(ByteString.copyFrom(addr)) + .setOriginAddress(ByteString.copyFrom(addr)) + .setConsumeUserResourcePercent(100L) + .setOriginEnergyLimit(0L) + .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 (!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 BlockHashHistory"); + } + } + 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 + * {@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) { + // 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[] storageKey = composeStorageKey(slot, HISTORY_STORAGE_ADDRESS); + byte[] parentHash = block.getParentHash().getBytes(); + manager.getStorageRowStore().put(storageKey, new StorageRowCapsule(storageKey, parentHash)); + } +} 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..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(); @@ -1850,6 +1852,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..c0f49f24156 --- /dev/null +++ b/framework/src/test/java/org/tron/core/db/HistoryBlockHashIntegrationTest.java @@ -0,0 +1,369 @@ +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 static org.junit.Assert.fail; + +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.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 + * 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); + CommonParameter.getInstance().setAllowTvmPrague(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)); + } + } + + @After + public void resetCommonParameter() { + CommonParameter.getInstance().setAllowTvmPrague(0L); + } + + @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)); + } + + /** + * 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)); + } + + /** + * 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("NotBlockHashHistory") + .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)); + } +} 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..53d766b024b --- /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.BLOCK_HASH_HISTORY_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)); + } +}