-
Notifications
You must be signed in to change notification settings - Fork 1.6k
refactor(config): replace manual parsing with ConfigBeanFactory binding #6615
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
dd349f3
d249675
1323c54
4d8d9b9
651e019
c38f388
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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); | ||
| } | ||
| } | ||
| } |
| 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; | ||
| private long memoFee = 0; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Behavioral regression] The original // 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,
Suggested fix in 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great catch — fixed in c38f388. Both clamps restored in 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 Thanks for the rigorous review — the original |
||
| 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) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggest to replace class-level The previous approach used class-level annotations but silently suppressed them on 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:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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."); | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should throws
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| } | ||
| } | ||
| 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Behavioral regression]
allowNewRewardclamping is lost — and this changes the cross-field check semantics below.The original
Args.javaclampedallowNewRewardto[0, 1]:This is especially critical because the cross-field validation in
postProcess()(line 152) usesallowNewReward != 1:Without clamping, the semantics diverge from develop:
allowNewReward = 2+allowOldRewardOpt = 1allowNewReward = 99allowNewReward = -5A 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:Verified by a regression test against this PR branch — the cross-field semantic test fails without this clamp.
There was a problem hiding this comment.
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.