diff --git a/.changeset/tall-falcons-yawn.md b/.changeset/tall-falcons-yawn.md new file mode 100644 index 00000000000..98b90e5994b --- /dev/null +++ b/.changeset/tall-falcons-yawn.md @@ -0,0 +1,5 @@ +--- +"chainlink": patch +--- + +#added the ability to define a fallback.toml override config using CL_CHAIN_DEFAULTS env var diff --git a/core/chains/evm/config/toml/defaults.go b/core/chains/evm/config/toml/defaults.go index 6f03575056b..60da9bded1b 100644 --- a/core/chains/evm/config/toml/defaults.go +++ b/core/chains/evm/config/toml/defaults.go @@ -4,7 +4,7 @@ import ( "bytes" "embed" "fmt" - "io" + "io/fs" "log" "os" "path/filepath" @@ -19,7 +19,6 @@ import ( ) var ( - //go:embed defaults/*.toml defaultsFS embed.FS fallback Chain @@ -33,48 +32,24 @@ var ( ) func init() { - // read the defaults first + var ( + fb *Chain + err error + ) - fes, err := defaultsFS.ReadDir("defaults") + // read all default configs + DefaultIDs, defaultNames, defaults, fb, err = initDefaults(defaultsFS.ReadDir, defaultsFS.ReadFile, "defaults") if err != nil { - log.Fatalf("failed to read defaults/: %v", err) + log.Fatalf("failed to read defaults: %s", err) } - for _, fe := range fes { - path := filepath.Join("defaults", fe.Name()) - 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 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 { - log.Fatalf("fallback ChainID must be nil, not: %s", config.ChainID) - } - fallback = config.Chain - continue - } - if config.ChainID == nil { - log.Fatalf("missing ChainID: %s", path) - } - DefaultIDs = append(DefaultIDs, config.ChainID) - id := config.ChainID.String() - if _, ok := defaults[id]; ok { - log.Fatalf("%q contains duplicate ChainID: %s", path, id) - } - defaults[id] = config.Chain - defaultNames[id] = strings.ReplaceAll(strings.TrimSuffix(fe.Name(), ".toml"), "_", " ") + if fb == nil { + log.Fatal("failed to set fallback chain config") } - slices.SortFunc(DefaultIDs, func(a, b *big.Big) int { - return a.Cmp(b) - }) + fallback = *fb + + // check for and apply any overrides // read the custom defaults overrides dir := env.CustomDefaults.Get() if dir == "" { @@ -83,54 +58,99 @@ func init() { } // use evm overrides specifically - evmDir := fmt.Sprintf("%s/evm", dir) + _, _, customDefaults, fb, err = initDefaults(os.ReadDir, os.ReadFile, dir+"/evm") + if err != nil { + log.Fatalf("failed to read custom overrides: %s", err) + } - // Read directory contents for evm only - entries, err := os.ReadDir(evmDir) + if fb != nil { + fallback = *fb + } +} + +func initDefaults( + dirReader func(name string) ([]fs.DirEntry, error), + fileReader func(name string) ([]byte, error), + root string, +) ([]*big.Big, map[string]string, map[string]Chain, *Chain, error) { + entries, err := dirReader(root) if err != nil { - log.Fatalf("error reading evm custom defaults override directory: %v", err) - return + return nil, nil, nil, nil, fmt.Errorf("failed to read directory: %w", err) } + var fb *Chain + + ids := make([]*big.Big, 0) + configs := make(map[string]Chain) + names := make(map[string]string) + 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 the file to bytes + path := filepath.Join(root, entry.Name()) - // Read file contents - b, err := io.ReadAll(file) - file.Close() + chainID, chain, err := readConfig(path, fileReader) if err != nil { - log.Fatalf("error reading file (name: %v) contents in custom defaults override directory: %v", entry.Name(), err) + return nil, nil, nil, nil, err } - var config = struct { - ChainID *big.Big - Chain - }{} + if entry.Name() == "fallback.toml" { + if chainID != nil { + return nil, nil, nil, nil, fmt.Errorf("fallback ChainID must be nil: found: %s", chainID) + } + + fb = &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) + continue } - if config.ChainID == nil { - log.Fatalf("missing ChainID in: %s in custom defaults override directory. exiting", path) + // ensure ChainID is set + if chainID == nil { + return nil, nil, nil, nil, fmt.Errorf("missing ChainID: %s", path) } - id := config.ChainID.String() + ids = append(ids, chainID) - if _, ok := customDefaults[id]; ok { + // ChainID as a default should not be duplicated + id := chainID.String() + if _, ok := configs[id]; ok { log.Fatalf("%q contains duplicate ChainID: %s", path, id) } - customDefaults[id] = config.Chain + + // set lookups + configs[id] = chain + names[id] = strings.ReplaceAll(strings.TrimSuffix(entry.Name(), ".toml"), "_", " ") } + + // sort IDs in numeric order + slices.SortFunc(ids, func(a, b *big.Big) int { + return a.Cmp(b) + }) + + return ids, names, configs, fb, nil +} + +func readConfig(path string, reader func(name string) ([]byte, error)) (*big.Big, Chain, error) { + bts, err := reader(path) + if err != nil { + return nil, Chain{}, fmt.Errorf("error reading file: %w", err) + } + + var config = struct { + ChainID *big.Big + Chain + }{} + + // decode from toml to a chain config + if err := cconfig.DecodeTOML(bytes.NewReader(bts), &config); err != nil { + return nil, Chain{}, fmt.Errorf("error in TOML decoding %s: %w", path, err) + } + + return config.ChainID, config.Chain, nil } // DefaultsNamed returns the default Chain values, optionally for the given chainID, as well as a name if the chainID is known. diff --git a/testdata/scripts/node/validate/fallback-override.txtar b/testdata/scripts/node/validate/fallback-override.txtar new file mode 100644 index 00000000000..91feb48693d --- /dev/null +++ b/testdata/scripts/node/validate/fallback-override.txtar @@ -0,0 +1,552 @@ +# test with defaults +env CL_CHAIN_DEFAULTS= +exec chainlink node -c config.toml -s secrets.toml validate +cmp stdout out.txt + +# test with fallback override +env CL_CHAIN_DEFAULTS=default_overrides +exec chainlink node -c config.toml -s secrets.toml validate +! cmp stdout out.txt + +-- default_overrides/evm/fallback.toml -- +AutoCreateKey = true +BlockBackfillDepth = 1000000 +BlockBackfillSkip = false +FinalityDepth = 50 +FinalityTagEnabled = false +LogBackfillBatchSize = 1000 +LogPollInterval = '15s' +LogKeepBlocksDepth = 100000 +LogPrunePageSize = 0 +BackupLogPollerBlockDelay = 100 +MinContractPayment = '.00001 link' +MinIncomingConfirmations = 3 +NonceAutoSync = true +NoNewHeadsThreshold = '3m' +RPCDefaultBatchSize = 250 +RPCBlockQueryDelay = 1 +FinalizedBlockOffset = 0 +NoNewFinalizedHeadsThreshold = '0' +LogBroadcasterEnabled = true + +[Transactions] +ForwardersEnabled = false +MaxInFlight = 16 +MaxQueued = 250 +ReaperInterval = '1h' +ReaperThreshold = '168h' +ResendAfterThreshold = '1m' + +[Transactions.AutoPurge] +Enabled = false + +[BalanceMonitor] +Enabled = true + +[GasEstimator] +Mode = 'BlockHistory' +PriceDefault = '20 gwei' +PriceMax = '115792089237316195423570985008687907853269984665.640564039457584007913129639935 tether' +PriceMin = '1 gwei' +LimitDefault = 500_000 +LimitMax = 500_000 +LimitMultiplier = '1' +LimitTransfer = 21_000 +BumpMin = '5 gwei' +BumpPercent = 20 +BumpThreshold = 3 +EIP1559DynamicFees = false +FeeCapDefault = '100 gwei' +TipCapDefault = '1' +TipCapMin = '1' +EstimateLimit = false + +[GasEstimator.BlockHistory] +BatchSize = 25 +BlockHistorySize = 8 +CheckInclusionBlocks = 12 +CheckInclusionPercentile = 90 +TransactionPercentile = 60 + +[GasEstimator.FeeHistory] +CacheTimeout = '10s' + +[HeadTracker] +HistoryDepth = 100 +MaxBufferSize = 3 +SamplingInterval = '1s' +FinalityTagBypass = true +MaxAllowedFinalityDepth = 10000 +PersistenceEnabled = true + +[NodePool] +PollFailureThreshold = 5 +PollInterval = '10s' +SelectionMode = 'HighestHead' +SyncThreshold = 5 +LeaseDuration = '0s' +NodeIsSyncingEnabled = false +FinalizedBlockPollInterval = '5s' +EnforceRepeatableRead = true +DeathDeclarationDelay = '1m' +NewHeadsPollInterval = '0s' + +[OCR] +ContractConfirmations = 4 +ContractTransmitterTransmitTimeout = '10s' +DatabaseTimeout = '10s' +DeltaCOverride = '168h' +DeltaCJitterOverride = '1h' +ObservationGracePeriod = '1s' + +[OCR2.Automation] +GasLimit = 5400000 + +[Workflow] +GasLimitDefault = 400_000 + +-- 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 = false +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' +TransmitConcurrency = 100 + +[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' + +[Capabilities.WorkflowRegistry] +Address = '' +NetworkID = 'evm' +ChainID = '1' + +[Capabilities.GatewayConnector] +ChainIDForNodeKey = '' +NodeAddress = '' +DonID = '' +WSHandshakeTimeoutMillis = 0 +AuthMinChallengeLen = 0 +AuthTimestampToleranceSec = 0 + +[[Capabilities.GatewayConnector.Gateways]] +ID = '' +URL = '' + +[Telemetry] +Enabled = false +CACertFile = '' +Endpoint = '' +InsecureConnection = false +TraceSampleRatio = 0.01 +EmitterBatchProcessor = true +EmitterExportTimeout = '1s' + +[[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' +LogBroadcasterEnabled = true +RPCDefaultBatchSize = 250 +RPCBlockQueryDelay = 1 +FinalizedBlockOffset = 0 +NoNewFinalizedHeadsThreshold = '9m0s' + +[EVM.Transactions] +Enabled = true +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 +EstimateLimit = 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.GasEstimator.FeeHistory] +CacheTimeout = '10s' + +[EVM.HeadTracker] +HistoryDepth = 100 +MaxBufferSize = 3 +SamplingInterval = '1s' +MaxAllowedFinalityDepth = 10000 +FinalityTagBypass = true +PersistenceEnabled = true + +[EVM.NodePool] +PollFailureThreshold = 5 +PollInterval = '10s' +SelectionMode = 'HighestHead' +SyncThreshold = 5 +LeaseDuration = '0s' +NodeIsSyncingEnabled = false +FinalizedBlockPollInterval = '5s' +EnforceRepeatableRead = true +DeathDeclarationDelay = '1m0s' +NewHeadsPollInterval = '0s' + +[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.