Skip to content
Open
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
16 changes: 16 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,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;
}
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,8 @@ 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();

@Autowired
private DynamicPropertiesStore(@Value("properties") String dbName) {
super(dbName);
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,10 @@ public class CommonParameter {
@Setter
public long allowTvmOsaka;

@Getter
@Setter
public long allowTvmPrague;

private static double calcMaxTimeRatio() {
return 5.0;
}
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 @@ -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();
}

Expand Down
4 changes: 4 additions & 0 deletions framework/src/main/java/org/tron/core/config/args/Args.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
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,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;
Expand Down
162 changes: 162 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,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.
*
* <p>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.
*
* <p>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}:
* <ul>
* <li>config-boot: {@code committee.allowTvmPrague=1} without a proposal;
* <li>partial install: a prior startup crashed between the three store
* writes, leaving a half-installed state that would otherwise never
* self-heal.
* </ul>
* 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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idempotent deploy() with per-store has() checks and the genesis guard in write() are really clean.

Minor: would it be helpful to add a log line in deploy() / deployIfMissing() for easier troubleshooting during upgrades?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the nudge — good point, one info log is cheap and pays off
the first time someone has to trace when TIP-2935 actually landed on a
given node.

Just pushed it in c4dde34: a single logger.info inside the
CodeStore write branch of deploy(), guarded by !has(addr) so it
only fires the instant fresh bytecode first lands. Steady-state restarts
stay silent.

Format:

  TIP-2935: wrote HistoryStorage bytecode at <addr-hex>

Topic is DB, matching the convention in org.tron.core.db.backup.* etc.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pushing this in — the !has(addr) guard plus @Slf4j(topic = "DB") is spot-on, and having steady-state restarts stay silent is exactly the right trade-off. 🎯

Nice bonus I noticed while reading the commit: you also took the chance to rename the constant to BlockHashHistory (clearer domain naming; the log string carries the new name too) and add validateExistingOrThrow() so deploy() now validates-first-then-writes. That pattern cleanly handles both the "canonical address already occupied by foreign state" case and the "crashed between the three store writes" partial-install case — a solid defensive improvement that goes well beyond the original log-line ask. 👍

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));
}
}
5 changes: 5 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 @@ -545,6 +545,8 @@ public void init() {
//for test only
chainBaseManager.getDynamicPropertiesStore().updateDynamicStoreByConfig();

HistoryBlockHashUtil.deployIfMissing(this);

// init liteFullNode
initLiteNode();

Expand Down Expand Up @@ -1850,6 +1852,9 @@ private void processBlock(BlockCapsule block, List<TransactionCapsule> txs)

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

testAllowTvmSelfdestructRestrictionProposal();

testAllowTvmPragueProposal();

forkUtils.getManager().getDynamicPropertiesStore()
.statsByVersion(ForkBlockVersionEnum.ENERGY_LIMIT.getValue(), stats);
forkUtils.reset();
Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading