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 @@ -14,7 +14,6 @@
import org.tron.common.logsfilter.FilterQuery;
import org.tron.common.setting.RocksDbSettings;
import org.tron.core.Constant;
import org.tron.core.config.args.Overlay;
import org.tron.core.config.args.SeedNode;
import org.tron.core.config.args.Storage;
import org.tron.p2p.P2pConfig;
Expand Down Expand Up @@ -445,8 +444,6 @@ public class CommonParameter {
@Getter
public Storage storage;
@Getter
public Overlay overlay;
@Getter
public SeedNode seedNode;
@Getter
public EventPluginConfig eventPluginConfig;
Expand Down
145 changes: 145 additions & 0 deletions common/src/main/java/org/tron/core/config/BeanDefaults.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package org.tron.core.config;

import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigObject;
import com.typesafe.config.ConfigValue;
import com.typesafe.config.ConfigValueType;
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
* Generates a Typesafe {@link Config} from a bean instance's current field values.
*
* <p>Used by each {@code XxxConfig.fromConfig()} to replace the role that
* {@code reference.conf} played: ensures every key ConfigBeanFactory needs is
* present, so a partial user config works without throwing
* {@code ConfigException.Missing}.
*
* <p>Only public getter+setter pairs (standard JavaBean properties) are included —
* the same set that {@code ConfigBeanFactory.create()} auto-binds. Keys are
* decapitalized exactly as ConfigBeanFactory does:
* {@code Character.toLowerCase(name.charAt(0)) + name.substring(1)}.
*
* <p>Nested bean fields are recursed into nested HOCON objects.
* {@code List} fields are serialized as HOCON lists (empty by default).
* Fields with no public setter (e.g. {@code @Getter(AccessLevel.NONE)} overrides)
* are automatically skipped — these are handled manually in each
* {@code fromConfig()} via {@code hasPath} guards.
*/
public final class BeanDefaults {

private BeanDefaults() {}

/**
* Convert {@code bean}'s public JavaBean properties to a Typesafe Config.
* The resulting Config can be used as a {@code withFallback()} for a user's
* config section to guarantee all keys are present for ConfigBeanFactory.
*/
public static Config toConfig(Object bean) {
return ConfigFactory.parseMap(toMap(bean));
}

/**
* Returns a copy of {@code config} with all null-valued leaf paths removed.
* Call this on a user-supplied config section before {@link Config#withFallback}
* so that HOCON {@code null} entries in legacy configs do not shadow bean defaults.
*
* <p>Uses {@link ConfigObject#entrySet()} (not {@link Config#entrySet()}) because
* the latter silently excludes null values, making them impossible to detect.
*/
public static Config stripNullLeaves(Config config) {
return stripNullObject(config.root()).toConfig();
}

/**
* Returns a copy of {@code config} where the value at {@code fromKey} is moved to
* {@code toKey}, leaving the original key absent. If {@code fromKey} is absent, the
* config is returned unchanged. Use this in {@code fromConfig()} to bridge config keys
* that violate JavaBean naming (e.g. {@code pBFTExpireNum} → {@code PBFTExpireNum}) so
* that {@code ConfigBeanFactory} finds the value under the key it derives from the setter.
*/
public static Config remapKey(Config config, String fromKey, String toKey) {
if (!config.hasPath(fromKey)) {
return config;
}
return config.withValue(toKey, config.getValue(fromKey)).withoutPath(fromKey);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: remapKey should not overwrite an already-present canonical key; legacy key remapping currently changes user precedence when both keys are set.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At common/src/main/java/org/tron/core/config/BeanDefaults.java, line 72:

<comment>remapKey should not overwrite an already-present canonical key; legacy key remapping currently changes user precedence when both keys are set.</comment>

<file context>
@@ -43,6 +46,45 @@ public static Config toConfig(Object bean) {
+    if (!config.hasPath(fromKey)) {
+      return config;
+    }
+    return config.withValue(toKey, config.getValue(fromKey)).withoutPath(fromKey);
+  }
+
</file context>

Tip: Review your code locally with the cubic CLI to iterate faster.

}

private static ConfigObject stripNullObject(ConfigObject obj) {
ConfigObject result = obj;
for (Map.Entry<String, ConfigValue> entry : obj.entrySet()) {
ConfigValue v = entry.getValue();
if (v.valueType() == ConfigValueType.NULL) {
result = result.withoutKey(entry.getKey());
} else if (v.valueType() == ConfigValueType.OBJECT) {
result = result.withValue(entry.getKey(), stripNullObject((ConfigObject) v));
}
}
return result;
}

private static Map<String, Object> toMap(Object bean) {
Map<String, Object> map = new LinkedHashMap<>();
BeanInfo info;
try {
info = Introspector.getBeanInfo(bean.getClass());
} catch (java.beans.IntrospectionException e) {
// Programming error: bean class does not conform to JavaBean spec.
// Propagate immediately so the misconfigured class is identified at startup,
// rather than returning a silent empty map that produces a confusing
// ConfigException.Missing pointing at the user config.
throw new IllegalStateException("Cannot introspect bean: " + bean.getClass().getName(), e);
}
for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
Method getter = pd.getReadMethod();
Method setter = pd.getWriteMethod();
// Skip read-only properties (no setter) — matches ConfigBeanFactory's contract
if (getter == null || setter == null) {
continue;
}
// Use the property name exactly as Introspector produced it.
// ConfigBeanFactory does configProps.get(beanProp.getName()) — the lookup key
// is the property name verbatim, not decapitalized. For ordinary camelCase
// setters (setMaxConnections → "MaxConnections" → decapitalize → "maxConnections")
// Introspector already returns the lowercase form. For setters that start with
// two consecutive uppercase letters (setPBFTEnable → "PBFTEnable") the JavaBean
// spec forbids decapitalization, so pd.getName() == "PBFTEnable" — matching the
// capital-P key that config.conf uses for those fields.
try {
String key = pd.getName();
Object value = getter.invoke(bean);
map.put(key, toValue(value));
} catch (Exception ignored) {
// Best-effort: skip individual unresolvable property so that the rest of
// the defaults are still emitted. getter.invoke() is the only realistic
// throw site (InvocationTargetException / IllegalAccessException).
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return map;
}

private static Object toValue(Object value) {
if (value == null) {
return "";
}
if (value instanceof Boolean || value instanceof Number || value instanceof String) {
return value;
}
if (value instanceof List) {
List<Object> list = new ArrayList<>();
for (Object item : (List<?>) value) {
list.add(toValue(item));
}
return list;
}
// Assume nested bean — recurse so it becomes a nested HOCON object.
return toMap(value);
}
}
5 changes: 3 additions & 2 deletions common/src/main/java/org/tron/core/config/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,11 @@ public static com.typesafe.config.Config getByFileName(

private static void resolveConfigFile(String fileName, File confFile) {
if (confFile.exists()) {
config = ConfigFactory.parseFile(confFile)
.withFallback(ConfigFactory.defaultReference());
config = ConfigFactory.parseFile(confFile);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: parseFile(confFile) drops the default reference.conf fallback, so file-based config loading can lose required defaults and diverge from classpath loading behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At common/src/main/java/org/tron/core/config/Configuration.java, line 51:

<comment>`parseFile(confFile)` drops the default `reference.conf` fallback, so file-based config loading can lose required defaults and diverge from classpath loading behavior.</comment>

<file context>
@@ -48,10 +48,11 @@ public static com.typesafe.config.Config getByFileName(
     if (confFile.exists()) {
-      config = ConfigFactory.parseFile(confFile)
-          .withFallback(ConfigFactory.defaultReference());
+      config = ConfigFactory.parseFile(confFile);
     } else if (Thread.currentThread().getContextClassLoader().getResourceAsStream(fileName)
         != null) {
</file context>
Suggested change
config = ConfigFactory.parseFile(confFile);
config = ConfigFactory.parseFile(confFile)
.withFallback(ConfigFactory.defaultReference());

} else if (Thread.currentThread().getContextClassLoader().getResourceAsStream(fileName)
!= null) {
// ConfigFactory.load merges system properties (higher priority than the file),
// which tests rely on to override storage.db.engine via -D flags.
config = ConfigFactory.load(fileName);
} else {
throw new IllegalArgumentException(
Expand Down
14 changes: 9 additions & 5 deletions common/src/main/java/org/tron/core/config/args/BlockConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@

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

/**
Expand All @@ -21,12 +23,10 @@
public class BlockConfig {

private boolean needSyncCheck = false;
private long maintenanceTimeInterval = 21600000L;
private long maintenanceTimeInterval = 6 * 3600 * 1000L; // 6 hours
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).
Expand All @@ -38,8 +38,12 @@ public static BlockConfig fromConfig(Config config) {
+ "config.conf, please set the value in block.proposalExpireTime.", PARAMETER_INIT);
}

Config blockSection = config.getConfig("block");
BlockConfig blockConfig = ConfigBeanFactory.create(blockSection, BlockConfig.class);
Config defaults = BeanDefaults.toConfig(new BlockConfig());
Config userSection = config.hasPath("block")
? BeanDefaults.stripNullLeaves(config.getConfig("block"))
: ConfigFactory.empty();
Config section = userSection.withFallback(defaults);
BlockConfig blockConfig = ConfigBeanFactory.create(section, BlockConfig.class);
blockConfig.postProcess();
return blockConfig;
}
Expand Down
51 changes: 11 additions & 40 deletions common/src/main/java/org/tron/core/config/args/CommitteeConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.typesafe.config.Config;
import com.typesafe.config.ConfigBeanFactory;
import com.typesafe.config.ConfigFactory;
import org.tron.core.config.BeanDefaults;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -35,29 +37,8 @@ public class CommitteeConfig {
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;

// Only getters are exposed. No public setters — ConfigBeanFactory scans public
// setters via reflection and would derive key "PBFTExpireNum" / "AllowPBFT"
// (JavaBean uppercase rule), which does not match config keys "pBFTExpireNum"
// / "allowPBFT" and would throw. Values are assigned to fields directly in
// fromConfig() below.
public long getAllowPBFT() { return allowPBFT; }
public long getPBFTExpireNum() { return pBFTExpireNum; }
private long allowTvmFreeze = 0;
private long allowTvmVote = 0;
private long allowTvmLondon = 0;
Expand Down Expand Up @@ -86,27 +67,17 @@ public class CommitteeConfig {

// 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) {
Config section = config.getConfig("committee");

Config defaults = BeanDefaults.toConfig(new CommitteeConfig());
Config userSection = config.hasPath("committee")
? BeanDefaults.stripNullLeaves(config.getConfig("committee"))
: ConfigFactory.empty();
// pBFTExpireNum: config key uses lowercase-p prefix, but setPBFTExpireNum causes
// Introspector to derive "PBFTExpireNum" (consecutive uppercase prevents decapitalization).
// Remap so ConfigBeanFactory finds it under the expected key.
userSection = BeanDefaults.remapKey(userSection, "pBFTExpireNum", "PBFTExpireNum");
Config section = userSection.withFallback(defaults);
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;
}
Expand Down
Loading