diff --git a/core/chains/evm/config/toml/defaults.go b/core/chains/evm/config/toml/defaults.go index c3f087da8c5..4e1ebfb34df 100644 --- a/core/chains/evm/config/toml/defaults.go +++ b/core/chains/evm/config/toml/defaults.go @@ -3,7 +3,10 @@ package toml import ( "bytes" "embed" + "fmt" + "io" "log" + "os" "path/filepath" "slices" "strings" @@ -12,37 +15,43 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/chains/evm/config/chaintype" "github.com/smartcontractkit/chainlink/v2/core/chains/evm/utils/big" + "github.com/smartcontractkit/chainlink/v2/core/config/env" ) var ( + //go:embed defaults/*.toml defaultsFS embed.FS fallback Chain defaults = map[string]Chain{} defaultNames = map[string]string{} + customDefaults = map[string]Chain{} + // DefaultIDs is the set of chain ids which have defaults. DefaultIDs []*big.Big ) func init() { + // read the defaults first + fes, err := defaultsFS.ReadDir("defaults") if err != nil { log.Fatalf("failed to read defaults/: %v", err) } for _, fe := range fes { path := filepath.Join("defaults", fe.Name()) - b, err := defaultsFS.ReadFile(path) - if err != nil { - log.Fatalf("failed to read %q: %v", path, err) + b, err2 := defaultsFS.ReadFile(path) + if err2 != nil { + log.Fatalf("failed to read %q: %v", path, err2) } var config = struct { ChainID *big.Big Chain }{} - if err := cconfig.DecodeTOML(bytes.NewReader(b), &config); err != nil { - log.Fatalf("failed to decode %q: %v", path, err) + if err3 := cconfig.DecodeTOML(bytes.NewReader(b), &config); err3 != nil { + log.Fatalf("failed to decode %q: %v", path, err3) } if fe.Name() == "fallback.toml" { if config.ChainID != nil { @@ -65,6 +74,63 @@ func init() { slices.SortFunc(DefaultIDs, func(a, b *big.Big) int { return a.Cmp(b) }) + + // read the custom defaults overrides + dir := env.CustomDefaults.Get() + if dir == "" { + // short-circuit; no default overrides provided + return + } + + // use evm overrides specifically + evmDir := fmt.Sprintf("%s/evm", dir) + + // Read directory contents for evm only + entries, err := os.ReadDir(evmDir) + if err != nil { + log.Fatalf("error reading evm custom defaults override directory: %v", err) + return + } + + for _, entry := range entries { + if entry.IsDir() { + // Skip directories + continue + } + + path := evmDir + "/" + entry.Name() + file, err := os.Open(path) + if err != nil { + log.Fatalf("error opening file (name: %v) in custom defaults override directory: %v", entry.Name(), err) + } + + // Read file contents + b, err := io.ReadAll(file) + file.Close() + if err != nil { + log.Fatalf("error reading file (name: %v) contents in custom defaults override directory: %v", entry.Name(), err) + } + + var config = struct { + ChainID *big.Big + Chain + }{} + + if err := cconfig.DecodeTOML(bytes.NewReader(b), &config); err != nil { + log.Fatalf("failed to decode %q in custom defaults override directory: %v", path, err) + } + + if config.ChainID == nil { + log.Fatalf("missing ChainID in: %s in custom defaults override directory. exiting", path) + } + + id := config.ChainID.String() + + if _, ok := customDefaults[id]; ok { + log.Fatalf("%q contains duplicate ChainID: %s", path, id) + } + customDefaults[id] = config.Chain + } } // DefaultsNamed returns the default Chain values, optionally for the given chainID, as well as a name if the chainID is known. @@ -78,6 +144,9 @@ func DefaultsNamed(chainID *big.Big) (c Chain, name string) { c.SetFrom(&d) name = defaultNames[s] } + if overrides, ok := customDefaults[s]; ok { + c.SetFrom(&overrides) + } return } diff --git a/core/config/env/env.go b/core/config/env/env.go index 0ebfc357bf3..c34cd7f4f5e 100644 --- a/core/config/env/env.go +++ b/core/config/env/env.go @@ -20,6 +20,7 @@ var ( ThresholdKeyShare = Secret("CL_THRESHOLD_KEY_SHARE") // Migrations env vars EVMChainIDNotNullMigration0195 = "CL_EVM_CHAINID_NOT_NULL_MIGRATION_0195" + CustomDefaults = Var("CL_CHAIN_DEFAULTS") ) // LOOPP commands and vars diff --git a/main_test.go b/main_test.go index 81e056e3b84..419a4af0517 100644 --- a/main_test.go +++ b/main_test.go @@ -42,6 +42,10 @@ func TestMain(m *testing.M) { })) } +// TestScripts walks through the testdata/scripts directory and runs all tests that end in +// .txt or .txtar with the testscripts library. To run an individual test, specify it in the +// -run param of go test without the txtar or txt suffix, like so: +// go test . -run TestScripts/node/validate/default func TestScripts(t *testing.T) { if testing.Short() { t.Skip("skipping testscript") diff --git a/testdata/scripts/node/validate/defaults-override.txtar b/testdata/scripts/node/validate/defaults-override.txtar new file mode 100644 index 00000000000..3accd1584bb --- /dev/null +++ b/testdata/scripts/node/validate/defaults-override.txtar @@ -0,0 +1,444 @@ +# test with default override +env CL_CHAIN_DEFAULTS=default_overrides +exec chainlink node -c config.toml -s secrets.toml validate +cmp stdout out.txt + +# remove override, FinalityTagEnabled should no longer match +env CL_CHAIN_DEFAULTS= +exec chainlink node -c config.toml -s secrets.toml validate +! cmp stdout out.txt + +# overrides outside of evm suffix, FinalityTagEnabled should not match as override is not applied +env CL_CHAIN_DEFAULTS=default_overrides2 +! exec chainlink node -c config.toml -s secrets.toml validate + +# duplicate chain IDs +env CL_CHAIN_DEFAULTS=default_overrides3 +! exec chainlink node -c config.toml -s secrets.toml validate +stderr 'contains duplicate ChainID' + +-- default_overrides/evm/Ethereum_Mainnet.toml -- +ChainID = '1' +FinalityTagEnabled = true + +-- default_overrides2/Ethereum_Mainnet.toml -- +ChainID = '1' +FinalityTagEnabled = true + +-- default_overrides3/evm/Ethereum_Mainnet.toml -- +ChainID = '1' + +-- default_overrides3/evm/Ethereum_Testnet.toml -- +ChainID = '1' + +-- config.toml -- +Log.Level = 'debug' + +[[EVM]] +ChainID = '1' + +[[EVM.Nodes]] +Name = 'fake' +WSURL = 'wss://foo.bar/ws' +HTTPURL = 'https://foo.bar' + +-- secrets.toml -- +[Database] +URL = 'postgresql://user:pass1234567890abcd@localhost:5432/dbname?sslmode=disable' + +[Password] +Keystore = 'keystore_pass' + +-- out.txt -- +# Secrets: +[Database] +URL = 'xxxxx' +AllowSimplePasswords = false + +[Password] +Keystore = 'xxxxx' + +# Input Configuration: +[Log] +Level = 'debug' + +[[EVM]] +ChainID = '1' + +[[EVM.Nodes]] +Name = 'fake' +WSURL = 'wss://foo.bar/ws' +HTTPURL = 'https://foo.bar' + +# Effective Configuration, with defaults applied: +InsecureFastScrypt = false +RootDir = '~/.chainlink' +ShutdownGracePeriod = '5s' + +[Feature] +FeedsManager = true +LogPoller = false +UICSAKeys = false +CCIP = true +MultiFeedsManagers = false + +[Database] +DefaultIdleInTxSessionTimeout = '1h0m0s' +DefaultLockTimeout = '15s' +DefaultQueryTimeout = '10s' +LogQueries = false +MaxIdleConns = 10 +MaxOpenConns = 100 +MigrateOnStartup = true + +[Database.Backup] +Dir = '' +Frequency = '1h0m0s' +Mode = 'none' +OnVersionUpgrade = true + +[Database.Listener] +MaxReconnectDuration = '10m0s' +MinReconnectInterval = '1m0s' +FallbackPollInterval = '30s' + +[Database.Lock] +Enabled = true +LeaseDuration = '10s' +LeaseRefreshInterval = '1s' + +[TelemetryIngress] +UniConn = true +Logging = false +BufferSize = 100 +MaxBatchSize = 50 +SendInterval = '500ms' +SendTimeout = '10s' +UseBatchSend = true + +[AuditLogger] +Enabled = false +ForwardToUrl = '' +JsonWrapperKey = '' +Headers = [] + +[Log] +Level = 'debug' +JSONConsole = false +UnixTS = false + +[Log.File] +Dir = '' +MaxSize = '5.12gb' +MaxAgeDays = 0 +MaxBackups = 1 + +[WebServer] +AuthenticationMethod = 'local' +AllowOrigins = 'http://localhost:3000,http://localhost:6688' +BridgeResponseURL = '' +BridgeCacheTTL = '0s' +HTTPWriteTimeout = '10s' +HTTPPort = 6688 +SecureCookies = true +SessionTimeout = '15m0s' +SessionReaperExpiration = '240h0m0s' +HTTPMaxSize = '32.77kb' +StartTimeout = '15s' +ListenIP = '0.0.0.0' + +[WebServer.LDAP] +ServerTLS = true +SessionTimeout = '15m0s' +QueryTimeout = '2m0s' +BaseUserAttr = 'uid' +BaseDN = '' +UsersDN = 'ou=users' +GroupsDN = 'ou=groups' +ActiveAttribute = '' +ActiveAttributeAllowedValue = '' +AdminUserGroupCN = 'NodeAdmins' +EditUserGroupCN = 'NodeEditors' +RunUserGroupCN = 'NodeRunners' +ReadUserGroupCN = 'NodeReadOnly' +UserApiTokenEnabled = false +UserAPITokenDuration = '240h0m0s' +UpstreamSyncInterval = '0s' +UpstreamSyncRateLimit = '2m0s' + +[WebServer.MFA] +RPID = '' +RPOrigin = '' + +[WebServer.RateLimit] +Authenticated = 1000 +AuthenticatedPeriod = '1m0s' +Unauthenticated = 5 +UnauthenticatedPeriod = '20s' + +[WebServer.TLS] +CertPath = '' +ForceRedirect = false +Host = '' +HTTPSPort = 6689 +KeyPath = '' +ListenIP = '0.0.0.0' + +[JobPipeline] +ExternalInitiatorsEnabled = false +MaxRunDuration = '10m0s' +MaxSuccessfulRuns = 10000 +ReaperInterval = '1h0m0s' +ReaperThreshold = '24h0m0s' +ResultWriteQueueDepth = 100 +VerboseLogging = true + +[JobPipeline.HTTPRequest] +DefaultTimeout = '15s' +MaxSize = '32.77kb' + +[FluxMonitor] +DefaultTransactionQueueDepth = 1 +SimulateTransactions = false + +[OCR2] +Enabled = false +ContractConfirmations = 3 +BlockchainTimeout = '20s' +ContractPollInterval = '1m0s' +ContractSubscribeInterval = '2m0s' +ContractTransmitterTransmitTimeout = '10s' +DatabaseTimeout = '10s' +KeyBundleID = '0000000000000000000000000000000000000000000000000000000000000000' +CaptureEATelemetry = false +CaptureAutomationCustomTelemetry = true +DefaultTransactionQueueDepth = 1 +SimulateTransactions = false +TraceLogging = false + +[OCR] +Enabled = false +ObservationTimeout = '5s' +BlockchainTimeout = '20s' +ContractPollInterval = '1m0s' +ContractSubscribeInterval = '2m0s' +DefaultTransactionQueueDepth = 1 +KeyBundleID = '0000000000000000000000000000000000000000000000000000000000000000' +SimulateTransactions = false +TransmitterAddress = '' +CaptureEATelemetry = false +TraceLogging = false + +[P2P] +IncomingMessageBufferSize = 10 +OutgoingMessageBufferSize = 10 +PeerID = '' +TraceLogging = false + +[P2P.V2] +Enabled = true +AnnounceAddresses = [] +DefaultBootstrappers = [] +DeltaDial = '15s' +DeltaReconcile = '1m0s' +ListenAddresses = [] + +[Keeper] +DefaultTransactionQueueDepth = 1 +GasPriceBufferPercent = 20 +GasTipCapBufferPercent = 20 +BaseFeeBufferPercent = 20 +MaxGracePeriod = 100 +TurnLookBack = 1000 + +[Keeper.Registry] +CheckGasOverhead = 200000 +PerformGasOverhead = 300000 +MaxPerformDataSize = 5000 +SyncInterval = '30m0s' +SyncUpkeepQueueSize = 10 + +[AutoPprof] +Enabled = false +ProfileRoot = '' +PollInterval = '10s' +GatherDuration = '10s' +GatherTraceDuration = '5s' +MaxProfileSize = '100.00mb' +CPUProfileRate = 1 +MemProfileRate = 1 +BlockProfileRate = 1 +MutexProfileFraction = 1 +MemThreshold = '4.00gb' +GoroutineThreshold = 5000 + +[Pyroscope] +ServerAddress = '' +Environment = 'mainnet' + +[Sentry] +Debug = false +DSN = '' +Environment = '' +Release = '' + +[Insecure] +DevWebServer = false +OCRDevelopmentMode = false +InfiniteDepthQueries = false +DisableRateLimiting = false + +[Tracing] +Enabled = false +CollectorTarget = '' +NodeID = '' +SamplingRatio = 0.0 +Mode = 'tls' +TLSCertPath = '' + +[Mercury] +VerboseLogging = false + +[Mercury.Cache] +LatestReportTTL = '1s' +MaxStaleAge = '1h0m0s' +LatestReportDeadline = '5s' + +[Mercury.TLS] +CertFile = '' + +[Mercury.Transmitter] +TransmitQueueMaxSize = 10000 +TransmitTimeout = '5s' + +[Capabilities] +[Capabilities.Peering] +IncomingMessageBufferSize = 10 +OutgoingMessageBufferSize = 10 +PeerID = '' +TraceLogging = false + +[Capabilities.Peering.V2] +Enabled = false +AnnounceAddresses = [] +DefaultBootstrappers = [] +DeltaDial = '15s' +DeltaReconcile = '1m0s' +ListenAddresses = [] + +[Capabilities.Dispatcher] +SupportedVersion = 1 +ReceiverBufferSize = 10000 + +[Capabilities.Dispatcher.RateLimit] +GlobalRPS = 800.0 +GlobalBurst = 1000 +PerSenderRPS = 10.0 +PerSenderBurst = 50 + +[Capabilities.ExternalRegistry] +Address = '' +NetworkID = 'evm' +ChainID = '1' + +[[EVM]] +ChainID = '1' +AutoCreateKey = true +BlockBackfillDepth = 10 +BlockBackfillSkip = false +FinalityDepth = 50 +FinalityTagEnabled = true +LinkContractAddress = '0x514910771AF9Ca656af840dff83E8264EcF986CA' +LogBackfillBatchSize = 1000 +LogPollInterval = '15s' +LogKeepBlocksDepth = 100000 +LogPrunePageSize = 0 +BackupLogPollerBlockDelay = 100 +MinIncomingConfirmations = 3 +MinContractPayment = '0.1 link' +NonceAutoSync = true +NoNewHeadsThreshold = '3m0s' +OperatorFactoryAddress = '0x3E64Cd889482443324F91bFA9c84fE72A511f48A' +RPCDefaultBatchSize = 250 +RPCBlockQueryDelay = 1 +FinalizedBlockOffset = 0 +NoNewFinalizedHeadsThreshold = '9m0s' + +[EVM.Transactions] +ForwardersEnabled = false +MaxInFlight = 16 +MaxQueued = 250 +ReaperInterval = '1h0m0s' +ReaperThreshold = '168h0m0s' +ResendAfterThreshold = '1m0s' + +[EVM.Transactions.AutoPurge] +Enabled = false + +[EVM.BalanceMonitor] +Enabled = true + +[EVM.GasEstimator] +Mode = 'BlockHistory' +PriceDefault = '20 gwei' +PriceMax = '115792089237316195423570985008687907853269984665.640564039457584007913129639935 tether' +PriceMin = '1 gwei' +LimitDefault = 500000 +LimitMax = 500000 +LimitMultiplier = '1' +LimitTransfer = 21000 +EstimateGasLimit = false +BumpMin = '5 gwei' +BumpPercent = 20 +BumpThreshold = 3 +EIP1559DynamicFees = true +FeeCapDefault = '100 gwei' +TipCapDefault = '1 wei' +TipCapMin = '1 wei' + +[EVM.GasEstimator.BlockHistory] +BatchSize = 25 +BlockHistorySize = 4 +CheckInclusionBlocks = 12 +CheckInclusionPercentile = 90 +TransactionPercentile = 50 + +[EVM.HeadTracker] +HistoryDepth = 100 +MaxBufferSize = 3 +SamplingInterval = '1s' +MaxAllowedFinalityDepth = 10000 +FinalityTagBypass = true + +[EVM.NodePool] +PollFailureThreshold = 5 +PollInterval = '10s' +SelectionMode = 'HighestHead' +SyncThreshold = 5 +LeaseDuration = '0s' +NodeIsSyncingEnabled = false +FinalizedBlockPollInterval = '5s' +EnforceRepeatableRead = false +DeathDeclarationDelay = '10s' + +[EVM.OCR] +ContractConfirmations = 4 +ContractTransmitterTransmitTimeout = '10s' +DatabaseTimeout = '10s' +DeltaCOverride = '168h0m0s' +DeltaCJitterOverride = '1h0m0s' +ObservationGracePeriod = '1s' + +[EVM.OCR2] +[EVM.OCR2.Automation] +GasLimit = 10500000 + +[EVM.Workflow] +GasLimitDefault = 400000 + +[[EVM.Nodes]] +Name = 'fake' +WSURL = 'wss://foo.bar/ws' +HTTPURL = 'https://foo.bar' + +Valid configuration.