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..d4a8f7cf0b7 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(0L); + } + + 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/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/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..49e8fa0a1e8 --- /dev/null +++ b/framework/src/main/java/org/tron/core/db/HistoryBlockHashUtil.java @@ -0,0 +1,133 @@ +package org.tron.core.db; + +import static java.lang.System.arraycopy; + +import com.google.protobuf.ByteString; +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 BlockHashHistory 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"); + + // TIP-2935 runtime bytecode (83 bytes, no constructor prefix). Identical to + // EIP-2935's so the same address resolves to the same code on both chains. + 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; + } + + /** + * Deploy the TIP-2935 BlockHashHistory contract at {@code HISTORY_STORAGE_ADDRESS}. + * If foreign code or contract metadata already sits at the canonical address, + * logs a warning and returns without writing — the collision is deterministic + * across nodes (same pre-state ⇒ same decision), so the flag still commits + * and consensus is intact; user STATICCALLs to the address return empty at + * the user level, not the chain level. A SHA-3 pre-image of the address is + * the only realistic way that branch fires, so it's belt-and-braces. A + * pre-existing non-contract account at the address is the common case (anyone + * can transfer TRX there to activate it as an EOA), so we upgrade its type to + * {@code Contract} in place, preserving balance/asset state. + * + *

Called only from {@code ProposalService} inside maintenance-time block + * processing. Proposal validation rejects re-activation, so this runs at most + * once per chain history; the three store writes share the block's revoking + * session, so any node-local exception (RocksDB / IO) propagates and rolls + * the {@code saveAllowTvmPrague(1)} write back atomically. + */ + public static void deploy(Manager manager) { + byte[] addr = HISTORY_STORAGE_ADDRESS; + if (manager.getCodeStore().has(addr) || manager.getContractStore().has(addr)) { + logger.warn("TIP-2935: foreign state at {}, skipping deploy", + Hex.toHexString(addr)); + return; + } + + manager.getCodeStore().put(addr, new CodeCapsule(HISTORY_STORAGE_CODE)); + manager.getContractStore().put(addr, new ContractCapsule(SmartContract.newBuilder() + .setName(BLOCK_HASH_HISTORY_NAME) + .setContractAddress(ByteString.copyFrom(addr)) + .setOriginAddress(ByteString.copyFrom(addr)) + .setConsumeUserResourcePercent(100L) + .setOriginEnergyLimit(0L) + .build())); + + boolean preExistingAccount = manager.getAccountStore().has(addr); + AccountCapsule account = preExistingAccount + ? manager.getAccountStore().get(addr) + : new AccountCapsule(ByteString.copyFrom(addr), Protocol.AccountType.Contract); + account.updateAccountType(Protocol.AccountType.Contract); + manager.getAccountStore().put(addr, account); + + logger.info("TIP-2935: deployed BlockHashHistory at {} (preExistingAccount={})", + Hex.toHexString(addr), preExistingAccount); + } + + /** + * 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..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..a115dc0afbc --- /dev/null +++ b/framework/src/test/java/org/tron/core/db/HistoryBlockHashIntegrationTest.java @@ -0,0 +1,261 @@ +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.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); + 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)); + } + + /** + * 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), activation must skip + * the deploy entirely — leaving the foreign code intact and writing nothing + * to ContractStore / AccountStore — rather than silently merging into a + * broken contract. Same expectation applies to foreign contract metadata. + */ + @Test + public void deploySkipsWhenForeignBytecodePresent() { + byte[] addr = HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS; + byte[] foreignCode = new byte[]{0x60, 0x00}; + chainBaseManager.getCodeStore().put(addr, new CodeCapsule(foreignCode)); + + HistoryBlockHashUtil.deploy(dbManager); + + assertArrayEquals(foreignCode, + chainBaseManager.getCodeStore().get(addr).getData()); + assertFalse(chainBaseManager.getContractStore().has(addr)); + assertFalse(chainBaseManager.getAccountStore().has(addr)); + } + + @Test + public void deploySkipsWhenForeignContractPresent() { + 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)); + + HistoryBlockHashUtil.deploy(dbManager); + + assertEquals("NotBlockHashHistory", + chainBaseManager.getContractStore().get(addr).getInstance().getName()); + assertFalse(chainBaseManager.getCodeStore().has(addr)); + assertFalse(chainBaseManager.getAccountStore().has(addr)); + } + + /** + * Anyone can transfer TRX to {@code HISTORY_STORAGE_ADDRESS} before the + * proposal fires, leaving an EOA at the canonical address. Activation must + * upgrade the type to {@code Contract} in place — preserving balance — + * rather than failing or zeroing the account. + */ + @Test + public void deployUpgradesPreExistingNormalAccountPreservingBalance() { + byte[] addr = HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS; + long balance = 12345L; + AccountCapsule eoa = new AccountCapsule( + ByteString.copyFrom(addr), Protocol.AccountType.Normal); + eoa.setBalance(balance); + chainBaseManager.getAccountStore().put(addr, eoa); + + HistoryBlockHashUtil.deploy(dbManager); + + AccountCapsule after = chainBaseManager.getAccountStore().get(addr); + assertEquals(Protocol.AccountType.Contract, after.getType()); + assertEquals(balance, after.getBalance()); + assertTrue(chainBaseManager.getCodeStore().has(addr)); + assertTrue(chainBaseManager.getContractStore().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..320d1582d0c --- /dev/null +++ b/framework/src/test/java/org/tron/core/db/HistoryBlockHashUtilTest.java @@ -0,0 +1,149 @@ +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 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)); + } +}