Skip to content

Commit 4f88973

Browse files
Merge pull request #6686 from yanghang8612/feat/tip-2935-historical-blockhash
feat(vm): implement TIP-2935 serve historical block hashes from state
2 parents 736e0f1 + 6ec19f5 commit 4f88973

9 files changed

Lines changed: 1017 additions & 0 deletions

File tree

actuator/src/main/java/org/tron/core/utils/ProposalUtil.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -886,6 +886,29 @@ public static void validator(DynamicPropertiesStore dynamicPropertiesStore,
886886
}
887887
break;
888888
}
889+
case ALLOW_TVM_PRAGUE: {
890+
if (!forkController.pass(ForkBlockVersionEnum.VERSION_4_8_2)) {
891+
throw new ContractValidateException(
892+
"Bad chain parameter id [ALLOW_TVM_PRAGUE]");
893+
}
894+
// The deployed BlockHashHistory bytecode contains PUSH0 (0x5f), which
895+
// is itself gated on ALLOW_TVM_SHANGHAI at execution time. Refuse the
896+
// proposal until Shanghai is enacted so an out-of-order activation
897+
// can't leave a contract whose every STATICCALL hits InvalidOpcode.
898+
if (dynamicPropertiesStore.getAllowTvmShangHai() != 1) {
899+
throw new ContractValidateException(
900+
"[ALLOW_TVM_PRAGUE] requires [ALLOW_TVM_SHANGHAI] to be enacted first");
901+
}
902+
if (dynamicPropertiesStore.getAllowTvmPrague() == 1) {
903+
throw new ContractValidateException(
904+
"[ALLOW_TVM_PRAGUE] has been valid, no need to propose again");
905+
}
906+
if (value != 1) {
907+
throw new ContractValidateException(
908+
"This value[ALLOW_TVM_PRAGUE] is only allowed to be 1");
909+
}
910+
break;
911+
}
889912
case ALLOW_HARDEN_RESOURCE_CALCULATION: {
890913
if (!forkController.pass(ForkBlockVersionEnum.VERSION_4_8_2)) {
891914
throw new ContractValidateException(
@@ -1003,6 +1026,7 @@ public enum ProposalType { // current value, value range
10031026
ALLOW_TVM_BLOB(89), // 0, 1
10041027
PROPOSAL_EXPIRE_TIME(92), // (0, 31536003000)
10051028
ALLOW_TVM_SELFDESTRUCT_RESTRICTION(94), // 0, 1
1029+
ALLOW_TVM_PRAGUE(95), // 0, 1
10061030
ALLOW_TVM_OSAKA(96), // 0, 1
10071031
ALLOW_HARDEN_RESOURCE_CALCULATION(97), // 0, 1
10081032
ALLOW_HARDEN_EXCHANGE_CALCULATION(98); // 0, 1

chainbase/src/main/java/org/tron/core/store/DynamicPropertiesStore.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,15 @@ public class DynamicPropertiesStore extends TronStoreWithRevoking<BytesCapsule>
240240

241241
private static final byte[] ALLOW_TVM_OSAKA = "ALLOW_TVM_OSAKA".getBytes();
242242

243+
private static final byte[] ALLOW_TVM_PRAGUE = "ALLOW_TVM_PRAGUE".getBytes();
244+
245+
// TIP-2935 install marker — flipped to 1 inside HistoryBlockHashUtil.deploy()
246+
// only after the three store writes succeed. Stays 0 when deploy() skips on
247+
// foreign-state collision; HistoryBlockHashUtil.write() reads this to decide
248+
// whether StorageRowStore at the canonical address is ours to mutate.
249+
private static final byte[] BLOCK_HASH_HISTORY_INSTALLED =
250+
"BLOCK_HASH_HISTORY_INSTALLED".getBytes();
251+
243252
private static final byte[] ALLOW_HARDEN_RESOURCE_CALCULATION =
244253
"ALLOW_HARDEN_RESOURCE_CALCULATION".getBytes();
245254

@@ -3002,6 +3011,36 @@ public void saveAllowTvmOsaka(long value) {
30023011
this.put(ALLOW_TVM_OSAKA, new BytesCapsule(ByteArray.fromLong(value)));
30033012
}
30043013

3014+
public long getAllowTvmPrague() {
3015+
return Optional.ofNullable(getUnchecked(ALLOW_TVM_PRAGUE))
3016+
.map(BytesCapsule::getData)
3017+
.map(ByteArray::toLong)
3018+
.orElse(0L);
3019+
}
3020+
3021+
public void saveAllowTvmPrague(long value) {
3022+
this.put(ALLOW_TVM_PRAGUE, new BytesCapsule(ByteArray.fromLong(value)));
3023+
}
3024+
3025+
public boolean allowTvmPrague() {
3026+
return getAllowTvmPrague() == 1L;
3027+
}
3028+
3029+
public long getBlockHashHistoryInstalled() {
3030+
return Optional.ofNullable(getUnchecked(BLOCK_HASH_HISTORY_INSTALLED))
3031+
.map(BytesCapsule::getData)
3032+
.map(ByteArray::toLong)
3033+
.orElse(0L);
3034+
}
3035+
3036+
public void saveBlockHashHistoryInstalled(long value) {
3037+
this.put(BLOCK_HASH_HISTORY_INSTALLED, new BytesCapsule(ByteArray.fromLong(value)));
3038+
}
3039+
3040+
public boolean isBlockHashHistoryInstalled() {
3041+
return getBlockHashHistoryInstalled() == 1L;
3042+
}
3043+
30053044
public long getAllowHardenResourceCalculation() {
30063045
return Optional.ofNullable(getUnchecked(ALLOW_HARDEN_RESOURCE_CALCULATION))
30073046
.map(BytesCapsule::getData)

framework/src/main/java/org/tron/core/Wallet.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1480,6 +1480,11 @@ public Protocol.ChainParameters getChainParameters() {
14801480
.setValue(dbManager.getDynamicPropertiesStore().getAllowTvmOsaka())
14811481
.build());
14821482

1483+
builder.addChainParameter(Protocol.ChainParameters.ChainParameter.newBuilder()
1484+
.setKey("getAllowTvmPrague")
1485+
.setValue(dbManager.getDynamicPropertiesStore().getAllowTvmPrague())
1486+
.build());
1487+
14831488
builder.addChainParameter(Protocol.ChainParameters.ChainParameter.newBuilder()
14841489
.setKey("getAllowHardenResourceCalculation")
14851490
.setValue(dbManager.getDynamicPropertiesStore().getAllowHardenResourceCalculation())

framework/src/main/java/org/tron/core/consensus/ProposalService.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import lombok.extern.slf4j.Slf4j;
55
import org.tron.core.capsule.ProposalCapsule;
66
import org.tron.core.config.Parameter.ForkBlockVersionEnum;
7+
import org.tron.core.db.HistoryBlockHashUtil;
78
import org.tron.core.db.Manager;
89
import org.tron.core.store.DynamicPropertiesStore;
910
import org.tron.core.utils.ProposalUtil;
@@ -396,6 +397,11 @@ public static boolean process(Manager manager, ProposalCapsule proposalCapsule)
396397
manager.getDynamicPropertiesStore().saveAllowTvmOsaka(entry.getValue());
397398
break;
398399
}
400+
case ALLOW_TVM_PRAGUE: {
401+
manager.getDynamicPropertiesStore().saveAllowTvmPrague(entry.getValue());
402+
HistoryBlockHashUtil.deploy(manager);
403+
break;
404+
}
399405
case ALLOW_HARDEN_RESOURCE_CALCULATION: {
400406
manager.getDynamicPropertiesStore()
401407
.saveAllowHardenResourceCalculation(entry.getValue());
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package org.tron.core.db;
2+
3+
import com.google.protobuf.ByteString;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.bouncycastle.util.encoders.Hex;
6+
import org.tron.common.runtime.vm.DataWord;
7+
import org.tron.core.capsule.AccountCapsule;
8+
import org.tron.core.capsule.BlockCapsule;
9+
import org.tron.core.capsule.CodeCapsule;
10+
import org.tron.core.capsule.ContractCapsule;
11+
import org.tron.core.vm.program.Storage;
12+
import org.tron.protos.Protocol;
13+
import org.tron.protos.Protocol.Account;
14+
import org.tron.protos.contract.SmartContractOuterClass.SmartContract;
15+
16+
/**
17+
* TIP-2935 (EIP-2935): serve historical block hashes from state.
18+
*
19+
* <p>Approach A1 — at proposal activation, deploy the BlockHashHistory bytecode
20+
* and minimal contract/account metadata via direct store writes; on every block
21+
* (before the tx loop) write the parent block hash to slot
22+
* {@code (blockNum - 1) % HISTORY_SERVE_WINDOW} via {@link Storage}.
23+
* No VM execution is needed for {@code set()}; user contracts read via normal
24+
* STATICCALL which executes the deployed bytecode.
25+
*/
26+
@Slf4j(topic = "DB")
27+
public class HistoryBlockHashUtil {
28+
29+
public static final long HISTORY_SERVE_WINDOW = 8191L;
30+
31+
// 21-byte TRON address (0x41 prefix + 20-byte EVM address 0x0000F908...2935)
32+
public static final byte[] HISTORY_STORAGE_ADDRESS =
33+
Hex.decode("410000f90827f1c53a10cb7a02335b175320002935");
34+
35+
// Recovered sender of the EIP-2935 presigned (no-private-key) deploy
36+
// transaction on Ethereum, in TRON 21-byte form. Used as {@code originAddress}
37+
// on the deployed SmartContract so the deployer-of-record matches Ethereum
38+
// byte-for-byte; cross-chain tooling that inspects this field sees the same
39+
// address on both sides.
40+
public static final byte[] HISTORY_DEPLOYER_ADDRESS =
41+
Hex.decode("413462413af4609098e1e27a490f554f260213d685");
42+
43+
// TIP-2935 runtime bytecode (83 bytes, no constructor prefix). Identical to
44+
// EIP-2935's so the same address resolves to the same code on both chains.
45+
public static final byte[] HISTORY_STORAGE_CODE = Hex.decode(
46+
"3373fffffffffffffffffffffffffffffffffffffffe"
47+
+ "14604657602036036042575f35600143038111604257"
48+
+ "611fff81430311604257611fff9006545f5260205ff3"
49+
+ "5b5f5ffd5b5f35611fff60014303065500");
50+
51+
public static final String HISTORY_STORAGE_NAME = "BlockHashHistory";
52+
53+
// Account template for the new-account branch of {@code deploy()} (no prior
54+
// state at the canonical address). Equivalent to create2's
55+
// {@code createAccount(addr, name, Contract)}: only type, accountName, and
56+
// address are set. The pre-existing-account branch never uses this template
57+
// — it mutates the existing capsule in place to preserve balance / asset
58+
// state, mirroring the CREATE2 collision path. Safe to share: the proto is
59+
// immutable, and AccountCapsule mutations rebuild via {@code toBuilder}.
60+
private static final Account HISTORY_STORAGE_ACCOUNT = Account.newBuilder()
61+
.setType(Protocol.AccountType.Contract)
62+
.setAccountName(ByteString.copyFromUtf8(HISTORY_STORAGE_NAME))
63+
.setAddress(ByteString.copyFrom(HISTORY_STORAGE_ADDRESS))
64+
.build();
65+
66+
// SmartContract template: every field is fixed at activation time, so the
67+
// proto is immutable and shared across calls. Mirrors the create2 path's
68+
// shape (version=0, contractAddress, consumeUserResourcePercent=100,
69+
// originAddress) plus a descriptive name. No trxHash since activation is
70+
// not a transaction.
71+
private static final SmartContract HISTORY_STORAGE_CONTRACT = SmartContract.newBuilder()
72+
.setName(HISTORY_STORAGE_NAME)
73+
.setContractAddress(ByteString.copyFrom(HISTORY_STORAGE_ADDRESS))
74+
.setOriginAddress(ByteString.copyFrom(HISTORY_DEPLOYER_ADDRESS))
75+
.setConsumeUserResourcePercent(100L)
76+
.build();
77+
78+
private HistoryBlockHashUtil() {
79+
}
80+
81+
/**
82+
* Deploy the TIP-2935 BlockHashHistory contract at {@code HISTORY_STORAGE_ADDRESS}.
83+
* If foreign code or contract metadata already sits at the canonical address,
84+
* logs a warning and returns without writing — the collision is deterministic
85+
* across nodes (same pre-state ⇒ same decision), so the proposal flag still
86+
* commits and chain consensus is intact. The foreign contract executes as-is
87+
* on every node; TIP-2935 functionality is silently absent at this address.
88+
* A SHA-3 pre-image of the address is the only realistic way that branch
89+
* fires, so it's belt-and-braces. A pre-existing non-contract account at the
90+
* address is the common case (anyone can transfer TRX there to activate it
91+
* as an EOA), so we upgrade its type to {@code Contract} in place — matching
92+
* the CREATE2 collision branch ({@code updateAccountType} +
93+
* {@code clearDelegatedResource}) and preserving balance/asset state.
94+
*
95+
* <p>Called only from {@code ProposalService} inside maintenance-time block
96+
* processing. Proposal validation rejects re-activation, so this runs at most
97+
* once per chain history; the three store writes share the block's revoking
98+
* session, so any node-local exception (RocksDB / IO) propagates and rolls
99+
* the {@code saveAllowTvmPrague(1)} write back atomically.
100+
*/
101+
public static void deploy(Manager manager) {
102+
if (manager.getCodeStore().has(HISTORY_STORAGE_ADDRESS)
103+
|| manager.getContractStore().has(HISTORY_STORAGE_ADDRESS)) {
104+
logger.warn("TIP-2935: foreign state at {}, skipping deploy",
105+
Hex.toHexString(HISTORY_STORAGE_ADDRESS));
106+
return;
107+
}
108+
109+
manager.getCodeStore().put(HISTORY_STORAGE_ADDRESS,
110+
new CodeCapsule(HISTORY_STORAGE_CODE));
111+
manager.getContractStore().put(HISTORY_STORAGE_ADDRESS,
112+
new ContractCapsule(HISTORY_STORAGE_CONTRACT));
113+
114+
AccountCapsule account = manager.getAccountStore().get(HISTORY_STORAGE_ADDRESS);
115+
boolean accountExisting = account != null;
116+
if (!accountExisting) {
117+
account = new AccountCapsule(HISTORY_STORAGE_ACCOUNT);
118+
} else {
119+
account.updateAccountType(Protocol.AccountType.Contract);
120+
account.clearDelegatedResource();
121+
}
122+
manager.getAccountStore().put(HISTORY_STORAGE_ADDRESS, account);
123+
124+
// Flip the install marker only after all three store writes succeed; this
125+
// gates the per-block write() path so a skipped deploy never mutates
126+
// foreign storage. Any node-local exception above propagates and rolls
127+
// the marker back together with the partial writes via the revoking session.
128+
manager.getDynamicPropertiesStore().saveBlockHashHistoryInstalled(1L);
129+
130+
logger.info("TIP-2935: deployed BlockHashHistory at {} (preExistingAccount={})",
131+
Hex.toHexString(HISTORY_STORAGE_ADDRESS), accountExisting);
132+
}
133+
134+
/**
135+
* Write the parent block hash to storage at slot
136+
* {@code (blockNum - 1) % HISTORY_SERVE_WINDOW}. Called from
137+
* {@code Manager.processBlock} before the tx loop so transactions can SLOAD
138+
* it via STATICCALL to the deployed bytecode.
139+
*/
140+
public static void write(Manager manager, BlockCapsule block) {
141+
// Genesis has no parent; applyBlock never invokes this for block 0, but be
142+
// explicit so (0-1) % 8191 = -1 in Java can never corrupt a slot.
143+
if (block.getNum() <= 0) {
144+
return;
145+
}
146+
// Defense-in-depth: deploy() skips on foreign state at the canonical
147+
// address, but the proposal flag still commits. Gate on the install
148+
// marker (set at the tail of a successful deploy()) so write() can never
149+
// overwrite an unrelated contract's storage. Single store hit, cached.
150+
if (!manager.getDynamicPropertiesStore().isBlockHashHistoryInstalled()) {
151+
return;
152+
}
153+
long slot = (block.getNum() - 1) % HISTORY_SERVE_WINDOW;
154+
Storage storage = new Storage(HISTORY_STORAGE_ADDRESS, manager.getStorageRowStore());
155+
storage.put(new DataWord(slot), new DataWord(block.getParentHash().getBytes()));
156+
storage.commit();
157+
}
158+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1638,6 +1638,7 @@ public BlockCapsule generateBlock(Miner miner, long blockTime, long timeout) {
16381638
session.reset();
16391639
session.setValue(revokingStore.buildSession());
16401640

1641+
HistoryBlockHashUtil.write(this, blockCapsule);
16411642
accountStateCallBack.preExecute(blockCapsule);
16421643

16431644
if (getDynamicPropertiesStore().getAllowMultiSign() == 1) {
@@ -1867,6 +1868,7 @@ private void processBlock(BlockCapsule block, List<TransactionCapsule> txs)
18671868

18681869
TransactionRetCapsule transactionRetCapsule =
18691870
new TransactionRetCapsule(block);
1871+
HistoryBlockHashUtil.write(this, block);
18701872
try {
18711873
merkleContainer.resetCurrentMerkleTree();
18721874
accountStateCallBack.preExecute(block);

framework/src/test/java/org/tron/core/actuator/utils/ProposalUtilTest.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,8 @@ public void validateCheck() {
343343

344344
testAllowTvmSelfdestructRestrictionProposal();
345345

346+
testAllowTvmPragueProposal();
347+
346348
testAllowHardenResourceCalculationProposal();
347349

348350
testAllowHardenExchangeCalculationProposal();
@@ -577,6 +579,8 @@ private void testAllowHardenResourceCalculationProposal() {
577579
byte[] stats = new byte[27];
578580
forkUtils.getManager().getDynamicPropertiesStore()
579581
.statsByVersion(ForkBlockVersionEnum.VERSION_4_8_1.getValue(), stats);
582+
forkUtils.getManager().getDynamicPropertiesStore()
583+
.statsByVersion(ForkBlockVersionEnum.VERSION_4_8_2.getValue(), stats);
580584
ContractValidateException e1 = Assert.assertThrows(ContractValidateException.class,
581585
() -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils,
582586
ProposalType.ALLOW_HARDEN_RESOURCE_CALCULATION.getCode(), 1));
@@ -615,6 +619,69 @@ private void testAllowHardenResourceCalculationProposal() {
615619
e3.getMessage());
616620
}
617621

622+
private void testAllowTvmPragueProposal() {
623+
byte[] stats = new byte[27];
624+
forkUtils.getManager().getDynamicPropertiesStore()
625+
.statsByVersion(ForkBlockVersionEnum.VERSION_4_8_2.getValue(), stats);
626+
try {
627+
ProposalUtil.validator(dynamicPropertiesStore, forkUtils,
628+
ProposalType.ALLOW_TVM_PRAGUE.getCode(), 1);
629+
Assert.fail();
630+
} catch (ContractValidateException e) {
631+
Assert.assertEquals(
632+
"Bad chain parameter id [ALLOW_TVM_PRAGUE]",
633+
e.getMessage());
634+
}
635+
636+
long maintenanceTimeInterval = forkUtils.getManager().getDynamicPropertiesStore()
637+
.getMaintenanceTimeInterval();
638+
long hardForkTime =
639+
((ForkBlockVersionEnum.VERSION_4_8_2.getHardForkTime() - 1) / maintenanceTimeInterval + 1)
640+
* maintenanceTimeInterval;
641+
forkUtils.getManager().getDynamicPropertiesStore()
642+
.saveLatestBlockHeaderTimestamp(hardForkTime + 1);
643+
644+
stats = new byte[27];
645+
Arrays.fill(stats, (byte) 1);
646+
forkUtils.getManager().getDynamicPropertiesStore()
647+
.statsByVersion(ForkBlockVersionEnum.VERSION_4_8_2.getValue(), stats);
648+
649+
// Fork passed but Shanghai not yet enacted: prague validator must refuse,
650+
// since the deployed bytecode uses PUSH0 (gated on ALLOW_TVM_SHANGHAI).
651+
try {
652+
ProposalUtil.validator(dynamicPropertiesStore, forkUtils,
653+
ProposalType.ALLOW_TVM_PRAGUE.getCode(), 1);
654+
Assert.fail();
655+
} catch (ContractValidateException e) {
656+
Assert.assertEquals(
657+
"[ALLOW_TVM_PRAGUE] requires [ALLOW_TVM_SHANGHAI] to be enacted first",
658+
e.getMessage());
659+
}
660+
661+
dynamicPropertiesStore.saveAllowTvmShangHai(1);
662+
663+
try {
664+
ProposalUtil.validator(dynamicPropertiesStore, forkUtils,
665+
ProposalType.ALLOW_TVM_PRAGUE.getCode(), 2);
666+
Assert.fail();
667+
} catch (ContractValidateException e) {
668+
Assert.assertEquals(
669+
"This value[ALLOW_TVM_PRAGUE] is only allowed to be 1",
670+
e.getMessage());
671+
}
672+
673+
dynamicPropertiesStore.saveAllowTvmPrague(1);
674+
try {
675+
ProposalUtil.validator(dynamicPropertiesStore, forkUtils,
676+
ProposalType.ALLOW_TVM_PRAGUE.getCode(), 1);
677+
Assert.fail();
678+
} catch (ContractValidateException e) {
679+
Assert.assertEquals(
680+
"[ALLOW_TVM_PRAGUE] has been valid, no need to propose again",
681+
e.getMessage());
682+
}
683+
}
684+
618685
private void testAllowHardenExchangeCalculationProposal() {
619686
long code = ProposalType.ALLOW_HARDEN_EXCHANGE_CALCULATION.getCode();
620687
ThrowingRunnable proposeZero = () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils,

0 commit comments

Comments
 (0)