Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions actuator/src/main/java/org/tron/core/utils/ProposalUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,29 @@ 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]");
}
// The deployed BlockHashHistory bytecode contains PUSH0 (0x5f), which
// is itself gated on ALLOW_TVM_SHANGHAI at execution time. Refuse the
// proposal until Shanghai is enacted so an out-of-order activation
// can't leave a contract whose every STATICCALL hits InvalidOpcode.
if (dynamicPropertiesStore.getAllowTvmShangHai() != 1) {
throw new ContractValidateException(
"[ALLOW_TVM_PRAGUE] requires [ALLOW_TVM_SHANGHAI] to be enacted first");
}
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;
}
case ALLOW_HARDEN_RESOURCE_CALCULATION: {
if (!forkController.pass(ForkBlockVersionEnum.VERSION_4_8_2)) {
throw new ContractValidateException(
Expand Down Expand Up @@ -1003,6 +1026,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
ALLOW_HARDEN_RESOURCE_CALCULATION(97), // 0, 1
ALLOW_HARDEN_EXCHANGE_CALCULATION(98); // 0, 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,15 @@ public class DynamicPropertiesStore extends TronStoreWithRevoking<BytesCapsule>

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

private static final byte[] ALLOW_TVM_PRAGUE = "ALLOW_TVM_PRAGUE".getBytes();

// TIP-2935 install marker — flipped to 1 inside HistoryBlockHashUtil.deploy()
// only after the three store writes succeed. Stays 0 when deploy() skips on
// foreign-state collision; HistoryBlockHashUtil.write() reads this to decide
// whether StorageRowStore at the canonical address is ours to mutate.
private static final byte[] BLOCK_HASH_HISTORY_INSTALLED =
"BLOCK_HASH_HISTORY_INSTALLED".getBytes();

private static final byte[] ALLOW_HARDEN_RESOURCE_CALCULATION =
"ALLOW_HARDEN_RESOURCE_CALCULATION".getBytes();

Expand Down Expand Up @@ -2999,6 +3008,36 @@ 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;
}

public long getBlockHashHistoryInstalled() {
return Optional.ofNullable(getUnchecked(BLOCK_HASH_HISTORY_INSTALLED))
.map(BytesCapsule::getData)
.map(ByteArray::toLong)
.orElse(0L);
}

public void saveBlockHashHistoryInstalled(long value) {
this.put(BLOCK_HASH_HISTORY_INSTALLED, new BytesCapsule(ByteArray.fromLong(value)));
}

public boolean isBlockHashHistoryInstalled() {
return getBlockHashHistoryInstalled() == 1L;
}

public long getAllowHardenResourceCalculation() {
return Optional.ofNullable(getUnchecked(ALLOW_HARDEN_RESOURCE_CALCULATION))
.map(BytesCapsule::getData)
Expand Down
5 changes: 5 additions & 0 deletions framework/src/main/java/org/tron/core/Wallet.java
Original file line number Diff line number Diff line change
Expand Up @@ -1480,6 +1480,11 @@ public Protocol.ChainParameters getChainParameters() {
.setValue(dbManager.getDynamicPropertiesStore().getAllowTvmOsaka())
.build());

builder.addChainParameter(Protocol.ChainParameters.ChainParameter.newBuilder()
.setKey("getAllowTvmPrague")
.setValue(dbManager.getDynamicPropertiesStore().getAllowTvmPrague())
.build());

builder.addChainParameter(Protocol.ChainParameters.ChainParameter.newBuilder()
.setKey("getAllowHardenResourceCalculation")
.setValue(dbManager.getDynamicPropertiesStore().getAllowHardenResourceCalculation())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -396,6 +397,11 @@ public static boolean process(Manager manager, ProposalCapsule proposalCapsule)
manager.getDynamicPropertiesStore().saveAllowTvmOsaka(entry.getValue());
break;
}
case ALLOW_TVM_PRAGUE: {
manager.getDynamicPropertiesStore().saveAllowTvmPrague(entry.getValue());
HistoryBlockHashUtil.deploy(manager);
break;
}
case ALLOW_HARDEN_RESOURCE_CALCULATION: {
manager.getDynamicPropertiesStore()
.saveAllowHardenResourceCalculation(entry.getValue());
Expand Down
158 changes: 158 additions & 0 deletions framework/src/main/java/org/tron/core/db/HistoryBlockHashUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package org.tron.core.db;

import com.google.protobuf.ByteString;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.util.encoders.Hex;
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.vm.program.Storage;
import org.tron.protos.Protocol;
import org.tron.protos.Protocol.Account;
import org.tron.protos.contract.SmartContractOuterClass.SmartContract;

/**
* TIP-2935 (EIP-2935): serve historical block hashes from state.
*
* <p>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 {@link Storage}.
* No VM execution is needed for {@code set()}; user contracts read via normal
* STATICCALL which executes the deployed bytecode.
*/
@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");

// Recovered sender of the EIP-2935 presigned (no-private-key) deploy
// transaction on Ethereum, in TRON 21-byte form. Used as {@code originAddress}
// on the deployed SmartContract so the deployer-of-record matches Ethereum
// byte-for-byte; cross-chain tooling that inspects this field sees the same
// address on both sides.
public static final byte[] HISTORY_DEPLOYER_ADDRESS =
Hex.decode("413462413af4609098e1e27a490f554f260213d685");

// 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 HISTORY_STORAGE_NAME = "BlockHashHistory";

// Account template for the new-account branch of {@code deploy()} (no prior
// state at the canonical address). Equivalent to create2's
// {@code createAccount(addr, name, Contract)}: only type, accountName, and
// address are set. The pre-existing-account branch never uses this template
// — it mutates the existing capsule in place to preserve balance / asset
// state, mirroring the CREATE2 collision path. Safe to share: the proto is
// immutable, and AccountCapsule mutations rebuild via {@code toBuilder}.
private static final Account HISTORY_STORAGE_ACCOUNT = Account.newBuilder()
.setType(Protocol.AccountType.Contract)
.setAccountName(ByteString.copyFromUtf8(HISTORY_STORAGE_NAME))
.setAddress(ByteString.copyFrom(HISTORY_STORAGE_ADDRESS))
.build();

// SmartContract template: every field is fixed at activation time, so the
// proto is immutable and shared across calls. Mirrors the create2 path's
// shape (version=0, contractAddress, consumeUserResourcePercent=100,
// originAddress) plus a descriptive name. No trxHash since activation is
// not a transaction.
private static final SmartContract HISTORY_STORAGE_CONTRACT = SmartContract.newBuilder()
.setName(HISTORY_STORAGE_NAME)
.setContractAddress(ByteString.copyFrom(HISTORY_STORAGE_ADDRESS))
.setOriginAddress(ByteString.copyFrom(HISTORY_DEPLOYER_ADDRESS))
.setConsumeUserResourcePercent(100L)
.build();

private HistoryBlockHashUtil() {
}

/**
* 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 proposal flag still
* commits and chain consensus is intact. The foreign contract executes as-is
* on every node; TIP-2935 functionality is silently absent at this address.
* 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 — matching
* the CREATE2 collision branch ({@code updateAccountType} +
* {@code clearDelegatedResource}) and preserving balance/asset state.
*
* <p>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) {
if (manager.getCodeStore().has(HISTORY_STORAGE_ADDRESS)
|| manager.getContractStore().has(HISTORY_STORAGE_ADDRESS)) {
logger.warn("TIP-2935: foreign state at {}, skipping deploy",
Hex.toHexString(HISTORY_STORAGE_ADDRESS));
return;
}

manager.getCodeStore().put(HISTORY_STORAGE_ADDRESS,
new CodeCapsule(HISTORY_STORAGE_CODE));
manager.getContractStore().put(HISTORY_STORAGE_ADDRESS,
new ContractCapsule(HISTORY_STORAGE_CONTRACT));

AccountCapsule account = manager.getAccountStore().get(HISTORY_STORAGE_ADDRESS);
boolean accountExisting = account != null;
if (!accountExisting) {
account = new AccountCapsule(HISTORY_STORAGE_ACCOUNT);
} else {
account.updateAccountType(Protocol.AccountType.Contract);
account.clearDelegatedResource();
}
manager.getAccountStore().put(HISTORY_STORAGE_ADDRESS, account);

// Flip the install marker only after all three store writes succeed; this
// gates the per-block write() path so a skipped deploy never mutates
// foreign storage. Any node-local exception above propagates and rolls
// the marker back together with the partial writes via the revoking session.
manager.getDynamicPropertiesStore().saveBlockHashHistoryInstalled(1L);

logger.info("TIP-2935: deployed BlockHashHistory at {} (preExistingAccount={})",
Hex.toHexString(HISTORY_STORAGE_ADDRESS), accountExisting);
}

/**
* 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;
}
// Defense-in-depth: deploy() skips on foreign state at the canonical
// address, but the proposal flag still commits. Gate on the install
// marker (set at the tail of a successful deploy()) so write() can never
// overwrite an unrelated contract's storage. Single store hit, cached.
if (!manager.getDynamicPropertiesStore().isBlockHashHistoryInstalled()) {
return;
}
long slot = (block.getNum() - 1) % HISTORY_SERVE_WINDOW;
Storage storage = new Storage(HISTORY_STORAGE_ADDRESS, manager.getStorageRowStore());
storage.put(new DataWord(slot), new DataWord(block.getParentHash().getBytes()));
storage.commit();
}
}
2 changes: 2 additions & 0 deletions framework/src/main/java/org/tron/core/db/Manager.java
Original file line number Diff line number Diff line change
Expand Up @@ -1629,6 +1629,7 @@ public BlockCapsule generateBlock(Miner miner, long blockTime, long timeout) {
session.reset();
session.setValue(revokingStore.buildSession());

HistoryBlockHashUtil.write(this, blockCapsule);
accountStateCallBack.preExecute(blockCapsule);

if (getDynamicPropertiesStore().getAllowMultiSign() == 1) {
Expand Down Expand Up @@ -1858,6 +1859,7 @@ private void processBlock(BlockCapsule block, List<TransactionCapsule> txs)

TransactionRetCapsule transactionRetCapsule =
new TransactionRetCapsule(block);
HistoryBlockHashUtil.write(this, block);
try {
merkleContainer.resetCurrentMerkleTree();
accountStateCallBack.preExecute(block);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,8 @@ public void validateCheck() {

testAllowTvmSelfdestructRestrictionProposal();

testAllowTvmPragueProposal();

testAllowHardenResourceCalculationProposal();

testAllowHardenExchangeCalculationProposal();
Expand Down Expand Up @@ -577,6 +579,8 @@ private void testAllowHardenResourceCalculationProposal() {
byte[] stats = new byte[27];
forkUtils.getManager().getDynamicPropertiesStore()
.statsByVersion(ForkBlockVersionEnum.VERSION_4_8_1.getValue(), stats);
forkUtils.getManager().getDynamicPropertiesStore()
.statsByVersion(ForkBlockVersionEnum.VERSION_4_8_2.getValue(), stats);
ContractValidateException e1 = Assert.assertThrows(ContractValidateException.class,
() -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils,
ProposalType.ALLOW_HARDEN_RESOURCE_CALCULATION.getCode(), 1));
Expand Down Expand Up @@ -615,6 +619,69 @@ private void testAllowHardenResourceCalculationProposal() {
e3.getMessage());
}

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);

// Fork passed but Shanghai not yet enacted: prague validator must refuse,
// since the deployed bytecode uses PUSH0 (gated on ALLOW_TVM_SHANGHAI).
try {
ProposalUtil.validator(dynamicPropertiesStore, forkUtils,
ProposalType.ALLOW_TVM_PRAGUE.getCode(), 1);
Assert.fail();
} catch (ContractValidateException e) {
Assert.assertEquals(
"[ALLOW_TVM_PRAGUE] requires [ALLOW_TVM_SHANGHAI] to be enacted first",
e.getMessage());
}

dynamicPropertiesStore.saveAllowTvmShangHai(1);

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 testAllowHardenExchangeCalculationProposal() {
long code = ProposalType.ALLOW_HARDEN_EXCHANGE_CALCULATION.getCode();
ThrowingRunnable proposeZero = () -> ProposalUtil.validator(dynamicPropertiesStore, forkUtils,
Expand Down
Loading
Loading