From a80c95d29bf8c5850e0cf8776b49adef802ee937 Mon Sep 17 00:00:00 2001 From: obchain Date: Fri, 8 May 2026 13:40:20 +0530 Subject: [PATCH 1/4] feat(config): add GetNonDefaultValues helper --- .../ConfigProviderTests.cs | 59 +++++++++++ .../Nethermind.Config/ConfigExtensions.cs | 97 +++++++++++++++++++ 2 files changed, 156 insertions(+) diff --git a/src/Nethermind/Nethermind.Config.Test/ConfigProviderTests.cs b/src/Nethermind/Nethermind.Config.Test/ConfigProviderTests.cs index b335c98383e..3bbda93eea5 100644 --- a/src/Nethermind/Nethermind.Config.Test/ConfigProviderTests.cs +++ b/src/Nethermind/Nethermind.Config.Test/ConfigProviderTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using FluentAssertions; using Nethermind.Core.Extensions; using Nethermind.JsonRpc; @@ -102,4 +103,62 @@ public void Can_useExistingConfig() configProvider.GetConfig().MinGasPrice.Should().Be(12345); } } + + [TestFixture] + [Parallelizable(ParallelScope.None)] + public class GetNonDefaultValuesTests + { + [Test] + public void Returns_empty_when_no_overrides() + { + ConfigProvider configProvider = new(); + configProvider.Initialize(); + + List<(string Category, string Name, object? CurrentValue, object? DefaultValue)> nonDefaults = + configProvider.GetNonDefaultValues().ToList(); + + Assert.That(nonDefaults, Is.Empty); + } + + [Test] + public void Returns_overridden_value_with_current_and_default() + { + Dictionary args = new() + { + { "Network.DiscoveryPort", "12345" } + }; + ConfigProvider configProvider = new(); + configProvider.AddSource(new ArgsConfigSource(args)); + configProvider.Initialize(); + + List<(string Category, string Name, object? CurrentValue, object? DefaultValue)> nonDefaults = + configProvider.GetNonDefaultValues() + .Where(static x => x.Category == "Network" && x.Name == nameof(INetworkConfig.DiscoveryPort)) + .ToList(); + + Assert.That(nonDefaults, Has.Count.EqualTo(1)); + Assert.That(nonDefaults[0].CurrentValue, Is.EqualTo(12345)); + Assert.That(nonDefaults[0].DefaultValue, Is.EqualTo(30303)); + } + + [Test] + public void Surfaces_overrides_across_categories() + { + Dictionary args = new() + { + { "Network.DiscoveryPort", "12345" }, + { "JsonRpc.Enabled", "true" } + }; + ConfigProvider configProvider = new(); + configProvider.AddSource(new ArgsConfigSource(args)); + configProvider.Initialize(); + + HashSet keys = configProvider.GetNonDefaultValues() + .Select(static x => $"{x.Category}.{x.Name}") + .ToHashSet(); + + Assert.That(keys, Does.Contain("Network.DiscoveryPort")); + Assert.That(keys, Does.Contain("JsonRpc.Enabled")); + } + } } diff --git a/src/Nethermind/Nethermind.Config/ConfigExtensions.cs b/src/Nethermind/Nethermind.Config/ConfigExtensions.cs index 31e60cea642..d8aba580a40 100644 --- a/src/Nethermind/Nethermind.Config/ConfigExtensions.cs +++ b/src/Nethermind/Nethermind.Config/ConfigExtensions.cs @@ -2,10 +2,14 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; +using System.Collections; using System.Collections.Concurrent; +using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Reflection; +using Nethermind.Core; +using Nethermind.Core.Extensions; namespace Nethermind.Config; @@ -41,4 +45,97 @@ public static T GetDefaultValue(this IConfig config, string propertyName) string? defaultValue = attribute.DefaultValue; return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(defaultValue!)!; } + + /// + /// Enumerates configuration properties whose current value differs from the + /// implementation's default. Useful for surfacing on startup which knobs the + /// operator has actually changed, rather than dumping every value. + /// + /// + /// The default is taken from a freshly constructed instance of the + /// implementing type, so initializers and constructors are honoured exactly + /// as production wiring would do (no parsing of the string). + /// + /// The provider to query. + /// + /// Tuples of (category, propertyName, currentValue, defaultValue) for + /// every property whose current value is not equal to its default. + /// + public static IEnumerable<(string Category, string Name, object? CurrentValue, object? DefaultValue)> + GetNonDefaultValues(this IConfigProvider configProvider) + { + ArgumentNullException.ThrowIfNull(configProvider); + + foreach (Type configInterface in TypeDiscovery + .FindNethermindBasedTypes(typeof(IConfig)) + .Where(static t => t.IsInterface)) + { + Type? implementation = configInterface.GetDirectInterfaceImplementation(); + if (implementation is null) continue; + + IConfig current; + try + { + current = configProvider.GetConfig(configInterface); + } + catch (ArgumentException) + { + continue; + } + + IConfig fresh; + try + { + fresh = (IConfig)Activator.CreateInstance(implementation)!; + } + catch (MissingMethodException) + { + continue; + } + + string category = GetCategoryName(configInterface) ?? string.Empty; + + foreach (PropertyInfo property in configInterface.GetProperties()) + { + if (!property.CanRead) continue; + + object? actual = property.GetValue(current); + object? defaultValue = property.GetValue(fresh); + + if (!ValuesEqual(actual, defaultValue)) + { + yield return (category, property.Name, actual, defaultValue); + } + } + } + } + + private static bool ValuesEqual(object? a, object? b) + { + if (a is null) return b is null; + if (b is null) return false; + if (a is string || b is string) return a.Equals(b); + if (a is IEnumerable enumerableA && b is IEnumerable enumerableB) + { + IEnumerator iteratorA = enumerableA.GetEnumerator(); + IEnumerator iteratorB = enumerableB.GetEnumerator(); + try + { + while (true) + { + bool hasA = iteratorA.MoveNext(); + bool hasB = iteratorB.MoveNext(); + if (hasA != hasB) return false; + if (!hasA) return true; + if (!ValuesEqual(iteratorA.Current, iteratorB.Current)) return false; + } + } + finally + { + (iteratorA as IDisposable)?.Dispose(); + (iteratorB as IDisposable)?.Dispose(); + } + } + return a.Equals(b); + } } From ae4df8836aa90b8358c40b6d89ca23787cacf4fa Mon Sep 17 00:00:00 2001 From: obchain Date: Fri, 8 May 2026 13:44:32 +0530 Subject: [PATCH 2/4] feat(runner): log non-default config on startup --- src/Nethermind/Nethermind.Runner/Program.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Nethermind/Nethermind.Runner/Program.cs b/src/Nethermind/Nethermind.Runner/Program.cs index ffcea45adad..ad00fcf1e6c 100644 --- a/src/Nethermind/Nethermind.Runner/Program.cs +++ b/src/Nethermind/Nethermind.Runner/Program.cs @@ -186,6 +186,24 @@ async Task RunAsync(ParseResult parseResult, PluginLoader pluginLoader, Can EthereumJsonSerializer serializer = new(); + if (logger.IsInfo) + { + List nonDefaultLines = []; + foreach ((string category, string name, object? currentValue, object? _) in configProvider.GetNonDefaultValues()) + { + nonDefaultLines.Add($" {category}.{name} = {serializer.Serialize(currentValue)}"); + } + + if (nonDefaultLines.Count == 0) + { + logger.Info("Configuration: all values at defaults."); + } + else + { + logger.Info($"Configuration: {nonDefaultLines.Count} non-default value(s):\n{string.Join('\n', nonDefaultLines)}"); + } + } + if (logger.IsDebug) { logger.Debug($"Nethermind configuration:\n{serializer.Serialize(initConfig, true)}"); From 05d521070a1e4dcb4dd61c32c7b070d26b44d456 Mon Sep 17 00:00:00 2001 From: "lukasz.rozmej" Date: Sun, 10 May 2026 08:43:12 +0200 Subject: [PATCH 3/4] fix(config): skip secrets, broaden errors, off-load non-default scan - Add IsSensitive flag to ConfigItemAttribute and mark Seq.ApiKey, KeyStore.Passwords, KeyStore.TestNodeKey, EthStats.Secret so their values never reach startup logs via GetNonDefaultValues. - Catch TargetInvocationException, TypeLoadException, and MethodAccessException alongside MissingMethodException so a single reflection-construction failure cannot crash startup. - Replace LINQ Where filter with an early-continue foreach per repo coding style. - Skip the redundant GetDirectInterfaceImplementation TypeDiscovery rescan; reuse the populated instance's concrete type for the fresh default. - Run the non-default scan via Task.Run so the reflection cost no longer blocks the rest of startup; warn (not crash) on failure. - Add a regression test covering sensitive-property suppression. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ConfigProviderTests.cs | 23 +++++++++++++ .../Nethermind.Config/ConfigExtensions.cs | 24 ++++++++----- .../Nethermind.Config/ConfigItemAttribute.cs | 6 ++++ .../Nethermind.EthStats/IEthStatsConfig.cs | 2 +- .../Config/IKeystoreConfig.cs | 4 +-- src/Nethermind/Nethermind.Runner/Program.cs | 34 ++++++++++++------- .../Nethermind.Seq/Config/ISeqConfig.cs | 2 +- 7 files changed, 71 insertions(+), 24 deletions(-) diff --git a/src/Nethermind/Nethermind.Config.Test/ConfigProviderTests.cs b/src/Nethermind/Nethermind.Config.Test/ConfigProviderTests.cs index 3bbda93eea5..44feda86aff 100644 --- a/src/Nethermind/Nethermind.Config.Test/ConfigProviderTests.cs +++ b/src/Nethermind/Nethermind.Config.Test/ConfigProviderTests.cs @@ -8,6 +8,7 @@ using FluentAssertions; using Nethermind.Core.Extensions; using Nethermind.JsonRpc; +using Nethermind.KeyStore.Config; using Nethermind.Network.Config; using NUnit.Framework; @@ -160,5 +161,27 @@ public void Surfaces_overrides_across_categories() Assert.That(keys, Does.Contain("Network.DiscoveryPort")); Assert.That(keys, Does.Contain("JsonRpc.Enabled")); } + + [Test] + public void Skips_sensitive_properties() + { + Dictionary args = new() + { + { "KeyStore.TestNodeKey", "0xdeadbeef" }, + { "KeyStore.Passwords", "[\"hunter2\"]" }, + { "KeyStore.KeyStoreDirectory", "custom-keystore" } + }; + ConfigProvider configProvider = new(); + configProvider.AddSource(new ArgsConfigSource(args)); + configProvider.Initialize(); + + HashSet keys = configProvider.GetNonDefaultValues() + .Select(static x => $"{x.Category}.{x.Name}") + .ToHashSet(); + + Assert.That(keys, Does.Not.Contain($"KeyStore.{nameof(IKeyStoreConfig.TestNodeKey)}")); + Assert.That(keys, Does.Not.Contain($"KeyStore.{nameof(IKeyStoreConfig.Passwords)}")); + Assert.That(keys, Does.Contain($"KeyStore.{nameof(IKeyStoreConfig.KeyStoreDirectory)}")); + } } } diff --git a/src/Nethermind/Nethermind.Config/ConfigExtensions.cs b/src/Nethermind/Nethermind.Config/ConfigExtensions.cs index d8aba580a40..7029af0bca3 100644 --- a/src/Nethermind/Nethermind.Config/ConfigExtensions.cs +++ b/src/Nethermind/Nethermind.Config/ConfigExtensions.cs @@ -52,26 +52,30 @@ public static T GetDefaultValue(this IConfig config, string propertyName) /// operator has actually changed, rather than dumping every value. /// /// + /// /// The default is taken from a freshly constructed instance of the /// implementing type, so initializers and constructors are honoured exactly /// as production wiring would do (no parsing of the string). + /// + /// + /// Properties marked with + /// (passwords, API keys, private keys, ...) are skipped to avoid leaking + /// secrets into logs. + /// /// /// The provider to query. /// /// Tuples of (category, propertyName, currentValue, defaultValue) for - /// every property whose current value is not equal to its default. + /// every non-sensitive property whose current value is not equal to its default. /// public static IEnumerable<(string Category, string Name, object? CurrentValue, object? DefaultValue)> GetNonDefaultValues(this IConfigProvider configProvider) { ArgumentNullException.ThrowIfNull(configProvider); - foreach (Type configInterface in TypeDiscovery - .FindNethermindBasedTypes(typeof(IConfig)) - .Where(static t => t.IsInterface)) + foreach (Type configInterface in TypeDiscovery.FindNethermindBasedTypes(typeof(IConfig))) { - Type? implementation = configInterface.GetDirectInterfaceImplementation(); - if (implementation is null) continue; + if (!configInterface.IsInterface) continue; IConfig current; try @@ -86,9 +90,12 @@ public static T GetDefaultValue(this IConfig config, string propertyName) IConfig fresh; try { - fresh = (IConfig)Activator.CreateInstance(implementation)!; + fresh = (IConfig)Activator.CreateInstance(current.GetType())!; } - catch (MissingMethodException) + catch (Exception e) when (e is MissingMethodException + or TargetInvocationException + or TypeLoadException + or MethodAccessException) { continue; } @@ -98,6 +105,7 @@ public static T GetDefaultValue(this IConfig config, string propertyName) foreach (PropertyInfo property in configInterface.GetProperties()) { if (!property.CanRead) continue; + if (property.GetCustomAttribute()?.IsSensitive == true) continue; object? actual = property.GetValue(current); object? defaultValue = property.GetValue(fresh); diff --git a/src/Nethermind/Nethermind.Config/ConfigItemAttribute.cs b/src/Nethermind/Nethermind.Config/ConfigItemAttribute.cs index 19b2b9094b4..2eb325f16c4 100644 --- a/src/Nethermind/Nethermind.Config/ConfigItemAttribute.cs +++ b/src/Nethermind/Nethermind.Config/ConfigItemAttribute.cs @@ -21,4 +21,10 @@ public class ConfigItemAttribute : Attribute public bool IsPortOption { get; set; } public string CliOptionAlias { get; set; } = ""; + + /// + /// Marks the property as containing secrets (passwords, API keys, private keys, ...). + /// Such values must never be written to logs or other diagnostic surfaces. + /// + public bool IsSensitive { get; set; } } diff --git a/src/Nethermind/Nethermind.EthStats/IEthStatsConfig.cs b/src/Nethermind/Nethermind.EthStats/IEthStatsConfig.cs index 9007980c623..414bd639674 100644 --- a/src/Nethermind/Nethermind.EthStats/IEthStatsConfig.cs +++ b/src/Nethermind/Nethermind.EthStats/IEthStatsConfig.cs @@ -16,7 +16,7 @@ public interface IEthStatsConfig : IConfig [ConfigItem(Description = "The node name displayed on Ethstats.", DefaultValue = "Nethermind")] string? Name { get; } - [ConfigItem(Description = "The Ethstats secret.", DefaultValue = "secret")] + [ConfigItem(Description = "The Ethstats secret.", DefaultValue = "secret", IsSensitive = true)] string? Secret { get; } [ConfigItem(Description = "The node owner contact details displayed on Ethstats.", DefaultValue = "hello@nethermind.io")] diff --git a/src/Nethermind/Nethermind.KeyStore/Config/IKeystoreConfig.cs b/src/Nethermind/Nethermind.KeyStore/Config/IKeystoreConfig.cs index 1429c3e6f07..443af9f9899 100644 --- a/src/Nethermind/Nethermind.KeyStore/Config/IKeystoreConfig.cs +++ b/src/Nethermind/Nethermind.KeyStore/Config/IKeystoreConfig.cs @@ -50,7 +50,7 @@ public interface IKeyStoreConfig : IConfig [ConfigItem(Description = "See [Web3 secret storage definition][web3-secret-storage].", DefaultValue = "16")] int IVSize { get; set; } - [ConfigItem(Description = "A plaintext private key to use for testing purposes.")] + [ConfigItem(Description = "A plaintext private key to use for testing purposes.", IsSensitive = true)] string TestNodeKey { get; set; } [ConfigItem(Description = "An account to use as the block author (coinbase).")] @@ -62,7 +62,7 @@ public interface IKeyStoreConfig : IConfig [ConfigItem(Description = $"The path to the key file to use by for networking (enode). If neither this nor the `{nameof(EnodeAccount)}` is specified, the key is autogenerated in `node.key.plain` file.")] string EnodeKeyFile { get; set; } - [ConfigItem(Description = $"An array of passwords used to unlock the accounts set with `{nameof(UnlockAccounts)}`.", DefaultValue = "[]")] + [ConfigItem(Description = $"An array of passwords used to unlock the accounts set with `{nameof(UnlockAccounts)}`.", DefaultValue = "[]", IsSensitive = true)] string[] Passwords { get; set; } [ConfigItem(Description = $"An array of password files paths used to unlock the accounts set with `{nameof(UnlockAccounts)}`.", DefaultValue = "[]")] diff --git a/src/Nethermind/Nethermind.Runner/Program.cs b/src/Nethermind/Nethermind.Runner/Program.cs index ad00fcf1e6c..a364a1f578a 100644 --- a/src/Nethermind/Nethermind.Runner/Program.cs +++ b/src/Nethermind/Nethermind.Runner/Program.cs @@ -188,20 +188,30 @@ async Task RunAsync(ParseResult parseResult, PluginLoader pluginLoader, Can if (logger.IsInfo) { - List nonDefaultLines = []; - foreach ((string category, string name, object? currentValue, object? _) in configProvider.GetNonDefaultValues()) + _ = Task.Run(() => { - nonDefaultLines.Add($" {category}.{name} = {serializer.Serialize(currentValue)}"); - } + try + { + List nonDefaultLines = []; + foreach ((string category, string name, object? currentValue, object? _) in configProvider.GetNonDefaultValues()) + { + nonDefaultLines.Add($" {category}.{name} = {serializer.Serialize(currentValue)}"); + } - if (nonDefaultLines.Count == 0) - { - logger.Info("Configuration: all values at defaults."); - } - else - { - logger.Info($"Configuration: {nonDefaultLines.Count} non-default value(s):\n{string.Join('\n', nonDefaultLines)}"); - } + if (nonDefaultLines.Count == 0) + { + logger.Info("Configuration: all values at defaults."); + } + else + { + logger.Info($"Configuration: {nonDefaultLines.Count} non-default value(s):\n{string.Join('\n', nonDefaultLines)}"); + } + } + catch (Exception e) + { + if (logger.IsWarn) logger.Warn($"Failed to enumerate non-default config values: {e.Message}"); + } + }); } if (logger.IsDebug) diff --git a/src/Nethermind/Nethermind.Seq/Config/ISeqConfig.cs b/src/Nethermind/Nethermind.Seq/Config/ISeqConfig.cs index aaefd5d08d6..53152921fc5 100644 --- a/src/Nethermind/Nethermind.Seq/Config/ISeqConfig.cs +++ b/src/Nethermind/Nethermind.Seq/Config/ISeqConfig.cs @@ -14,6 +14,6 @@ public interface ISeqConfig : IConfig [ConfigItem(Description = "The Seq instance URL.", DefaultValue = "http://localhost:5341")] string ServerUrl { get; } - [ConfigItem(Description = "The Seq API key.", DefaultValue = "")] + [ConfigItem(Description = "The Seq API key.", DefaultValue = "", IsSensitive = true)] string ApiKey { get; } } From 4ff9cc95f4e1124b12ca63e83cca6c8a71b98598 Mon Sep 17 00:00:00 2001 From: "lukasz.rozmej" Date: Sun, 10 May 2026 09:43:59 +0200 Subject: [PATCH 4/4] refactor(config): inline scan, broad per-config catch, structural eq MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Run the non-default scan synchronously before the runner can mutate config (was a fire-and-forget Task.Run that raced ResolveDataDirectory, BlockTree.Initializer, plugin load, etc., serialised mutating graphs, and emitted resolved absolute paths as "non-default" overrides). - Make the helper resilient: a single failing config interface (provider lookup, ctor, or property getter) now skips that one interface only via an Action? callback the caller uses for logging. Previously a single throw would short-circuit the yield iterator and silently drop every later config. - Replace the hand-rolled IEnumerator-based ValuesEqual with StructuralComparisons.StructuralEqualityComparer.Equals. Configs only use scalars / strings / arrays — all handled correctly — and the recursion-cycle hazard goes away. - Mark KeyStore identity fields sensitive (UnlockAccounts, BlockAuthorAccount, EnodeAccount, EnodeKeyFile) so account addresses and key-file paths never reach startup logs. - Add a regression test where one IConfigProvider.GetConfig call throws: enumeration must continue and surface other categories' overrides while the callback receives the failing type. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ConfigProviderTests.cs | 31 +++++++ .../Nethermind.Config/ConfigExtensions.cs | 88 ++++++------------- .../Config/IKeystoreConfig.cs | 8 +- src/Nethermind/Nethermind.Runner/Program.cs | 39 ++++---- 4 files changed, 79 insertions(+), 87 deletions(-) diff --git a/src/Nethermind/Nethermind.Config.Test/ConfigProviderTests.cs b/src/Nethermind/Nethermind.Config.Test/ConfigProviderTests.cs index 44feda86aff..63ebf5f5fb7 100644 --- a/src/Nethermind/Nethermind.Config.Test/ConfigProviderTests.cs +++ b/src/Nethermind/Nethermind.Config.Test/ConfigProviderTests.cs @@ -183,5 +183,36 @@ public void Skips_sensitive_properties() Assert.That(keys, Does.Not.Contain($"KeyStore.{nameof(IKeyStoreConfig.Passwords)}")); Assert.That(keys, Does.Contain($"KeyStore.{nameof(IKeyStoreConfig.KeyStoreDirectory)}")); } + + [Test] + public void Failing_provider_for_one_type_does_not_poison_others() + { + Dictionary args = new() { { "Network.DiscoveryPort", "12345" } }; + ConfigProvider inner = new(); + inner.AddSource(new ArgsConfigSource(args)); + inner.Initialize(); + + FailingProvider failing = new(inner, typeof(IJsonRpcConfig)); + List<(Type ConfigType, Exception Error)> errors = []; + + HashSet keys = failing.GetNonDefaultValues((t, e) => errors.Add((t, e))) + .Select(static x => $"{x.Category}.{x.Name}") + .ToHashSet(); + + Assert.That(keys, Does.Contain("Network.DiscoveryPort")); + Assert.That(errors, Has.Some.Matches<(Type, Exception)>(static x => x.Item1 == typeof(IJsonRpcConfig))); + } + + private sealed class FailingProvider(IConfigProvider inner, Type failingType) : IConfigProvider + { + public T GetConfig() where T : IConfig => (T)GetConfig(typeof(T)); + + public IConfig GetConfig(Type configType) => + configType == failingType + ? throw new InvalidOperationException("simulated failure") + : inner.GetConfig(configType); + + public object? GetRawValue(string category, string name) => inner.GetRawValue(category, name); + } } } diff --git a/src/Nethermind/Nethermind.Config/ConfigExtensions.cs b/src/Nethermind/Nethermind.Config/ConfigExtensions.cs index 7029af0bca3..c6a025bd4ad 100644 --- a/src/Nethermind/Nethermind.Config/ConfigExtensions.cs +++ b/src/Nethermind/Nethermind.Config/ConfigExtensions.cs @@ -52,24 +52,19 @@ public static T GetDefaultValue(this IConfig config, string propertyName) /// operator has actually changed, rather than dumping every value. /// /// - /// - /// The default is taken from a freshly constructed instance of the - /// implementing type, so initializers and constructors are honoured exactly - /// as production wiring would do (no parsing of the string). - /// - /// - /// Properties marked with - /// (passwords, API keys, private keys, ...) are skipped to avoid leaking - /// secrets into logs. - /// + /// Defaults come from a freshly constructed instance of the implementing + /// type, so initializers and constructors are honoured exactly as production + /// wiring would do (no parsing of the string). + /// Properties flagged are skipped. /// /// The provider to query. - /// - /// Tuples of (category, propertyName, currentValue, defaultValue) for - /// every non-sensitive property whose current value is not equal to its default. - /// + /// + /// Optional callback invoked when a single config interface cannot be enumerated + /// (provider lookup or fresh-default construction throws). Enumeration of the + /// remaining interfaces continues regardless. If null, failures are silent. + /// public static IEnumerable<(string Category, string Name, object? CurrentValue, object? DefaultValue)> - GetNonDefaultValues(this IConfigProvider configProvider) + GetNonDefaultValues(this IConfigProvider configProvider, Action? onConfigError = null) { ArgumentNullException.ThrowIfNull(configProvider); @@ -78,72 +73,43 @@ public static T GetDefaultValue(this IConfig config, string propertyName) if (!configInterface.IsInterface) continue; IConfig current; - try - { - current = configProvider.GetConfig(configInterface); - } - catch (ArgumentException) - { - continue; - } - IConfig fresh; + string category; try { + current = configProvider.GetConfig(configInterface); fresh = (IConfig)Activator.CreateInstance(current.GetType())!; + category = GetCategoryName(configInterface) ?? string.Empty; } - catch (Exception e) when (e is MissingMethodException - or TargetInvocationException - or TypeLoadException - or MethodAccessException) + catch (Exception e) { + onConfigError?.Invoke(configInterface, e); continue; } - string category = GetCategoryName(configInterface) ?? string.Empty; - foreach (PropertyInfo property in configInterface.GetProperties()) { if (!property.CanRead) continue; if (property.GetCustomAttribute()?.IsSensitive == true) continue; - object? actual = property.GetValue(current); - object? defaultValue = property.GetValue(fresh); - - if (!ValuesEqual(actual, defaultValue)) + object? actual; + object? defaultValue; + try { - yield return (category, property.Name, actual, defaultValue); + actual = property.GetValue(current); + defaultValue = property.GetValue(fresh); + } + catch (Exception e) + { + onConfigError?.Invoke(configInterface, e); + continue; } - } - } - } - private static bool ValuesEqual(object? a, object? b) - { - if (a is null) return b is null; - if (b is null) return false; - if (a is string || b is string) return a.Equals(b); - if (a is IEnumerable enumerableA && b is IEnumerable enumerableB) - { - IEnumerator iteratorA = enumerableA.GetEnumerator(); - IEnumerator iteratorB = enumerableB.GetEnumerator(); - try - { - while (true) + if (!StructuralComparisons.StructuralEqualityComparer.Equals(actual, defaultValue)) { - bool hasA = iteratorA.MoveNext(); - bool hasB = iteratorB.MoveNext(); - if (hasA != hasB) return false; - if (!hasA) return true; - if (!ValuesEqual(iteratorA.Current, iteratorB.Current)) return false; + yield return (category, property.Name, actual, defaultValue); } } - finally - { - (iteratorA as IDisposable)?.Dispose(); - (iteratorB as IDisposable)?.Dispose(); - } } - return a.Equals(b); } } diff --git a/src/Nethermind/Nethermind.KeyStore/Config/IKeystoreConfig.cs b/src/Nethermind/Nethermind.KeyStore/Config/IKeystoreConfig.cs index 443af9f9899..9ffe042f54d 100644 --- a/src/Nethermind/Nethermind.KeyStore/Config/IKeystoreConfig.cs +++ b/src/Nethermind/Nethermind.KeyStore/Config/IKeystoreConfig.cs @@ -53,13 +53,13 @@ public interface IKeyStoreConfig : IConfig [ConfigItem(Description = "A plaintext private key to use for testing purposes.", IsSensitive = true)] string TestNodeKey { get; set; } - [ConfigItem(Description = "An account to use as the block author (coinbase).")] + [ConfigItem(Description = "An account to use as the block author (coinbase).", IsSensitive = true)] string BlockAuthorAccount { get; set; } - [ConfigItem(Description = $"An account to use for networking (enode). If neither this nor the `{nameof(EnodeKeyFile)}` option is specified, the key is autogenerated in `node.key.plain` file.")] + [ConfigItem(Description = $"An account to use for networking (enode). If neither this nor the `{nameof(EnodeKeyFile)}` option is specified, the key is autogenerated in `node.key.plain` file.", IsSensitive = true)] string EnodeAccount { get; set; } - [ConfigItem(Description = $"The path to the key file to use by for networking (enode). If neither this nor the `{nameof(EnodeAccount)}` is specified, the key is autogenerated in `node.key.plain` file.")] + [ConfigItem(Description = $"The path to the key file to use by for networking (enode). If neither this nor the `{nameof(EnodeAccount)}` is specified, the key is autogenerated in `node.key.plain` file.", IsSensitive = true)] string EnodeKeyFile { get; set; } [ConfigItem(Description = $"An array of passwords used to unlock the accounts set with `{nameof(UnlockAccounts)}`.", DefaultValue = "[]", IsSensitive = true)] @@ -68,7 +68,7 @@ public interface IKeyStoreConfig : IConfig [ConfigItem(Description = $"An array of password files paths used to unlock the accounts set with `{nameof(UnlockAccounts)}`.", DefaultValue = "[]")] string[] PasswordFiles { get; set; } - [ConfigItem(Description = $"An array of accounts to unlock on startup using passwords either in `{nameof(PasswordFiles)}` and `{nameof(Passwords)}`.", DefaultValue = "[]")] + [ConfigItem(Description = $"An array of accounts to unlock on startup using passwords either in `{nameof(PasswordFiles)}` and `{nameof(Passwords)}`.", DefaultValue = "[]", IsSensitive = true)] string[] UnlockAccounts { get; set; } } diff --git a/src/Nethermind/Nethermind.Runner/Program.cs b/src/Nethermind/Nethermind.Runner/Program.cs index a364a1f578a..183114a7dfa 100644 --- a/src/Nethermind/Nethermind.Runner/Program.cs +++ b/src/Nethermind/Nethermind.Runner/Program.cs @@ -188,30 +188,25 @@ async Task RunAsync(ParseResult parseResult, PluginLoader pluginLoader, Can if (logger.IsInfo) { - _ = Task.Run(() => + List nonDefaultLines = []; + Action onConfigError = (configType, error) => { - try - { - List nonDefaultLines = []; - foreach ((string category, string name, object? currentValue, object? _) in configProvider.GetNonDefaultValues()) - { - nonDefaultLines.Add($" {category}.{name} = {serializer.Serialize(currentValue)}"); - } + if (logger.IsWarn) logger.Warn($"Skipped {configType.Name} in non-default config diff: {error.Message}"); + }; - if (nonDefaultLines.Count == 0) - { - logger.Info("Configuration: all values at defaults."); - } - else - { - logger.Info($"Configuration: {nonDefaultLines.Count} non-default value(s):\n{string.Join('\n', nonDefaultLines)}"); - } - } - catch (Exception e) - { - if (logger.IsWarn) logger.Warn($"Failed to enumerate non-default config values: {e.Message}"); - } - }); + foreach ((string category, string name, object? currentValue, object? _) in configProvider.GetNonDefaultValues(onConfigError)) + { + nonDefaultLines.Add($" {category}.{name} = {serializer.Serialize(currentValue)}"); + } + + if (nonDefaultLines.Count == 0) + { + logger.Info("Configuration: all values at defaults."); + } + else + { + logger.Info($"Configuration: {nonDefaultLines.Count} non-default value(s):\n{string.Join('\n', nonDefaultLines)}"); + } } if (logger.IsDebug)