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
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ public HttpRateLimiterItem(ConfigObject asset) {
strategy = asset.get("strategy").unwrapped().toString();
params = asset.get("paramString").unwrapped().toString();
}

public HttpRateLimiterItem(String component, String strategy, String params) {
this.component = component;
this.strategy = strategy;
this.params = params;
}
}


Expand All @@ -93,5 +99,11 @@ public RpcRateLimiterItem(ConfigObject asset) {
strategy = asset.get("strategy").unwrapped().toString();
params = asset.get("paramString").unwrapped().toString();
}

public RpcRateLimiterItem(String component, String strategy, String params) {
this.component = component;
this.strategy = strategy;
this.params = params;
}
}
}
3 changes: 2 additions & 1 deletion common/src/main/java/org/tron/core/config/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ public static com.typesafe.config.Config getByFileName(

private static void resolveConfigFile(String fileName, File confFile) {
if (confFile.exists()) {
config = ConfigFactory.parseFile(confFile);
config = ConfigFactory.parseFile(confFile)
.withFallback(ConfigFactory.defaultReference());
} else if (Thread.currentThread().getContextClassLoader().getResourceAsStream(fileName)
!= null) {
config = ConfigFactory.load(fileName);
Expand Down
55 changes: 55 additions & 0 deletions common/src/main/java/org/tron/core/config/args/BlockConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.tron.core.config.args;

import static org.tron.core.Constant.DEFAULT_PROPOSAL_EXPIRE_TIME;
import static org.tron.core.Constant.MAX_PROPOSAL_EXPIRE_TIME;
import static org.tron.core.Constant.MIN_PROPOSAL_EXPIRE_TIME;
import static org.tron.core.exception.TronError.ErrCode.PARAMETER_INIT;

import com.typesafe.config.Config;
import com.typesafe.config.ConfigBeanFactory;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.tron.core.exception.TronError;

/**
* Block configuration bean. Field names match config.conf keys under the "block" section.
*/
@Slf4j
@Getter
@Setter
public class BlockConfig {

private boolean needSyncCheck = false;
private long maintenanceTimeInterval = 21600000L;
private long proposalExpireTime = DEFAULT_PROPOSAL_EXPIRE_TIME;
private int checkFrozenTime = 1;

// Defaults come from reference.conf (loaded globally via Configuration.java)

/**
* Create BlockConfig from the "block" section of the application config.
* Also checks that committee.proposalExpireTime is not used (must use block.proposalExpireTime).
*/
public static BlockConfig fromConfig(Config config) {
// Reject legacy committee.proposalExpireTime location
if (config.hasPath("committee.proposalExpireTime")) {
throw new TronError("It is not allowed to configure committee.proposalExpireTime in "
+ "config.conf, please set the value in block.proposalExpireTime.", PARAMETER_INIT);
}

Config blockSection = config.getConfig("block");
BlockConfig blockConfig = ConfigBeanFactory.create(blockSection, BlockConfig.class);
blockConfig.postProcess();
return blockConfig;
}

private void postProcess() {
if (proposalExpireTime <= MIN_PROPOSAL_EXPIRE_TIME
|| proposalExpireTime >= MAX_PROPOSAL_EXPIRE_TIME) {
throw new TronError("The value[block.proposalExpireTime] is only allowed to "
+ "be greater than " + MIN_PROPOSAL_EXPIRE_TIME + " and less than "
+ MAX_PROPOSAL_EXPIRE_TIME + "!", PARAMETER_INIT);
}
}
}
170 changes: 170 additions & 0 deletions common/src/main/java/org/tron/core/config/args/CommitteeConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package org.tron.core.config.args;

import com.typesafe.config.Config;
import com.typesafe.config.ConfigBeanFactory;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

/**
* Committee (governance) configuration bean.
* Field names match config.conf keys under the "committee" section.
* All fields are governance proposal toggles, default 0 (disabled).
*/
@Slf4j
@Getter
@Setter
@SuppressWarnings("unused") // setters used by ConfigBeanFactory via reflection
public class CommitteeConfig {

private long allowCreationOfContracts = 0;
private long allowMultiSign = 0;
private long allowAdaptiveEnergy = 0;
private long allowDelegateResource = 0;
private long allowSameTokenName = 0;
private long allowTvmTransferTrc10 = 0;
private long allowTvmConstantinople = 0;
private long allowTvmSolidity059 = 0;
private long forbidTransferToContract = 0;
private long allowShieldedTRC20Transaction = 0;
private long allowMarketTransaction = 0;
private long allowTransactionFeePool = 0;
private long allowBlackHoleOptimization = 0;
private long allowNewResourceModel = 0;
private long allowTvmIstanbul = 0;
private long allowProtoFilterNum = 0;
private long allowAccountStateRoot = 0;
private long changedDelegation = 0;
// NON-STANDARD NAMING: "allowPBFT" and "pBFTExpireNum" in config.conf contain
// consecutive uppercase letters ("PBFT"), which violates JavaBean naming convention.
// ConfigBeanFactory derives config keys from setter names using JavaBean rules:
// setPBFTExpireNum -> property "PBFTExpireNum" (capital P, per JavaBean spec)
// but config.conf uses "pBFTExpireNum" (lowercase p) -> mismatch -> binding fails.
//
// These two fields are excluded from auto-binding and handled manually in fromConfig().
// TODO: Rename config keys to standard camelCase (allowPbft, pbftExpireNum) when
// PBFT feature is enabled and a breaking config change is acceptable.
@Getter(lombok.AccessLevel.NONE)
@Setter(lombok.AccessLevel.NONE)
private long allowPBFT = 0;
@Getter(lombok.AccessLevel.NONE)
@Setter(lombok.AccessLevel.NONE)
private long pBFTExpireNum = 20;

public long getAllowPBFT() { return allowPBFT; }
public void setAllowPBFT(long v) { this.allowPBFT = v; }
public long getPBFTExpireNum() { return pBFTExpireNum; }
public void setPBFTExpireNum(long v) { this.pBFTExpireNum = v; }
private long allowTvmFreeze = 0;
private long allowTvmVote = 0;
private long allowTvmLondon = 0;
private long allowTvmCompatibleEvm = 0;
private long allowHigherLimitForMaxCpuTimeOfOneTx = 0;
private long allowNewRewardAlgorithm = 0;
private long allowOptimizedReturnValueOfChainId = 0;
private long allowTvmShangHai = 0;
private long allowOldRewardOpt = 0;
private long allowEnergyAdjustment = 0;
private long allowStrictMath = 0;
private long consensusLogicOptimization = 0;
private long allowTvmCancun = 0;
private long allowTvmBlob = 0;
private long allowTvmOsaka = 0;
private long unfreezeDelayDays = 0;
private long allowReceiptsMerkleRoot = 0;
private long allowAccountAssetOptimization = 0;
private long allowAssetOptimization = 0;
private long allowNewReward = 0;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Behavioral regression] allowNewReward clamping is lost — and this changes the cross-field check semantics below.

The original Args.java clamped allowNewReward to [0, 1]:

// Args.java (develop branch)
if (config.hasPath(ConfigKey.ALLOW_NEW_REWARD)) {
  PARAMETER.allowNewReward = config.getLong(ConfigKey.ALLOW_NEW_REWARD);
  if (PARAMETER.allowNewReward > 1) { PARAMETER.allowNewReward = 1; }
  if (PARAMETER.allowNewReward < 0) { PARAMETER.allowNewReward = 0; }
}

This is especially critical because the cross-field validation in postProcess() (line 152) uses allowNewReward != 1:

if (allowOldRewardOpt == 1 && allowNewRewardAlgorithm != 1
    && allowNewReward != 1 && allowTvmVote != 1) {
  throw new IllegalArgumentException(...);
}

Without clamping, the semantics diverge from develop:

Config develop behavior this PR behavior
allowNewReward = 2 + allowOldRewardOpt = 1 Clamped to 1, check passes Stays 2, check fails, throws exception
allowNewReward = 99 Clamped to 1 Stays 99
allowNewReward = -5 Clamped to 0 Stays -5

A user who configured allowNewReward = 2 (intending to enable it) would have their node start fine on develop, but fail to start after this PR.

Suggested fix in postProcess(), before the cross-field check at line 152:

if (allowNewReward < 0) { allowNewReward = 0; }
if (allowNewReward > 1) { allowNewReward = 1; }

Verified by a regression test against this PR branch — the cross-field semantic test fails without this clamp.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same fix in c38f388 — see the reply on the allowNewReward thread above. Thanks again.

private long memoFee = 0;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Behavioral regression] memoFee clamping is lost.

The original Args.java clamped memoFee to [0, 1_000_000_000]:

// Args.java (develop branch)
if (config.hasPath(ConfigKey.MEMO_FEE)) {
  PARAMETER.memoFee = config.getLong(ConfigKey.MEMO_FEE);
  if (PARAMETER.memoFee > 1_000_000_000) {
    PARAMETER.memoFee = 1_000_000_000;
  }
  if (PARAMETER.memoFee < 0) {
    PARAMETER.memoFee = 0;
  }
}

After this PR, postProcess() does not clamp memoFee, so out-of-range values are passed through unchanged. For example:

  • committee.memoFee = 5000000000 → develop clamps to 1_000_000_000, this PR keeps 5_000_000_000
  • committee.memoFee = -100 → develop clamps to 0, this PR keeps -100

Suggested fix in postProcess():

if (memoFee < 0) { memoFee = 0; }
if (memoFee > 1_000_000_000L) { memoFee = 1_000_000_000L; }

Verified by a regression test against this PR branch — 2 of 5 memoFee boundary tests fail without this clamp.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Great catch — fixed in c38f388. Both clamps restored in CommitteeConfig.postProcess():

if (allowNewReward < 0) { allowNewReward = 0; }
if (allowNewReward > 1) { allowNewReward = 1; }

if (memoFee < 0) { memoFee = 0; }
if (memoFee > 1_000_000_000L) { memoFee = 1_000_000_000L; }

Also added 31 boundary test cases across CommitteeConfigTest, NodeConfigTest, VmConfigTest, and ArgsTest to pin every clamp (below/above/in-range) so this kind of regression cannot happen silently again. Specifically pinned the allowNewReward = 2 + allowOldRewardOpt = 1 case you flagged — verifies the clamp runs before the cross-field check.

Thanks for the rigorous review — the original Args.java had no test coverage for these clamps, so writing the regression test was the only way this could have been caught.

private long allowDelegateOptimization = 0;
private long allowDynamicEnergy = 0;
private long dynamicEnergyThreshold = 0;
private long dynamicEnergyIncreaseFactor = 0;
private long dynamicEnergyMaxFactor = 0;

// proposalExpireTime is NOT a committee field — it's in block.* and handled by BlockConfig

// Defaults come from reference.conf (loaded globally via Configuration.java)

/**
* Create CommitteeConfig from the "committee" section of the application config.
*
* Note: allowPBFT and pBFTExpireNum have non-standard JavaBean naming (consecutive
* uppercase letters) which causes ConfigBeanFactory key mismatch. These two fields
* are excluded from automatic binding and handled manually after.
*/
private static final String PBFT_EXPIRE_NUM_KEY = "pBFTExpireNum";
private static final String ALLOW_PBFT_KEY = "allowPBFT";

public static CommitteeConfig fromConfig(Config config) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggest to replace class-level @Getter/@Setter with per-field annotations in CommitteeConfig.

The previous approach used class-level annotations but silently suppressed them on allowPBFT and pBFTExpireNum via AccessLevel.NONE. This is misleading — a reader assumes all fields are covered until they scan the entire file to find the exceptions. The code has poor readability.

Per-field annotations make each field's contract explicit. The two non-standard fields carry no Lombok annotations at all, so their manual accessors immediately signal that special handling is required.

There is no need to modify the configuration parameter name solely due to case differences.

Lombok best practice: use class-level annotations only when the policy is uniform across all fields; switch to per-field when exceptions exist.

Suggest to optimize like this concisely:

//no Getter Setter
@Slf4j
public class CommitteeConfig {

  @Getter @Setter private long allowCreationOfContracts = 0;
  ...
  @Getter @Setter private long changedDelegation = 0;

  // These two fields which violates JavaBean naming convention are excluded from auto-binding and
  // handled manually in fromConfig().
  @Getter private long allowPBFT = 0;
  @Getter private long pBFTExpireNum = 20;

  @Getter @Setter private long allowTvmFreeze = 0;
   ...
  @Getter @Setter private long dynamicEnergyMaxFactor = 0;

  // proposalExpireTime is NOT a committee field — it's in block.* and handled by BlockConfig

  // Defaults come from reference.conf (loaded globally via Configuration.java)

  private static final String PBFT_EXPIRE_NUM_KEY = "pBFTExpireNum";
  private static final String ALLOW_PBFT_KEY = "allowPBFT";

  public static CommitteeConfig fromConfig(Config config) {
    Config section = config.getConfig("committee");

    CommitteeConfig cc = ConfigBeanFactory.create(section, CommitteeConfig.class);
    // Ensure the manually-named fields get the right values from the original keys
    cc.allowPBFT = section.hasPath(ALLOW_PBFT_KEY) ? section.getLong(ALLOW_PBFT_KEY) : 0;
    cc.pBFTExpireNum = section.hasPath(PBFT_EXPIRE_NUM_KEY)
        ? section.getLong(PBFT_EXPIRE_NUM_KEY) : 20;

    cc.postProcess();
    return cc;
  }

  private void postProcess() {
  ...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Re: CommitteeConfig per-field @Getter/https://github.com/Setter — Good suggestion for readability. However, the root cause is that allowPBFT and pBFTExpireNum violate JavaBean naming convention (consecutive uppercase "PBFT"). Switching annotation style is a cosmetic fix.

Since PBFT is not yet active, I'd suggest addressing this in a future version by renaming the config keys to standard camelCase (allowPbft, pbftExpireNum). That eliminates the need for manual binding entirely, and the annotation style issue goes away with it. The same approach should apply to other beans (NodeConfig, StorageConfig, EventConfig) that have similar AccessLevel.NONE workarounds.

Config section = config.getConfig("committee");

// ConfigBeanFactory derives key names from setter methods. For setPBFTExpireNum()
// it expects "PBFTExpireNum" (capital P), but config.conf uses "pBFTExpireNum".
// Add uppercase alias so ConfigBeanFactory can find it.
Config aliased = section;
if (section.hasPath(PBFT_EXPIRE_NUM_KEY) && !section.hasPath("PBFTExpireNum")) {
aliased = aliased.withValue("PBFTExpireNum", section.getValue(PBFT_EXPIRE_NUM_KEY));
}

CommitteeConfig cc = ConfigBeanFactory.create(aliased, CommitteeConfig.class);
// Ensure the manually-named fields get the right values from the original keys
cc.allowPBFT = section.hasPath(ALLOW_PBFT_KEY) ? section.getLong(ALLOW_PBFT_KEY) : 0;
cc.pBFTExpireNum = section.hasPath(PBFT_EXPIRE_NUM_KEY)
? section.getLong(PBFT_EXPIRE_NUM_KEY) : 20;

cc.postProcess();
return cc;
}

private void postProcess() {
// clamp unfreezeDelayDays to 0-365
if (unfreezeDelayDays < 0) {
unfreezeDelayDays = 0;
}
if (unfreezeDelayDays > 365) {
unfreezeDelayDays = 365;
}

// clamp allowDelegateOptimization to 0-1
if (allowDelegateOptimization < 0) { allowDelegateOptimization = 0; }
if (allowDelegateOptimization > 1) { allowDelegateOptimization = 1; }

// clamp allowDynamicEnergy to 0-1
if (allowDynamicEnergy < 0) { allowDynamicEnergy = 0; }
if (allowDynamicEnergy > 1) { allowDynamicEnergy = 1; }

// clamp dynamicEnergyThreshold to 0-100_000_000_000_000_000
if (dynamicEnergyThreshold < 0) { dynamicEnergyThreshold = 0; }
if (dynamicEnergyThreshold > 100_000_000_000_000_000L) {
dynamicEnergyThreshold = 100_000_000_000_000_000L;
}

// clamp dynamicEnergyIncreaseFactor to 0-10_000
if (dynamicEnergyIncreaseFactor < 0) { dynamicEnergyIncreaseFactor = 0; }
if (dynamicEnergyIncreaseFactor > 10_000L) { dynamicEnergyIncreaseFactor = 10_000L; }

// clamp dynamicEnergyMaxFactor to 0-100_000
if (dynamicEnergyMaxFactor < 0) { dynamicEnergyMaxFactor = 0; }
if (dynamicEnergyMaxFactor > 100_000L) { dynamicEnergyMaxFactor = 100_000L; }

// clamp allowNewReward to 0-1 (must run BEFORE the cross-field check below,
// which depends on allowNewReward != 1)
if (allowNewReward < 0) { allowNewReward = 0; }
if (allowNewReward > 1) { allowNewReward = 1; }

// clamp memoFee to 0-1_000_000_000
if (memoFee < 0) { memoFee = 0; }
if (memoFee > 1_000_000_000L) { memoFee = 1_000_000_000L; }

// cross-field: allowOldRewardOpt requires at least one reward/vote flag
if (allowOldRewardOpt == 1 && allowNewRewardAlgorithm != 1
&& allowNewReward != 1 && allowTvmVote != 1) {
throw new IllegalArgumentException(
"At least one of the following proposals is required to be opened first: "
+ "committee.allowNewRewardAlgorithm = 1"
+ " or committee.allowNewReward = 1"
+ " or committee.allowTvmVote = 1.");
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It should throws TronError instead of IllegalArgumentException here, as the IllegalArgumentException here appears to be an oversight and may be silently swallowed by an upstream catch block, causing the node to start with an invalid committee configuration instead of exiting immediately.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agree that TronError is the better choice semantically. However, this PR is a pure refactor — the original code in Args.java used IllegalArgumentException here, so I kept it unchanged to avoid behavioral changes.

Also, in the current call stack (FullNode.main → Args.setParam → applyConfigParams → CommitteeConfig.fromConfig → postProcess), there is no catch(Exception) that would swallow it. The IllegalArgumentException propagates to the UncaughtExceptionHandler and exits the node correctly.

That said, unifying all config validation errors to TronError would be a good follow-up.

}
}
134 changes: 134 additions & 0 deletions common/src/main/java/org/tron/core/config/args/EventConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package org.tron.core.config.args;

import com.typesafe.config.Config;
import com.typesafe.config.ConfigBeanFactory;
import com.typesafe.config.ConfigFactory;
import java.util.ArrayList;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

/**
* Event subscribe configuration bean.
* Field names match config.conf keys under "event.subscribe".
*/
@Slf4j
@Getter
@Setter
public class EventConfig {

private boolean enable = false;
private int version = 0;
private long startSyncBlockNum = 0;
private String path = "";
private String server = "";
private String dbconfig = "";
private boolean contractParse = true;
@Getter(lombok.AccessLevel.NONE)
@Setter(lombok.AccessLevel.NONE)
private NativeConfig nativeQueue = new NativeConfig();

public NativeConfig getNativeQueue() { return nativeQueue; }
// Topics list has optional fields (ethCompatible, redundancy, solidified) that
// not all items have. ConfigBeanFactory requires all bean fields to exist in config.
// Excluded from auto-binding, read manually in fromConfig().
@Getter(lombok.AccessLevel.NONE)
@Setter(lombok.AccessLevel.NONE)
private List<TopicConfig> topics = new ArrayList<>();

public List<TopicConfig> getTopics() { return topics; }
private FilterConfig filter = new FilterConfig();

@Getter
@Setter
public static class NativeConfig {
private boolean useNativeQueue = true;
private int bindport = 5555;
private int sendqueuelength = 1000;
}

@Getter
@Setter
public static class TopicConfig {
private String triggerName = "";
private boolean enable = false;
private String topic = "";
private boolean solidified = false;
private boolean ethCompatible = false;
private boolean redundancy = false;
}

@Getter
@Setter
public static class FilterConfig {
private String fromblock = "";
private String toblock = "";
private List<String> contractAddress = new ArrayList<>();
private List<String> contractTopic = new ArrayList<>();
}

// Defaults come from reference.conf (loaded globally via Configuration.java)

/**
* Create EventConfig from the "event.subscribe" section of the application config.
*
* <p>Note: HOCON key "native" is a Java reserved word, so the bean field is named
* "nativeQueue" but config key is "native". We handle this manually after binding.
*/
public static EventConfig fromConfig(Config config) {
Config section = config.getConfig("event.subscribe");

// "native" is a Java reserved word, "topics" has optional fields per item —
// strip both before binding, read manually
String nativeKey = "native";
String topicsKey = "topics";
Config bindable = section.withoutPath(nativeKey).withoutPath(topicsKey)
.withoutPath("topicDefaults");
EventConfig ec = ConfigBeanFactory.create(bindable, EventConfig.class);

// manually bind "native" sub-section
Config nativeSection = section.hasPath(nativeKey)
? section.getConfig(nativeKey) : ConfigFactory.empty();
ec.nativeQueue = new NativeConfig();
if (nativeSection.hasPath("useNativeQueue")) {
ec.nativeQueue.useNativeQueue = nativeSection.getBoolean("useNativeQueue");
}
if (nativeSection.hasPath("bindport")) {
ec.nativeQueue.bindport = nativeSection.getInt("bindport");
}
if (nativeSection.hasPath("sendqueuelength")) {
ec.nativeQueue.sendqueuelength = nativeSection.getInt("sendqueuelength");
}

// manually bind topics — each item may have optional fields
if (section.hasPath(topicsKey)) {
ec.topics = new ArrayList<>();
for (com.typesafe.config.ConfigObject obj : section.getObjectList(topicsKey)) {
Config tc = obj.toConfig();
TopicConfig topic = new TopicConfig();
if (tc.hasPath("triggerName")) {
topic.triggerName = tc.getString("triggerName");
}
if (tc.hasPath("enable")) {
topic.enable = tc.getBoolean("enable");
}
if (tc.hasPath("topic")) {
topic.topic = tc.getString("topic");
}
if (tc.hasPath("solidified")) {
topic.solidified = tc.getBoolean("solidified");
}
if (tc.hasPath("ethCompatible")) {
topic.ethCompatible = tc.getBoolean("ethCompatible");
}
if (tc.hasPath("redundancy")) {
topic.redundancy = tc.getBoolean("redundancy");
}
ec.topics.add(topic);
}
}

return ec;
}
}
Loading
Loading