Skip to content

Commit c4dde34

Browse files
committed
fix(vm): validate existing state before TIP-2935 deploy and self-heal half-installs
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
1 parent 967dd86 commit c4dde34

2 files changed

Lines changed: 136 additions & 11 deletions

File tree

framework/src/main/java/org/tron/core/db/HistoryBlockHashUtil.java

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import static java.lang.System.arraycopy;
44

55
import com.google.protobuf.ByteString;
6+
import java.util.Arrays;
7+
import lombok.extern.slf4j.Slf4j;
68
import org.bouncycastle.util.encoders.Hex;
79
import org.tron.common.crypto.Hash;
810
import org.tron.common.runtime.vm.DataWord;
@@ -28,6 +30,7 @@
2830
* {@code contractVersion=0}: first 16 bytes of {@code sha3(address)} followed by
2931
* the last 16 bytes of the 32-byte slot key.
3032
*/
33+
@Slf4j(topic = "DB")
3134
public class HistoryBlockHashUtil {
3235

3336
public static final long HISTORY_SERVE_WINDOW = 8191L;
@@ -65,30 +68,38 @@ public static byte[] composeStorageKey(long slot, byte[] address) {
6568
}
6669

6770
/**
68-
* Deploy when the flag is on but bytecode is missing. Covers the config-boot
69-
* path (committee.allowTvmPrague=1) where no proposal fires to trigger deploy.
70-
* Run once from {@code Manager.init()} after config is loaded into the store.
71+
* Ensure the HistoryStorage contract is fully deployed when the flag is on.
72+
* Covers two recovery cases not handled by {@link ProposalService}:
73+
* <ul>
74+
* <li>config-boot: {@code committee.allowTvmPrague=1} without a proposal;
75+
* <li>partial install: a prior startup crashed between the three store
76+
* writes, leaving a half-installed state that would otherwise never
77+
* self-heal.
78+
* </ul>
79+
* Run once from {@code Manager.init()} after config is loaded.
7180
*/
7281
public static void deployIfMissing(Manager manager) {
73-
if (manager.getDynamicPropertiesStore().allowTvmPrague()
74-
&& !manager.getCodeStore().has(HISTORY_STORAGE_ADDRESS)) {
82+
if (manager.getDynamicPropertiesStore().allowTvmPrague()) {
7583
deploy(manager);
7684
}
7785
}
7886

7987
/**
80-
* Deploy the EIP-2935 HistoryStorage contract at HISTORY_STORAGE_ADDRESS.
81-
* Writes CodeStore, ContractStore and AccountStore. Idempotent — safe to call
82-
* multiple times; only the missing entries are written.
83-
* Called once from ProposalService when ALLOW_TVM_PRAGUE activates.
88+
* Deploy the EIP-2935 HistoryStorage contract at {@code HISTORY_STORAGE_ADDRESS}.
89+
* Validates first, writes second: any pre-existing code/contract/account at the
90+
* canonical address must match the expected HistoryStorage; otherwise the call
91+
* throws and activation halts rather than silently merging with foreign state.
92+
* Whatever is missing after validation is filled in, so a half-installed state
93+
* from a crashed prior run self-heals on the next start.
8494
*/
8595
public static void deploy(Manager manager) {
8696
byte[] addr = HISTORY_STORAGE_ADDRESS;
97+
validateExistingOrThrow(manager, addr);
8798

8899
if (!manager.getCodeStore().has(addr)) {
89100
manager.getCodeStore().put(addr, new CodeCapsule(HISTORY_STORAGE_CODE));
101+
logger.info("TIP-2935: wrote HistoryStorage bytecode at {}", Hex.toHexString(addr));
90102
}
91-
92103
if (!manager.getContractStore().has(addr)) {
93104
SmartContract sc = SmartContract.newBuilder()
94105
.setName(HISTORY_STORAGE_NAME)
@@ -99,13 +110,38 @@ public static void deploy(Manager manager) {
99110
.build();
100111
manager.getContractStore().put(addr, new ContractCapsule(sc));
101112
}
102-
103113
if (!manager.getAccountStore().has(addr)) {
104114
manager.getAccountStore().put(addr,
105115
new AccountCapsule(ByteString.copyFrom(addr), Protocol.AccountType.Contract));
106116
}
107117
}
108118

119+
private static void validateExistingOrThrow(Manager manager, byte[] addr) {
120+
if (manager.getCodeStore().has(addr)) {
121+
byte[] existing = manager.getCodeStore().get(addr).getData();
122+
if (!Arrays.equals(HISTORY_STORAGE_CODE, existing)) {
123+
throw new IllegalStateException(
124+
"TIP-2935: code at " + Hex.toHexString(addr) + " differs from expected bytecode");
125+
}
126+
}
127+
if (manager.getContractStore().has(addr)) {
128+
SmartContract existing = manager.getContractStore().get(addr).getInstance();
129+
if (!HISTORY_STORAGE_NAME.equals(existing.getName())
130+
|| !Arrays.equals(addr, existing.getContractAddress().toByteArray())) {
131+
throw new IllegalStateException(
132+
"TIP-2935: contract at " + Hex.toHexString(addr)
133+
+ " is not the expected HistoryStorage");
134+
}
135+
}
136+
if (manager.getAccountStore().has(addr)) {
137+
AccountCapsule existing = manager.getAccountStore().get(addr);
138+
if (existing.getType() != Protocol.AccountType.Contract) {
139+
throw new IllegalStateException(
140+
"TIP-2935: account at " + Hex.toHexString(addr) + " exists but is not a contract");
141+
}
142+
}
143+
}
144+
109145
/**
110146
* Write the parent block hash to storage at slot
111147
* {@code (blockNum - 1) % HISTORY_SERVE_WINDOW}. Called from

framework/src/test/java/org/tron/core/db/HistoryBlockHashIntegrationTest.java

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import static org.junit.Assert.assertFalse;
66
import static org.junit.Assert.assertNotNull;
77
import static org.junit.Assert.assertTrue;
8+
import static org.junit.Assert.fail;
89

910
import com.google.protobuf.ByteString;
1011
import java.lang.reflect.Field;
@@ -17,13 +18,17 @@
1718
import org.tron.common.parameter.CommonParameter;
1819
import org.tron.common.runtime.vm.DataWord;
1920
import org.tron.common.utils.Sha256Hash;
21+
import org.tron.core.capsule.AccountCapsule;
2022
import org.tron.core.capsule.BlockCapsule;
2123
import org.tron.core.capsule.CodeCapsule;
24+
import org.tron.core.capsule.ContractCapsule;
2225
import org.tron.core.capsule.StorageRowCapsule;
2326
import org.tron.core.config.args.Args;
2427
import org.tron.core.store.DynamicPropertiesStore;
2528
import org.tron.core.store.StoreFactory;
2629
import org.tron.core.vm.repository.RepositoryImpl;
30+
import org.tron.protos.Protocol;
31+
import org.tron.protos.contract.SmartContractOuterClass.SmartContract;
2732

2833
/**
2934
* TIP-2935 end-to-end: activation deploys the contract, subsequent blocks
@@ -276,4 +281,88 @@ public void writeIsNoOpForGenesisBlock() {
276281
0L, HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS);
277282
assertFalse(chainBaseManager.getStorageRowStore().has(slot0Key));
278283
}
284+
285+
/**
286+
* Collision guard: if foreign bytecode already sits at the canonical address
287+
* (theoretically impossible short of a hash pre-image, but we fail-fast rather
288+
* than silently merging), activation must refuse instead of skipping the code
289+
* write and proceeding with a broken contract.
290+
*/
291+
@Test
292+
public void deployRejectsPreExistingWrongCode() {
293+
byte[] addr = HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS;
294+
chainBaseManager.getCodeStore().put(addr, new CodeCapsule(new byte[]{0x60, 0x00}));
295+
296+
try {
297+
HistoryBlockHashUtil.deploy(dbManager);
298+
fail("expected deploy to refuse foreign bytecode at canonical address");
299+
} catch (IllegalStateException e) {
300+
assertTrue(e.getMessage().contains("TIP-2935"));
301+
}
302+
303+
assertFalse(chainBaseManager.getContractStore().has(addr));
304+
assertFalse(chainBaseManager.getAccountStore().has(addr));
305+
}
306+
307+
@Test
308+
public void deployRejectsPreExistingForeignContract() {
309+
byte[] addr = HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS;
310+
SmartContract foreign = SmartContract.newBuilder()
311+
.setName("NotHistoryStorage")
312+
.setContractAddress(ByteString.copyFrom(addr))
313+
.setOriginAddress(ByteString.copyFrom(addr))
314+
.build();
315+
chainBaseManager.getContractStore().put(addr, new ContractCapsule(foreign));
316+
317+
try {
318+
HistoryBlockHashUtil.deploy(dbManager);
319+
fail("expected deploy to refuse foreign contract at canonical address");
320+
} catch (IllegalStateException e) {
321+
assertTrue(e.getMessage().contains("TIP-2935"));
322+
}
323+
324+
assertFalse(chainBaseManager.getCodeStore().has(addr));
325+
assertFalse(chainBaseManager.getAccountStore().has(addr));
326+
}
327+
328+
@Test
329+
public void deployRejectsPreExistingNormalAccount() {
330+
byte[] addr = HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS;
331+
chainBaseManager.getAccountStore().put(addr,
332+
new AccountCapsule(ByteString.copyFrom(addr), Protocol.AccountType.Normal));
333+
334+
try {
335+
HistoryBlockHashUtil.deploy(dbManager);
336+
fail("expected deploy to refuse a pre-existing non-contract account");
337+
} catch (IllegalStateException e) {
338+
assertTrue(e.getMessage().contains("TIP-2935"));
339+
}
340+
341+
assertFalse(chainBaseManager.getCodeStore().has(addr));
342+
assertFalse(chainBaseManager.getContractStore().has(addr));
343+
}
344+
345+
/**
346+
* Half-installed state recovery: if a prior startup crashed between the three
347+
* writes (init() runs with revokingStore disabled, so writes are not atomic),
348+
* the DB is left with a subset of the three entries. deployIfMissing must
349+
* complete the install on the next start — the previous gate (which only
350+
* checked CodeStore.has) would skip and leave the state permanently broken.
351+
*/
352+
@Test
353+
public void deployIfMissingCompletesPartialInstall() {
354+
byte[] addr = HistoryBlockHashUtil.HISTORY_STORAGE_ADDRESS;
355+
chainBaseManager.getDynamicPropertiesStore().saveAllowTvmPrague(1L);
356+
357+
chainBaseManager.getCodeStore().put(addr, new CodeCapsule(HistoryBlockHashUtil.HISTORY_STORAGE_CODE));
358+
assertTrue(chainBaseManager.getCodeStore().has(addr));
359+
assertFalse(chainBaseManager.getContractStore().has(addr));
360+
assertFalse(chainBaseManager.getAccountStore().has(addr));
361+
362+
HistoryBlockHashUtil.deployIfMissing(dbManager);
363+
364+
assertTrue(chainBaseManager.getCodeStore().has(addr));
365+
assertTrue(chainBaseManager.getContractStore().has(addr));
366+
assertTrue(chainBaseManager.getAccountStore().has(addr));
367+
}
279368
}

0 commit comments

Comments
 (0)