diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3431185..637e9e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,40 @@ jobs: - name: Check cabal file formatting run: ./scripts/cabal-format.sh + cue: + name: CUE front-end (schema drift, vet, ajv) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: stable + + # Pin the CUE version: `cue import jsonschema` output (e.g. how defaults + # are translated) varies across releases, so the committed cue/schema/gen_* + # must be regenerated with this same version (see cue/README.md). + - name: Install CUE 0.17.0 + run: | + go install cuelang.org/go/cmd/cue@v0.17.0 + echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + "$(go env GOPATH)/bin/cue" version + + - name: Install just + run: | + sudo apt-get update + sudo apt-get install -y just + just --version + + - name: Install ajv-cli + run: npm install -g ajv-cli + + # Generated-CUE drift gate, then vet + schema-validate + lint every config + # and example (the per-file `just` recipes, looped over everything). + - name: Check CUE front-end + run: ./scripts/cue-ci.sh + test: name: Build & test (GHC ${{ matrix.ghc }}) runs-on: ubuntu-latest diff --git a/README.md b/README.md index 7e8b22b..6419fc6 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ The goal is one shared parser for applications that need the node's configuratio such as [`cardano-cli`](https://github.com/IntersectMBO/cardano-cli), [`dmq-node`](https://github.com/IntersectMBO/dmq-node/) and [the `ouroboros-consensus` tools](https://github.com/IntersectMBO/ouroboros-consensus/tree/main/ouroboros-consensus-cardano#consensus-db-tools). -The bundled `cardano-config` executable exposes the same via its `resolve` and -`schema` subcommands. +The bundled `cardano-config` executable exposes the same via its `resolve`, +`schema` and `migrate` subcommands. ## Recommended format @@ -42,21 +42,57 @@ pointing to that component's schema (e.g. a `StorageConfig` sub-file uses `schem so editors and validators pick up the right schema for the sub-file. The key is an annotation: the parser accepts and ignores it. -To port an old config to the new format, group the component keys under their -sections inside `Configuration` and add the `Version` / `MinNodeVersion` -envelope. `cardano-config schema` documents the recommended form; -`--legacy-one-file` documents the flat form. +To port an old config to the new format, run `cardano-config migrate` (it reads +`-` as stdin, so you can fetch and convert in one step): + +```console +$ cardano-config migrate old-config.json > config.json +$ curl -sL | cardano-config migrate - > config.json +``` + +It reshapes the document into the envelope as JSON: it adds `$schema` and +`Version`, carries `MinNodeVersion` through, and groups each component's keys +under its section inside `Configuration`. It also brings field names up to date: +the parser rejects the old names, so `migrate` rewrites the ones that were +renamed (`hardLimit`/`softLimit`/`delay` → `HardLimit`/`SoftLimit`/`Delay`, +`EnableRpc`/`RpcSocketPath` → `EnableGrpc`/`GrpcSocketPath`, `TargetNumberOf*` → +`DeadlineTargetNumberOf*`) and drops the ones that were removed +(`PBftSignatureThreshold`, `LastKnownBlockVersion-Major`/`-Minor`/`-Alt`, now +supplied by consensus defaults; the vestigial `Protocol`; and +`MaxKnownMajorProtocolVersion`, a dead key the node never read). Apart from that +it preserves the values as written and does not fill in defaults, inline +referenced sub-files, or read genesis files; follow it with `resolve` to check +the result. + +A genuinely unrecognised key (a typo, say) is **kept** rather than silently +dropped, so nothing is lost - but it remains unrecognised and so still surfaces +as an `UnrecognisedKeys` warning on the next parse. Remove it by hand if you want +a warning-free config. + +(To port by hand instead: group the component keys under their sections inside +`Configuration` and add the `Version` / `MinNodeVersion` envelope. `cardano-config +schema` documents the recommended form; `--legacy-one-file` documents the flat +form.) + +## Authoring with CUE + +For typed authoring use the CUE front-end in [`cue/`](cue/): you write the +configuration as a CUE value, `just vet` / `just lint` catch structural and +cross-field mistakes early, and `cue export` emits the envelope JSON this +library ingests. The library stays the final authority (it re-parses, fills +defaults, and verifies genesis hashes). See [`cue/README.md`](cue/README.md). ## Defaults and layering Every component ships a **default file** under [`defaults/`](defaults/), with the -network/role overlays under [`variants/`](variants/). For each component the -layering, from lowest to highest precedence, is: +per-network overlays under [`variants/`](variants/) and the `NetworkConfig` role +overlays under [`defaults/NetworkConfig/`](defaults/NetworkConfig/). For each +component the layering, from lowest to highest precedence, is: 1. the package's base default (`defaults/.json`), always applied; 2. for the `Network` component only, a **role layer** chosen automatically from credential presence: the block-producer or relay variant - (`variants/NetworkConfig/{blockproducer,relay}.json`) + (`defaults/NetworkConfig/{blockproducer,relay}.json`) fills the deadline peer targets and `PeerSharing` when the configuration leaves them unset (so it sits *below* the file value); 3. the component's value in the configuration file (an inline object or a sub-file @@ -150,14 +186,27 @@ The flag names, metavars and help text match those historically accepted by `cardano-node`, so existing operator scripts keep working. You can inspect the parsed options with `cabal run cardano-config -- resolve --help`. -## Tracing options are **not** parsed by this library - -Tracing is **not** parsed: it is owned by the node's tracing system (hermod / -`trace-dispatcher`) and given under a single top-level `HermodTracing` key whose -value is a path to a separate file. The key is recognised and captured opaquely -(it appears in the schema), but its contents are not interpreted here. The -authoritative schema lives in -[`hermod-tracing`](https://github.com/IntersectMBO/hermod-tracing). +## Tracing options are owned by `trace-dispatcher` + +Tracing is owned by the node's tracing system (hermod / `trace-dispatcher`), +given under a single top-level `HermodTracing` key whose value is **either** a +path to a separate file holding the tracing configuration **or** that +configuration object inline. This library does not define or validate the shape +of that object — the authoritative schema lives in +[`hermod-tracing`](https://github.com/IntersectMBO/hermod-tracing), so the +configuration schema describes `HermodTracing` only as "a path or a JSON +object". + +Instead, the parser hands the `HermodTracing` value to `trace-dispatcher`'s own +parser (`readConfiguration`), which resolves it into a `TraceConfig`: a file +reference is read via `FromFile` (after resolving the path to its canonical +location), an inline object via `FromJSONObject`. + +The resolved `TraceConfig` is carried through to the final `NodeConfiguration` +(as `tracingConfiguration :: Maybe TraceConfig`), so a consumer of the library +gets the tracing configuration already parsed, and `cardano-config resolve` +emits it back under the `HermodTracing` key (as an inline object). It is +`Nothing`/absent when the configuration has no `HermodTracing` key. ## Mandatory keys diff --git a/app/Main.hs b/app/Main.hs index 3feac2f..8cdb288 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -1,4 +1,4 @@ --- | The @cardano-config@ command-line tool. It exposes two subcommands: +-- | The @cardano-config@ command-line tool. It exposes three subcommands: -- -- * @cardano-config resolve@ resolves a @cardano-node@ configuration -- (per-component defaults, the configuration file and the CLI flags), @@ -7,11 +7,17 @@ -- -- * @cardano-config schema@ dumps the configuration JSON Schema (for the -- whole configuration or a single component), derived from the same codecs. +-- +-- * @cardano-config migrate@ reshapes a configuration into the recommended +-- @{ $schema, Version, MinNodeVersion, Configuration }@ envelope and prints +-- it as JSON (a purely structural migration, preserving the values). module Main (main) where import Cardano.Configuration (parseConfigurationFiles, renderConfigWarning, resolveConfiguration) import Cardano.Configuration.CliArgs (CliArgs, configFilePath, parseCliArgs) import Cardano.Configuration.File (componentDefaults) +import Cardano.Configuration.File.Merge (decodeValueFile) +import Cardano.Configuration.File.Migrate (migrate) import Cardano.Configuration.Render (GenesisRendering (..), nodeConfigurationToJSON) import Cardano.Configuration.Schema ( configurationSchemas @@ -28,6 +34,7 @@ import qualified Data.ByteString.Lazy.Char8 as L import Data.Foldable (for_) import Data.List (intercalate) import qualified Data.Text as T +import Data.Yaml (decodeThrow) import Data.Yaml.Pretty (encodePretty, setConfCompare, setConfDropNull) import qualified Data.Yaml.Pretty as Yaml import Options.Applicative @@ -42,6 +49,8 @@ data Command Resolve CliArgs GenesisRendering | -- | @schema@: dump a JSON Schema. Schema SchemaCmd + | -- | @migrate@: reshape a configuration file into the Version1 envelope. + Migrate FilePath -- | What the @schema@ subcommand should print. data SchemaCmd @@ -91,6 +100,20 @@ commandParser = <> footerDoc (Just schemaValidationHelp) ) ) + <> command + "migrate" + ( info + ( Migrate + <$> strArgument + (metavar "CONFIG" <> help "Configuration file to migrate (JSON or YAML), or - for stdin.") + ) + ( progDesc + ( "Reshape a configuration into the recommended " + <> "{ $schema, Version, MinNodeVersion, Configuration } envelope and print it as JSON. " + <> "Preserves the values as written (no defaults are filled, no sub-files inlined)." + ) + ) + ) ) -- | How to validate a configuration against the schema, shown under @@ -148,6 +171,17 @@ withGenesesFlag = run :: Command -> IO () run (Resolve cli geneses) = runResolve cli geneses run (Schema cmd) = runSchema cmd +run (Migrate path) = runMigrate path + +-- | Read a configuration and print it, reshaped into the Version1 envelope, as +-- JSON. A purely structural migration: it does not resolve, default or validate. +-- A path of @-@ reads the configuration from stdin (so it composes with @curl@). +runMigrate :: FilePath -> IO () +runMigrate path = handleAny (die . displayException) $ do + raw <- case path of + "-" -> BS.getContents >>= decodeThrow + _ -> decodeValueFile Nothing path + dump (migrate raw) -- | Resolve a configuration and print it as YAML. runResolve :: CliArgs -> GenesisRendering -> IO () diff --git a/cabal.project b/cabal.project index 0c4805c..e71cb61 100644 --- a/cabal.project +++ b/cabal.project @@ -17,33 +17,6 @@ index-state: packages: . --- cardano-ledger at current master (provides the genesis types we decode here). --- Kept to the minimum set of subdirs we actually depend on; add more only if the --- solver requires them. -source-repository-package - type: git - location: https://github.com/IntersectMBO/cardano-ledger - tag: 29c7e3040621f323f3af27fd4e89a223ba429021 - --sha256: sha256-XPXfHvYjuzN0ASL/z4HwWJwDPQn7vJbTPlEa5zV3w9k= - subdir: - eras/byron/ledger/impl - eras/byron/crypto - eras/byron/ledger/executable-spec - eras/byron/chain/executable-spec - eras/shelley/impl - eras/allegra/impl - eras/mary/impl - eras/alonzo/impl - eras/babbage/impl - eras/conway/impl - eras/dijkstra/impl - libs/cardano-ledger-core - libs/cardano-ledger-binary - libs/cardano-data - libs/vector-map - libs/non-integral - libs/small-steps - -- Ensures colourized output from test runners test-show-details: direct tests: true @@ -73,6 +46,7 @@ if impl(ghc >=9.14) , canonical-json:containers , cborg:base , cborg:containers + , cborg-json:base , constrained-generators:QuickCheck , dependent-map:containers , dictionary-sharing:containers @@ -91,3 +65,41 @@ if impl(ghc >=9.14) constraints: -- Happy version 2.2.1 fails to compile haskell-src-exts , happy < 2.2.1 + +-- cardano-ledger at current master (provides the genesis types we decode here). +-- Kept to the minimum set of subdirs we actually depend on; add more only if the +-- solver requires them. +source-repository-package + type: git + location: https://github.com/IntersectMBO/cardano-ledger + tag: 29c7e3040621f323f3af27fd4e89a223ba429021 + --sha256: sha256-XPXfHvYjuzN0ASL/z4HwWJwDPQn7vJbTPlEa5zV3w9k= + subdir: + eras/byron/ledger/impl + eras/byron/crypto + eras/byron/ledger/executable-spec + eras/byron/chain/executable-spec + eras/shelley/impl + eras/allegra/impl + eras/mary/impl + eras/alonzo/impl + eras/babbage/impl + eras/conway/impl + eras/dijkstra/impl + libs/cardano-ledger-core + libs/cardano-ledger-binary + libs/cardano-data + libs/vector-map + libs/non-integral + libs/small-steps + +-- trace-dispatcher (hermod tracing) — provides the tracing configuration parser +-- (ConfigSource/readConfiguration) used to resolve the HermodTracing key. +-- Pinned to the merge commit after hermod-tracing#18 (mkarg/config-source). +source-repository-package + type: git + location: https://github.com/IntersectMBO/hermod-tracing + tag: 3d7aeb85d9ef01df48a44c751e45609b4c6dea00 + --sha256: sha256-r4bVm4x2akrCLcg5f0TG6aovCGqNnZkiiBckA3YCz7Q= + subdir: + trace-dispatcher diff --git a/cardano-config.cabal b/cardano-config.cabal index 793e1a0..fdb6ad7 100644 --- a/cardano-config.cabal +++ b/cardano-config.cabal @@ -53,6 +53,7 @@ library internal Cardano.Configuration.File.Lint Cardano.Configuration.File.Mempool Cardano.Configuration.File.Merge + Cardano.Configuration.File.Migrate Cardano.Configuration.File.Network Cardano.Configuration.File.Protocol Cardano.Configuration.File.Storage @@ -97,6 +98,7 @@ library internal scientific, text, time, + trace-dispatcher, transformers, yaml, diff --git a/cue/README.md b/cue/README.md new file mode 100644 index 0000000..7227702 --- /dev/null +++ b/cue/README.md @@ -0,0 +1,129 @@ +# CUE front-end for cardano-config + +Author `cardano-node` configurations as typed CUE. `cue vet` checks structure and +the cross-field rules the JSON Schema can't express, `cue export` emits JSON, and +the `cardano-config` executable resolves it. CUE catches mistakes early; the +**library remains the final authority** - it re-parses the JSON, fills defaults, +and verifies genesis hashes (which CUE and ajv cannot). + +## Recommended format + +The **Version 1 envelope**: `$schema` (the schema the file follows), `Version` +and `MinNodeVersion` at the top level, with components grouped under +`Configuration` per section (each given inline, or as a path to a split +sub-file). Anything else draws a non-fatal warning (a missing envelope key, or +the legacy flat form). + +```json +{ + "$schema": "https://raw.githubusercontent.com/IntersectMBO/cardano-config/main/schemas/config.schema.json", + "Version": 1, + "MinNodeVersion": "10.5.0", + "Configuration": { + "ProtocolConfig": { "...": "..." }, + "StorageConfig": "storage.json" + } +} +``` + +## Cookbook + +All `just` recipes run from the repo root. The per-file recipes - `vet`, `lint`, +`export`, `ajv` and `check` - each **take a config path** (relative to `cue/`), +e.g. `just vet configs/my-node.cue`; they act on that one file, not on every +config. (`regen` takes no argument; `resolve` takes an exported JSON.) Override +the binaries with env vars if needed, e.g. +`CUE=~/go/bin/cue just vet configs/my-node.cue`. + +### 1. Install + +```sh +cabal install cardano-config:exe:cardano-config # the resolver/validator +go install cuelang.org/go/cmd/cue@v0.17.0 # CUE (pin: gen files use 0.17) +sudo apt-get install -y just # task runner +npm install -g ajv-cli # JSON Schema validator +``` + +### 2. Author a config + +Copy an example from `examples/` and edit. `#Node` is the envelope: `$schema` +and `Version` default and `Configuration` is required (all emitted +automatically); set `MinNodeVersion`. Unify a network preset (`mainnet`, +`preview` or `preprod`) into `Configuration`, then write only your deviations: + +```cue +// configs/my-node.cue +package main + +import "github.com/intersectmbo/cardano-config/cue/schema" + +node: schema.#Node & { + MinNodeVersion: "10.5.0" + Configuration: schema.mainnet & { + StorageConfig: LedgerDB: {Backend: "V2LSM", LSMExportPath: "lsm-export"} + } +} +``` + +### 3. Check, lint, export (one file) + +Each recipe takes a single config path (relative to `cue/`; a leading `cue/` is +stripped, so both forms work): + +```sh +just vet configs/my-node.cue # structure + hard rules (gRPC socket; mempool timeouts all-or-nothing) +just lint configs/my-node.cue # advisories: missing MinNodeVersion, V2LSM+Mithril without LSMExportPath, ... +just export configs/my-node.cue # write out/my-node.json (a single fully-inlined envelope) +just check configs/my-node.cue # vet + schema-validate (ajv) + lint, in one go +``` + +Raw `cue` equivalents, run from inside `cue/` (the module root, needed for +imports to resolve): + +```sh +cd cue +cue vet configs/my-node.cue -c # = just vet +cue export configs/my-node.cue -e node --out json # = just export +``` + +`just lint` has no one-liner equivalent: it unifies the config's `node` with +`schema.#warnings` and reads `.out`, which an inline `-e` can't reach (imports are +file-scoped), so the recipe injects a tiny helper file to do it. + +### 4. Validate the JSON against the published schema (ajv) + +```sh +just ajv configs/my-node.cue # exports it, then validates against schemas/config.schema.json +``` + +ajv checks structure only - not genesis hashes or the cross-field rules. A clean +ajv run does not guarantee the node will accept the config; only `resolve` does. + +Standalone (outside the repo, on an already-exported JSON), point ajv at the +published schema - the committed schemas declare it as their `$id`: + +```sh +curl -sO https://raw.githubusercontent.com/IntersectMBO/cardano-config/main/schemas/config.schema.json +ajv validate --spec=draft7 --strict=false -s config.schema.json -d my-node.json +``` + +### 5. Resolve and inspect + +```sh +cardano-config resolve --config my-node.json # fully-resolved YAML +cardano-config resolve --config my-node.json --with-geneses # also embed era genesis values +``` + +The genesis files referenced by the config must be reachable from its directory. +In-repo shortcut: `just resolve out/my-node.json`. + +## Maintaining + +`schema/gen_.cue` are generated from `../schemas/*.schema.json` and +`schema/gen_var_*.cue` from `../variants/`; never edit them by hand. After the +schemas or variants change, run `just regen` (with **CUE 0.17.0** - importer +output varies by version) and commit. + +`scripts/cue-ci.sh` is the CI gate: it fails on stale generated files, then runs +`just vet` / `just ajv` / `just lint` over every config and example. Run it +locally to check everything at once (the per-file `just` recipes check one file). diff --git a/cue/cue.mod/module.cue b/cue/cue.mod/module.cue new file mode 100644 index 0000000..8ecb23e --- /dev/null +++ b/cue/cue.mod/module.cue @@ -0,0 +1,4 @@ +module: "github.com/intersectmbo/cardano-config/cue" +language: { + version: "v0.17.0" +} diff --git a/cue/examples/mainnet-relay-example.cue b/cue/examples/mainnet-relay-example.cue new file mode 100644 index 0000000..9baa64a --- /dev/null +++ b/cue/examples/mainnet-relay-example.cue @@ -0,0 +1,29 @@ +// Example: a mainnet relay using the LSM ledger backend with Mithril snapshots. +// +// Authored as a #Node unified with the `mainnet` network preset (which supplies +// the mandatory genesis files/hashes through ProtocolConfig), so `cue vet` +// checks it against the schema and the cross-field constraints. Export with: +// +// cue export ./examples/mainnet-relay-example.cue -e node --out json +// +// (see the justfile `export` target), producing a single fully-inlined JSON +// document for the cardano-config library to ingest. +package main + +import "github.com/intersectmbo/cardano-config/cue/schema" + +node: schema.#Node & { + MinNodeVersion: "10.5.0" + + Configuration: schema.mainnet & { + StorageConfig: LedgerDB: { + Backend: "V2LSM" + LSMExportPath: "lsm-export" // set, so the LSM backend can export Mithril snapshots + } + + // A relay accepts inbound connections. The relay peer-target / PeerSharing + // role overlay is applied automatically by the library from the absence of + // block-forging credentials, so it is not inlined here. + NetworkConfig: DiffusionMode: "InitiatorAndResponder" + } +} diff --git a/cue/examples/preprod-blockproducer-example.cue b/cue/examples/preprod-blockproducer-example.cue new file mode 100644 index 0000000..5963020 --- /dev/null +++ b/cue/examples/preprod-blockproducer-example.cue @@ -0,0 +1,25 @@ +// Example: a preprod-testnet block producer using the in-memory ledger backend. +// +// The `preprod` preset supplies the preprod divergences (genesis files/hashes, +// GenesisMode consensus, the pinned snapshot interval). This config uses the +// V2InMemory backend - which keeps the Mithril snapshot policy valid without an +// LSMExportPath - and pins the block-producer role explicitly to show how. (You +// can omit the role: with block-forging credentials supplied on the CLI the +// library selects `blockProducerRole` automatically.) Export with: +// +// cue export ./examples/preprod-blockproducer-example.cue -e node --out json +package main + +import "github.com/intersectmbo/cardano-config/cue/schema" + +node: schema.#Node & { + MinNodeVersion: "10.5.0" + + Configuration: schema.preprod & { + StorageConfig: LedgerDB: Backend: "V2InMemory" + + NetworkConfig: schema.blockProducerRole & { + DiffusionMode: "InitiatorAndResponder" + } + } +} diff --git a/cue/examples/preview-relay-example.cue b/cue/examples/preview-relay-example.cue new file mode 100644 index 0000000..8b0ed03 --- /dev/null +++ b/cue/examples/preview-relay-example.cue @@ -0,0 +1,27 @@ +// Example: a preview-testnet relay using the LSM ledger backend. +// +// The `preview` preset supplies the preview divergences (genesis files/hashes, +// GenesisMode consensus, the pinned snapshot interval and the Shelley..Alonzo +// hard forks active from epoch 0); this config only adds the LSM backend and the +// relay diffusion mode. Export with: +// +// cue export ./examples/preview-relay-example.cue -e node --out json +package main + +import "github.com/intersectmbo/cardano-config/cue/schema" + +node: schema.#Node & { + MinNodeVersion: "10.5.0" + + Configuration: schema.preview & { + StorageConfig: LedgerDB: { + Backend: "V2LSM" + LSMExportPath: "lsm-export" // set, so the LSM backend can export Mithril snapshots + } + + // A relay accepts inbound connections; its peer-target / PeerSharing role + // overlay is applied automatically by the library (no block-forging + // credentials), so it is not inlined here. + NetworkConfig: DiffusionMode: "InitiatorAndResponder" + } +} diff --git a/cue/schema/constraints.cue b/cue/schema/constraints.cue new file mode 100644 index 0000000..eb99d40 --- /dev/null +++ b/cue/schema/constraints.cue @@ -0,0 +1,123 @@ +// Cross-field constraints that the JSON Schema cannot express. +// +// The generated definitions under cue.mod/gen/ (imported from +// ../schemas/*.schema.json) describe the *structure* of each component. They +// do NOT capture the consistency rules cardano-config enforces at load time. +// This file layers those rules on top, so `cue vet` rejects configurations the +// library would reject - with the sole exception of the genesis hash check, +// which requires reading and Blake2b-hashing the referenced files and so stays +// in the library as the final authority. +package schema + +// #GrpcConstraints: enabling the gRPC server requires a socket path to bind to, +// either its own GrpcSocketPath or the local-client SocketPath to derive from +// (the library enforces this as a hard error at resolution time). EnableGrpc is +// non-optional (CUE cannot branch on an optional field); its default mirrors the +// library's, so it only materialises when the section is present and then matches +// the default the library would apply anyway. +#GrpcConstraints: { + EnableGrpc: bool | *false + if EnableGrpc { + {SocketPath: string} | {GrpcSocketPath: string} + } + ... +} + +// #MempoolConstraints: the three mempool timeouts are coupled - set all three, +// or none (in which case the library applies 1 / 1.5 / 5 seconds). A partial +// set is rejected. An optional field constrained to bottom (`_|_`) means "must +// be absent"; the first disjunct (the all-absent case) is the default, so a +// MempoolConfig that sets other keys but no timeouts resolves to it; the second +// is the all-present case. A partial set satisfies neither and is rejected. +#MempoolConstraints: { + *{ + MempoolTimeoutSoft?: _|_ + MempoolTimeoutHard?: _|_ + MempoolTimeoutCapacity?: _|_ + } | { + MempoolTimeoutSoft!: number + MempoolTimeoutHard!: number + MempoolTimeoutCapacity!: number + } + ... +} + +// #Configuration is the set of component sections, each schema-generated +// definition unified with its constraints. ProtocolConfig is mandatory because +// it carries the genesis files/hashes, which have no defaults. This is the shape +// that goes under the envelope's `Configuration` key. +#Configuration: { + ProtocolConfig: #ProtocolConfig + + StorageConfig?: #StorageConfig + ConsensusConfig?: #ConsensusConfig + NetworkConfig?: #NetworkConfig + LocalConnectionsConfig?: #LocalConnectionsConfig & #GrpcConstraints + TestingConfig?: #TestingConfig + MempoolConfig?: #MempoolConfig & #MempoolConstraints + + // Tracing is consumed by trace-dispatcher, not validated here: the value is + // either a path to a separate file holding the tracing configuration, or that + // configuration object inline. Its shape is trace-dispatcher's to define, so + // we accept any object (`{...}`) without constraining its fields - matching the + // JSON Schema, which types HermodTracing as "a path or a JSON object". + HermodTracing?: string | {...} +} + +// #Node is the authoring view: the recommended Version1 envelope. `cue export` +// of a concrete #Node yields the `{ $schema, Version, MinNodeVersion, +// Configuration }` document the cardano-config library ingests. $schema (the URL +// of the published schema) and Version default, and Configuration is mandatory, +// so all three are emitted automatically; MinNodeVersion has no default and is +// flagged by #warnings when omitted. +#Node: { + "$schema": string | *"https://raw.githubusercontent.com/IntersectMBO/cardano-config/main/schemas/config.schema.json" + Version: int | *1 + MinNodeVersion?: string + Configuration: #Configuration +} + +// #warnings computes non-fatal advisories for a node - the soft counterpart to +// the hard constraints folded into #Node above. It mirrors cardano-config's +// own ConfigWarning channel: surfaced, never thrown. `just lint` evaluates +// `(schema.#warnings & {in: node}).out` and prints the list without failing. +// +// Each check defaults absent optional fields to their schema default (so e.g. +// an omitted Backend reads as "V2InMemory") and detects field presence with a +// struct comprehension, since CUE cannot branch on an optional field directly. +#warnings: { + in: #Node + + // Version1 envelope presence. (With #Node, $schema, Version and Configuration + // are always present - $schema and Version default, Configuration is required + // - so in practice this flags a missing MinNodeVersion; the others are checked + // for completeness and to catch a hand-built value that bypasses #Node.) + let _hasSchema = len([for k, _ in in if k == "$schema" {k}]) > 0 + let _hasVersion = len([for k, _ in in if k == "Version" {k}]) > 0 + let _hasMinNodeVersion = len([for k, _ in in if k == "MinNodeVersion" {k}]) > 0 + let _hasConfiguration = len([for k, _ in in if k == "Configuration" {k}]) > 0 + + // Components live under the envelope's Configuration key. + let _cfg = [for k, v in in if k == "Configuration" {v}, {}][0] + let _storage = [for k, v in _cfg if k == "StorageConfig" {v}, {}][0] + let _ldb = [for k, v in _storage if k == "LedgerDB" {v}, {}][0] + let _backend = [for k, v in _ldb if k == "Backend" {v}, "V2InMemory"][0] + let _snap = [for k, v in _ldb if k == "Snapshots" {v}, "Mithril"][0] + // Snapshots may be the string "Mithril" or an options object; narrow to a + // string (the struct & string mismatch is bottom, which the disjunction + // drops) so the guard below never compares a struct to a string. + let _snapStr = *(_snap & string) | "" + let _hasExport = len([for k, _ in _ldb if k == "LSMExportPath" {k}]) > 0 + + out: [ + // Not in the Version1 envelope form. + if !_hasSchema || !_hasVersion || !_hasMinNodeVersion || !_hasConfiguration { + "document is not in the Version1 envelope form: expected $schema, Version, MinNodeVersion and Configuration at the top level." + }, + // V2LSM + Mithril without an export path: allowed, but the LSM backend + // has nowhere to export snapshots. A warning, not an error. + if _backend == "V2LSM" if _snapStr == "Mithril" if !_hasExport { + "Configuration.StorageConfig.LedgerDB: backend V2LSM with Mithril snapshots but no LSMExportPath: the LSM backend has nowhere to export snapshots. Set LSMExportPath, or use the V2InMemory backend." + }, + ] +} diff --git a/cue/schema/gen_ConsensusConfig.cue b/cue/schema/gen_ConsensusConfig.cue new file mode 100644 index 0000000..75e2d03 --- /dev/null +++ b/cue/schema/gen_ConsensusConfig.cue @@ -0,0 +1,68 @@ +// ConsensusConfig +// +// ConsensusConfiguration +package schema + +#ConsensusConfig: { + @jsonschema(schema="http://json-schema.org/draft-07/schema#") + @jsonschema(id="https://raw.githubusercontent.com/IntersectMBO/cardano-config/main/schemas/ConsensusConfig.schema.json") + + // $schema + // + // URL of the JSON Schema this ConsensusConfig file follows (the $schema + // annotation), for editors and validators. + $schema?: string + + // ConsensusMode + // + // Which consensus mode to run (PraosMode or GenesisMode) + ConsensusMode?: "PraosMode" | "GenesisMode" + + // LowLevelGenesisOptions + // + // Low-level Genesis tuning (GenesisMode only) + // GenesisConfigFlags + LowLevelGenesisOptions?: { + // BlockFetchGracePeriod + // + // Grace period, in seconds, for BlockFetch + BlockFetchGracePeriod?: number + + // BucketCapacity + // + // Token bucket capacity for the LoP + BucketCapacity?: int + + // BucketRate + // + // Token bucket refill rate for the LoP + BucketRate?: int + + // CSJJumpSize + // + // Size, in slots, of ChainSync jumps + CSJJumpSize?: int & <=18446744073709551615.0 & >=0 + + // EnableCSJ + // + // Enable ChainSync Jumping + EnableCSJ?: bool + + // EnableLoEAndGDD + // + // Enable the Limit on Eagerness and the Genesis Density Disconnection + EnableLoEAndGDD?: bool + + // EnableLoP + // + // Enable the Limit on Patience + EnableLoP?: bool + + // GDDRateLimit + // + // Rate limit, in seconds, for the GDD + GDDRateLimit?: number + ... + } + ... +} diff --git a/cue/schema/gen_LocalConnectionsConfig.cue b/cue/schema/gen_LocalConnectionsConfig.cue new file mode 100644 index 0000000..68738eb --- /dev/null +++ b/cue/schema/gen_LocalConnectionsConfig.cue @@ -0,0 +1,31 @@ +// LocalConnectionsConfig +// +// LocalConnectionsConfig +package schema + +#LocalConnectionsConfig: { + @jsonschema(schema="http://json-schema.org/draft-07/schema#") + @jsonschema(id="https://raw.githubusercontent.com/IntersectMBO/cardano-config/main/schemas/LocalConnectionsConfig.schema.json") + + // $schema + // + // URL of the JSON Schema this LocalConnectionsConfig file follows (the $schema + // annotation), for editors and validators. + $schema?: string + + // EnableGrpc + // + // Whether to enable the gRPC server + EnableGrpc?: bool + + // GrpcSocketPath + // + // Path of the gRPC server socket + GrpcSocketPath?: string + + // SocketPath + // + // Path of the socket for local clients + SocketPath?: string + ... +} diff --git a/cue/schema/gen_MempoolConfig.cue b/cue/schema/gen_MempoolConfig.cue new file mode 100644 index 0000000..7e8d491 --- /dev/null +++ b/cue/schema/gen_MempoolConfig.cue @@ -0,0 +1,36 @@ +// MempoolConfig +// +// MempoolConfiguration +package schema + +#MempoolConfig: { + @jsonschema(schema="http://json-schema.org/draft-07/schema#") + @jsonschema(id="https://raw.githubusercontent.com/IntersectMBO/cardano-config/main/schemas/MempoolConfig.schema.json") + + // $schema + // + // URL of the JSON Schema this MempoolConfig file follows (the $schema + // annotation), for editors and validators. + $schema?: string + + // MempoolCapacityBytesOverride + // + // Override for the maximum mempool size in bytes, or the string "NoOverride" + MempoolCapacityBytesOverride?: matchN(>=1, [int & <=18446744073709551615.0 & >=0, "NoOverride"]) + + // MempoolTimeoutCapacity + // + // Capacity mempool timeout, in seconds + MempoolTimeoutCapacity?: number + + // MempoolTimeoutHard + // + // Hard mempool timeout, in seconds + MempoolTimeoutHard?: number + + // MempoolTimeoutSoft + // + // Soft mempool timeout, in seconds + MempoolTimeoutSoft?: number + ... +} diff --git a/cue/schema/gen_NetworkConfig.cue b/cue/schema/gen_NetworkConfig.cue new file mode 100644 index 0000000..f25d345 --- /dev/null +++ b/cue/schema/gen_NetworkConfig.cue @@ -0,0 +1,173 @@ +// NetworkConfig +// +// NetworkConfiguration +package schema + +#NetworkConfig: { + @jsonschema(schema="http://json-schema.org/draft-07/schema#") + @jsonschema(id="https://raw.githubusercontent.com/IntersectMBO/cardano-config/main/schemas/NetworkConfig.schema.json") + + // $schema + // + // URL of the JSON Schema this NetworkConfig file follows (the $schema + // annotation), for editors and validators. + $schema?: string + + // AcceptedConnectionsLimit + // + // Limits on accepted connections + // AcceptedConnectionsLimit + AcceptedConnectionsLimit?: { + // Delay + // + // Delay, in seconds, applied once the soft limit is reached + Delay!: number + + // HardLimit + // + // Hard limit on the number of connections + HardLimit!: int & <=4294967295 & >=0 + + // SoftLimit + // + // Soft limit on the number of connections + SoftLimit!: int & <=4294967295 & >=0 + ... + } + + // ChainSyncIdleTimeout + // + // ChainSync idle timeout, in seconds + ChainSyncIdleTimeout?: number + + // DeadlineTargetNumberOfActiveBigLedgerPeers + // + // Deadline target of active big ledger peers + DeadlineTargetNumberOfActiveBigLedgerPeers?: int & <=9223372036854775807 & >=-9223372036854775808 + + // DeadlineTargetNumberOfActivePeers + // + // Deadline target of active peers + DeadlineTargetNumberOfActivePeers?: int & <=9223372036854775807 & >=-9223372036854775808 + + // DeadlineTargetNumberOfEstablishedBigLedgerPeers + // + // Deadline target of established big ledger peers + DeadlineTargetNumberOfEstablishedBigLedgerPeers?: int & <=9223372036854775807 & >=-9223372036854775808 + + // DeadlineTargetNumberOfEstablishedPeers + // + // Deadline target of established peers + DeadlineTargetNumberOfEstablishedPeers?: int & <=9223372036854775807 & >=-9223372036854775808 + + // DeadlineTargetNumberOfKnownBigLedgerPeers + // + // Deadline target of known big ledger peers + DeadlineTargetNumberOfKnownBigLedgerPeers?: int & <=9223372036854775807 & >=-9223372036854775808 + + // DeadlineTargetNumberOfKnownPeers + // + // Deadline target of known peers + DeadlineTargetNumberOfKnownPeers?: int & <=9223372036854775807 & >=-9223372036854775808 + + // DeadlineTargetNumberOfRootPeers + // + // Deadline target of root peers + DeadlineTargetNumberOfRootPeers?: int & <=9223372036854775807 & >=-9223372036854775808 + + // DiffusionMode + // + // Initiator-only or initiator-and-responder + DiffusionMode?: "InitiatorOnly" | "InitiatorAndResponder" + + // EgressPollInterval + // + // Egress poll interval, in seconds + EgressPollInterval?: number + + // ExperimentalProtocolsEnabled + // + // Enable experimental network protocols + ExperimentalProtocolsEnabled?: bool + + // MaxConcurrencyBulkSync + // + // Bulk-sync block-fetch concurrency + MaxConcurrencyBulkSync?: int & <=18446744073709551615.0 & >=0 + + // MaxConcurrencyDeadline + // + // Deadline block-fetch concurrency + MaxConcurrencyDeadline?: int & <=18446744073709551615.0 & >=0 + + // MinBigLedgerPeersForTrustedState + // + // Minimum big ledger peers for trusted state + MinBigLedgerPeersForTrustedState?: int & <=9223372036854775807 & >=-9223372036854775808 + + // PeerSharing + // + // Whether to enable peer sharing + PeerSharing?: bool + + // ProtocolIdleTimeout + // + // Protocol idle timeout, in seconds + ProtocolIdleTimeout?: number + + // ResponderCoreAffinityPolicy + // + // Whether responders are pinned to a core + ResponderCoreAffinityPolicy?: "NoResponderCoreAffinity" | "ResponderCoreAffinity" + + // SyncTargetNumberOfActiveBigLedgerPeers + // + // Sync target of active big ledger peers + SyncTargetNumberOfActiveBigLedgerPeers?: int & <=9223372036854775807 & >=-9223372036854775808 + + // SyncTargetNumberOfActivePeers + // + // Sync target of active peers + SyncTargetNumberOfActivePeers?: int & <=9223372036854775807 & >=-9223372036854775808 + + // SyncTargetNumberOfEstablishedBigLedgerPeers + // + // Sync target of established big ledger peers + SyncTargetNumberOfEstablishedBigLedgerPeers?: int & <=9223372036854775807 & >=-9223372036854775808 + + // SyncTargetNumberOfEstablishedPeers + // + // Sync target of established peers + SyncTargetNumberOfEstablishedPeers?: int & <=9223372036854775807 & >=-9223372036854775808 + + // SyncTargetNumberOfKnownBigLedgerPeers + // + // Sync target of known big ledger peers + SyncTargetNumberOfKnownBigLedgerPeers?: int & <=9223372036854775807 & >=-9223372036854775808 + + // SyncTargetNumberOfKnownPeers + // + // Sync target of known peers + SyncTargetNumberOfKnownPeers?: int & <=9223372036854775807 & >=-9223372036854775808 + + // SyncTargetNumberOfRootPeers + // + // Sync target of root peers + SyncTargetNumberOfRootPeers?: int & <=9223372036854775807 & >=-9223372036854775808 + + // TimeWaitTimeout + // + // TIME-WAIT timeout, in seconds + TimeWaitTimeout?: number + + // TxSubmissionInitDelay + // + // Tx-submission initial delay, in seconds + TxSubmissionInitDelay?: number + + // TxSubmissionLogicVersion + // + // Which tx-submission inbound logic to run + TxSubmissionLogicVersion?: "TxSubmissionLogicV1" | "TxSubmissionLogicV2" + ... +} diff --git a/cue/schema/gen_ProtocolConfig.cue b/cue/schema/gen_ProtocolConfig.cue new file mode 100644 index 0000000..735a936 --- /dev/null +++ b/cue/schema/gen_ProtocolConfig.cue @@ -0,0 +1,84 @@ +// ProtocolConfig +// +// ProtocolConfiguration +package schema + +#ProtocolConfig: { + @jsonschema(schema="http://json-schema.org/draft-07/schema#") + @jsonschema(id="https://raw.githubusercontent.com/IntersectMBO/cardano-config/main/schemas/ProtocolConfig.schema.json") + + // $schema + // + // URL of the JSON Schema this ProtocolConfig file follows (the $schema + // annotation), for editors and validators. + $schema?: string + + // AlonzoGenesisFile + // + // Path to the genesis file + AlonzoGenesisFile!: string + + // AlonzoGenesisHash + // + // Hash of the genesis file + // Blake2b_256 hash + AlonzoGenesisHash!: string + + // ByronGenesisFile + // + // Path to the genesis file + ByronGenesisFile!: string + + // ByronGenesisHash + // + // Hash of the genesis file + // Blake2b_256 hash + ByronGenesisHash!: string + + // CheckpointsFile + // + // Path to the file + CheckpointsFile?: string + + // CheckpointsFileHash + // + // Hash of the file + // Blake2b_256 hash + CheckpointsFileHash?: string + + // ConwayGenesisFile + // + // Path to the genesis file + ConwayGenesisFile!: string + + // ConwayGenesisHash + // + // Hash of the genesis file + // Blake2b_256 hash + ConwayGenesisHash!: string + + // RequiresNetworkMagic + // + // Whether network magic is required + RequiresNetworkMagic?: "RequiresNoMagic" | "RequiresMagic" + + // ShelleyGenesisFile + // + // Path to the genesis file + ShelleyGenesisFile!: string + + // ShelleyGenesisHash + // + // Hash of the genesis file + // Blake2b_256 hash + ShelleyGenesisHash!: string + + // StartAsNonProducingNode + // + // Start the node without block production even when block-forging credentials + // are supplied. false (the default) behaves normally — producing blocks if + // credentials were supplied, otherwise just running as a relay; true + // suppresses block production even with credentials present. + StartAsNonProducingNode?: bool + ... +} diff --git a/cue/schema/gen_StorageConfig.cue b/cue/schema/gen_StorageConfig.cue new file mode 100644 index 0000000..32a37d6 --- /dev/null +++ b/cue/schema/gen_StorageConfig.cue @@ -0,0 +1,95 @@ +// StorageConfig +// +// StorageConfiguration +package schema + +#StorageConfig: { + @jsonschema(schema="http://json-schema.org/draft-07/schema#") + @jsonschema(id="https://raw.githubusercontent.com/IntersectMBO/cardano-config/main/schemas/StorageConfig.schema.json") + + // $schema + // + // URL of the JSON Schema this StorageConfig file follows (the $schema + // annotation), for editors and validators. + $schema?: string + + // DatabasePath + // + // Directory (or split directories) where the state is stored + DatabasePath?: matchN(>=1, [string, { + // ImmutablePath + // + // Directory for the immutable database + ImmutablePath!: string + + // VolatilePath + // + // Directory for the volatile database + VolatilePath!: string + ... + }]) + + // LedgerDB + // + // The LedgerDB configuration + // LedgerDB + LedgerDB?: { + // Backend + // + // Which LedgerDB backend to use (V2InMemory or V2LSM) + Backend?: "V2InMemory" | "V2LSM" + + // LSMDatabasePath + // + // Custom path to the LSM database (V2LSM only) + LSMDatabasePath?: string + + // LSMExportPath + // + // Directory into which the LSM backend exports snapshots (V2LSM only) + LSMExportPath?: string + + // QueryBatchSize + // + // Chunk size for large backend reads + QueryBatchSize?: int & <=18446744073709551615.0 & >=0 + + // Snapshots + // + // Snapshot policy: "Mithril" or an object of snapshot options + Snapshots?: matchN(>=1, ["Mithril", { + // MaxDelay + // + // Upper bound (seconds) of the random snapshot delay + MaxDelay?: int & <=18446744073709551615.0 & >=0 + + // MinDelay + // + // Lower bound (seconds) of the random snapshot delay + MinDelay?: int & <=18446744073709551615.0 & >=0 + + // NumOfDiskSnapshots + // + // How many snapshots to keep on disk + NumOfDiskSnapshots?: int & <=18446744073709551615.0 & >=0 + + // RateLimit + // + // Minimum seconds between snapshots + RateLimit?: int & <=18446744073709551615.0 & >=0 + + // SlotOffset + // + // Slot at which the snapshot schedule is anchored + SlotOffset?: int & <=18446744073709551615.0 & >=0 + + // SnapshotInterval + // + // Slots between snapshots (non-zero) + SnapshotInterval?: int & <=18446744073709551615.0 & >=0 + ... + }]) + ... + } + ... +} diff --git a/cue/schema/gen_TestingConfig.cue b/cue/schema/gen_TestingConfig.cue new file mode 100644 index 0000000..ccc504c --- /dev/null +++ b/cue/schema/gen_TestingConfig.cue @@ -0,0 +1,102 @@ +// TestingConfig +// +// TestingConfiguration +package schema + +#TestingConfig: { + @jsonschema(schema="http://json-schema.org/draft-07/schema#") + @jsonschema(id="https://raw.githubusercontent.com/IntersectMBO/cardano-config/main/schemas/TestingConfig.schema.json") + + // $schema + // + // URL of the JSON Schema this TestingConfig file follows (the $schema + // annotation), for editors and validators. + $schema?: string + + // DijkstraGenesisFile + // + // Path to the genesis file + DijkstraGenesisFile?: string + + // DijkstraGenesisHash + // + // Hash of the genesis file + // Blake2b_256 hash + DijkstraGenesisHash?: string + + // ExperimentalHardForksEnabled + // + // Enable the experimental eras + ExperimentalHardForksEnabled?: bool + + // TestAllegraHardForkAtEpoch + // + // Force the Allegra hard fork at this epoch + TestAllegraHardForkAtEpoch?: int & <=18446744073709551615.0 & >=0 + + // TestAllegraHardForkAtVersion + // + // Force the Allegra hard fork at this protocol version + TestAllegraHardForkAtVersion?: int & <=18446744073709551615.0 & >=0 + + // TestAlonzoHardForkAtEpoch + // + // Force the Alonzo hard fork at this epoch + TestAlonzoHardForkAtEpoch?: int & <=18446744073709551615.0 & >=0 + + // TestAlonzoHardForkAtVersion + // + // Force the Alonzo hard fork at this protocol version + TestAlonzoHardForkAtVersion?: int & <=18446744073709551615.0 & >=0 + + // TestBabbageHardForkAtEpoch + // + // Force the Babbage hard fork at this epoch + TestBabbageHardForkAtEpoch?: int & <=18446744073709551615.0 & >=0 + + // TestBabbageHardForkAtVersion + // + // Force the Babbage hard fork at this protocol version + TestBabbageHardForkAtVersion?: int & <=18446744073709551615.0 & >=0 + + // TestConwayHardForkAtEpoch + // + // Force the Conway hard fork at this epoch + TestConwayHardForkAtEpoch?: int & <=18446744073709551615.0 & >=0 + + // TestConwayHardForkAtVersion + // + // Force the Conway hard fork at this protocol version + TestConwayHardForkAtVersion?: int & <=18446744073709551615.0 & >=0 + + // TestDijkstraHardForkAtEpoch + // + // Force the Dijkstra hard fork at this epoch + TestDijkstraHardForkAtEpoch?: int & <=18446744073709551615.0 & >=0 + + // TestDijkstraHardForkAtVersion + // + // Force the Dijkstra hard fork at this protocol version + TestDijkstraHardForkAtVersion?: int & <=18446744073709551615.0 & >=0 + + // TestMaryHardForkAtEpoch + // + // Force the Mary hard fork at this epoch + TestMaryHardForkAtEpoch?: int & <=18446744073709551615.0 & >=0 + + // TestMaryHardForkAtVersion + // + // Force the Mary hard fork at this protocol version + TestMaryHardForkAtVersion?: int & <=18446744073709551615.0 & >=0 + + // TestShelleyHardForkAtEpoch + // + // Force the Shelley hard fork at this epoch + TestShelleyHardForkAtEpoch?: int & <=18446744073709551615.0 & >=0 + + // TestShelleyHardForkAtVersion + // + // Force the Shelley hard fork at this protocol version + TestShelleyHardForkAtVersion?: int & <=18446744073709551615.0 & >=0 + ... +} diff --git a/cue/schema/gen_var_consensusPreprod.cue b/cue/schema/gen_var_consensusPreprod.cue new file mode 100644 index 0000000..3010a26 --- /dev/null +++ b/cue/schema/gen_var_consensusPreprod.cue @@ -0,0 +1,6 @@ +package schema + +consensusPreprod: { + $schema: "https://raw.githubusercontent.com/IntersectMBO/cardano-config/main/schemas/ConsensusConfig.schema.json" + ConsensusMode: "GenesisMode" +} diff --git a/cue/schema/gen_var_consensusPreview.cue b/cue/schema/gen_var_consensusPreview.cue new file mode 100644 index 0000000..87c4867 --- /dev/null +++ b/cue/schema/gen_var_consensusPreview.cue @@ -0,0 +1,6 @@ +package schema + +consensusPreview: { + $schema: "https://raw.githubusercontent.com/IntersectMBO/cardano-config/main/schemas/ConsensusConfig.schema.json" + ConsensusMode: "GenesisMode" +} diff --git a/cue/schema/gen_var_networkBlockproducer.cue b/cue/schema/gen_var_networkBlockproducer.cue new file mode 100644 index 0000000..3e8748f --- /dev/null +++ b/cue/schema/gen_var_networkBlockproducer.cue @@ -0,0 +1,13 @@ +package schema + +networkBlockproducer: { + $schema: "https://raw.githubusercontent.com/IntersectMBO/cardano-config/main/schemas/NetworkConfig.schema.json" + DeadlineTargetNumberOfRootPeers: 100 + DeadlineTargetNumberOfKnownPeers: 100 + DeadlineTargetNumberOfEstablishedPeers: 30 + DeadlineTargetNumberOfActivePeers: 20 + DeadlineTargetNumberOfKnownBigLedgerPeers: 15 + DeadlineTargetNumberOfEstablishedBigLedgerPeers: 10 + DeadlineTargetNumberOfActiveBigLedgerPeers: 5 + PeerSharing: false +} diff --git a/cue/schema/gen_var_networkRelay.cue b/cue/schema/gen_var_networkRelay.cue new file mode 100644 index 0000000..d6e48bc --- /dev/null +++ b/cue/schema/gen_var_networkRelay.cue @@ -0,0 +1,13 @@ +package schema + +networkRelay: { + $schema: "https://raw.githubusercontent.com/IntersectMBO/cardano-config/main/schemas/NetworkConfig.schema.json" + DeadlineTargetNumberOfRootPeers: 60 + DeadlineTargetNumberOfKnownPeers: 150 + DeadlineTargetNumberOfEstablishedPeers: 30 + DeadlineTargetNumberOfActivePeers: 20 + DeadlineTargetNumberOfKnownBigLedgerPeers: 15 + DeadlineTargetNumberOfEstablishedBigLedgerPeers: 10 + DeadlineTargetNumberOfActiveBigLedgerPeers: 5 + PeerSharing: true +} diff --git a/cue/schema/gen_var_protocolMainnet.cue b/cue/schema/gen_var_protocolMainnet.cue new file mode 100644 index 0000000..e548223 --- /dev/null +++ b/cue/schema/gen_var_protocolMainnet.cue @@ -0,0 +1,16 @@ +package schema + +protocolMainnet: { + $schema: "https://raw.githubusercontent.com/IntersectMBO/cardano-config/main/schemas/ProtocolConfig.schema.json" + RequiresNetworkMagic: "RequiresNoMagic" + ByronGenesisFile: "mainnet-byron-genesis.json" + ByronGenesisHash: "5f20df933584822601f9e3f8c024eb5eb252fe8cefb24d1317dc3d432e940ebb" + ShelleyGenesisFile: "mainnet-shelley-genesis.json" + ShelleyGenesisHash: "1a3be38bcbb7911969283716ad7aa550250226b76a61fc51cc9a9a35d9276d81" + AlonzoGenesisFile: "mainnet-alonzo-genesis.json" + AlonzoGenesisHash: "7e94a15f55d1e82d10f09203fa1d40f8eede58fd8066542cf6566008068ed874" + ConwayGenesisFile: "mainnet-conway-genesis.json" + ConwayGenesisHash: "15a199f895e461ec0ffc6dd4e4028af28a492ab4e806d39cb674c88f7643ef62" + CheckpointsFile: "mainnet-checkpoints.json" + CheckpointsFileHash: "3e6dee5bae7acc6d870187e72674b37c929be8c66e62a552cf6a876b1af31ade" +} diff --git a/cue/schema/gen_var_protocolPreprod.cue b/cue/schema/gen_var_protocolPreprod.cue new file mode 100644 index 0000000..ef5776a --- /dev/null +++ b/cue/schema/gen_var_protocolPreprod.cue @@ -0,0 +1,14 @@ +package schema + +protocolPreprod: { + $schema: "https://raw.githubusercontent.com/IntersectMBO/cardano-config/main/schemas/ProtocolConfig.schema.json" + RequiresNetworkMagic: "RequiresMagic" + ByronGenesisFile: "byron-genesis.json" + ByronGenesisHash: "d4b8de7a11d929a323373cbab6c1a9bdc931beffff11db111cf9d57356ee1937" + ShelleyGenesisFile: "shelley-genesis.json" + ShelleyGenesisHash: "162d29c4e1cf6b8a84f2d692e67a3ac6bc7851bc3e6e4afe64d15778bed8bd86" + AlonzoGenesisFile: "alonzo-genesis.json" + AlonzoGenesisHash: "7e94a15f55d1e82d10f09203fa1d40f8eede58fd8066542cf6566008068ed874" + ConwayGenesisFile: "conway-genesis.json" + ConwayGenesisHash: "0eb6adaec3fcb1fe286c1b4ae0da2a117eafc3add51e17577d36dd39eddfc3db" +} diff --git a/cue/schema/gen_var_protocolPreview.cue b/cue/schema/gen_var_protocolPreview.cue new file mode 100644 index 0000000..bc5a4db --- /dev/null +++ b/cue/schema/gen_var_protocolPreview.cue @@ -0,0 +1,16 @@ +package schema + +protocolPreview: { + $schema: "https://raw.githubusercontent.com/IntersectMBO/cardano-config/main/schemas/ProtocolConfig.schema.json" + RequiresNetworkMagic: "RequiresMagic" + ByronGenesisFile: "byron-genesis.json" + ByronGenesisHash: "83de1d7302569ad56cf9139a41e2e11346d4cb4a31c00142557b6ab3fa550761" + ShelleyGenesisFile: "shelley-genesis.json" + ShelleyGenesisHash: "363498d1024f84bb39d3fa9593ce391483cb40d479b87233f868d6e57c3a400d" + AlonzoGenesisFile: "alonzo-genesis.json" + AlonzoGenesisHash: "7e94a15f55d1e82d10f09203fa1d40f8eede58fd8066542cf6566008068ed874" + ConwayGenesisFile: "conway-genesis.json" + ConwayGenesisHash: "9cc5084f02e27210eacba47af0872e3dba8946ad9460b6072d793e1d2f3987ef" + CheckpointsFile: "checkpoints.json" + CheckpointsFileHash: "bb5056ff1ced9d68dd99720695789664f6bf6f0cb02a4010df09b813e225ac51" +} diff --git a/cue/schema/gen_var_storagePreprod.cue b/cue/schema/gen_var_storagePreprod.cue new file mode 100644 index 0000000..f5a7382 --- /dev/null +++ b/cue/schema/gen_var_storagePreprod.cue @@ -0,0 +1,13 @@ +package schema + +storagePreprod: { + $schema: "https://raw.githubusercontent.com/IntersectMBO/cardano-config/main/schemas/StorageConfig.schema.json" + LedgerDB: Snapshots: { + SnapshotInterval: 4320 + SlotOffset: 0 + RateLimit: 600 + MinDelay: 300 + MaxDelay: 600 + NumOfDiskSnapshots: 2 + } +} diff --git a/cue/schema/gen_var_storagePreview.cue b/cue/schema/gen_var_storagePreview.cue new file mode 100644 index 0000000..2c19158 --- /dev/null +++ b/cue/schema/gen_var_storagePreview.cue @@ -0,0 +1,13 @@ +package schema + +storagePreview: { + $schema: "https://raw.githubusercontent.com/IntersectMBO/cardano-config/main/schemas/StorageConfig.schema.json" + LedgerDB: Snapshots: { + SnapshotInterval: 864 + SlotOffset: 0 + RateLimit: 600 + MinDelay: 300 + MaxDelay: 600 + NumOfDiskSnapshots: 2 + } +} diff --git a/cue/schema/gen_var_testingPreview.cue b/cue/schema/gen_var_testingPreview.cue new file mode 100644 index 0000000..5ac8d03 --- /dev/null +++ b/cue/schema/gen_var_testingPreview.cue @@ -0,0 +1,9 @@ +package schema + +testingPreview: { + $schema: "https://raw.githubusercontent.com/IntersectMBO/cardano-config/main/schemas/TestingConfig.schema.json" + TestShelleyHardForkAtEpoch: 0 + TestAllegraHardForkAtEpoch: 0 + TestMaryHardForkAtEpoch: 0 + TestAlonzoHardForkAtEpoch: 0 +} diff --git a/cue/schema/presets.cue b/cue/schema/presets.cue new file mode 100644 index 0000000..4ff0b67 --- /dev/null +++ b/cue/schema/presets.cue @@ -0,0 +1,51 @@ +// Network and role presets, mirroring variants//*.json. +// +// The raw per-component overlays are imported from those JSON files as the +// open `gen_var_*` values (see `just regen`); this file bundles them into the +// per-network and per-role presets an authored config unifies in. They are +// kept open (no `#`) so unifying one into `#Node` only *fills* fields - `#Node` +// stays the single closed gate that rejects unknown keys. +// +// A network diverges across several components, so selecting one means pulling +// in its overlay in each affected section: +// +// node: #Node & preview & { ...your overrides... } +// +// Genesis files/hashes have no base default, so every network preset supplies +// them through ProtocolConfig. +package schema + +// mainnet: only ProtocolConfig diverges from the base defaults (PraosMode, +// Mithril snapshots and no hard-fork-at-epoch overrides are already the base). +mainnet: { + ProtocolConfig: protocolMainnet +} + +// preview: ProtocolConfig + GenesisMode consensus + pinned snapshot interval + +// the Shelley..Alonzo hard forks active from epoch 0. +preview: { + ProtocolConfig: protocolPreview + ConsensusConfig: consensusPreview + StorageConfig: storagePreview + TestingConfig: testingPreview +} + +// preprod: ProtocolConfig + GenesisMode consensus + pinned snapshot interval +// (no Testing overlay - preprod launched without pre-activated eras). +preprod: { + ProtocolConfig: protocolPreprod + ConsensusConfig: consensusPreprod + StorageConfig: storagePreprod +} + +// Role overlays for NetworkConfig (deadline peer targets + PeerSharing). +// +// NOTE: cardano-config applies one of these AUTOMATICALLY at resolution time, +// chosen by whether block-forging credentials were supplied, as a layer below +// the configuration file. Inlining one here is therefore optional and, because +// the file layer wins, OVERRIDES that automatic selection - only do so to pin a +// role explicitly. Usage: +// +// node: #Node & mainnet & { NetworkConfig: relayRole } +relayRole: networkRelay +blockProducerRole: networkBlockproducer diff --git a/defaults/HermodTracing.json b/defaults/HermodTracing.json new file mode 100644 index 0000000..c6b34d1 --- /dev/null +++ b/defaults/HermodTracing.json @@ -0,0 +1,79 @@ +{ + "ApplicationName": null, + "Forwarder": null, + "MetricsPrefix": "cardano.node.metrics.", + "Options": { + "": { + "backends": [ + "Stdout MachineFormat", + "EKGBackend" + ], + "detail": "DNormal", + "severity": "Notice" + }, + "BlockFetch.Client.CompletedBlockFetch": { + "maxFrequency": 2 + }, + "BlockFetch.Decision": { + "severity": "Silence" + }, + "ChainDB": { + "severity": "Info" + }, + "ChainDB.AddBlockEvent.AddBlockValidation": { + "severity": "Silence" + }, + "ChainDB.AddBlockEvent.AddBlockValidation.ValidCandidate": { + "maxFrequency": 2 + }, + "ChainDB.AddBlockEvent.AddedBlockToQueue": { + "maxFrequency": 2 + }, + "ChainDB.AddBlockEvent.AddedBlockToVolatileDB": { + "maxFrequency": 2 + }, + "ChainDB.CopyToImmutableDBEvent.CopiedBlockToImmutableDB": { + "maxFrequency": 2 + }, + "ChainSync.Client": { + "severity": "Warning" + }, + "Forge.Loop": { + "severity": "Info" + }, + "Forge.StateInfo": { + "severity": "Info" + }, + "LedgerMetrics": { + "severity": "Silence" + }, + "Mempool": { + "severity": "Info" + }, + "Net.ConnectionManager.Remote": { + "severity": "Info" + }, + "Net.ErrorPolicy": { + "severity": "Info" + }, + "Net.InboundGovernor": { + "severity": "Warning" + }, + "Net.InboundGovernor.Remote": { + "severity": "Info" + }, + "Net.Mux.Remote": { + "severity": "Info" + }, + "Net.PeerSelection": { + "severity": "Silence" + }, + "Resources": { + "severity": "Silence" + }, + "Startup.DiffusionInit": { + "severity": "Info" + } + }, + "PrometheusSimpleRun": null +} diff --git a/justfile b/justfile new file mode 100644 index 0000000..2b2f167 --- /dev/null +++ b/justfile @@ -0,0 +1,110 @@ +# Developer tasks for cardano-config. +# +# The CUE front-end lives in cue/; ajv and the resolve check are repo-level, so +# the whole config flow is driven from here. Run recipes from the repo root. +# Per-file recipes take a path relative to cue/ (a leading `cue/` is stripped, so +# `just vet examples/x.cue` and `just vet cue/examples/x.cue` both work). +# CUE/ajv binaries can be overridden, e.g. `CUE=~/go/bin/cue just vet ...`. + +cue := env('CUE', 'cue') +ajv := env('AJV', 'ajv') + +# Component schemas imported as #Definitions. +schemas := "StorageConfig ConsensusConfig ProtocolConfig NetworkConfig LocalConnectionsConfig TestingConfig MempoolConfig" + +# Show the recipe list. +default: + @just --list + +# Regenerate the committed generated CUE from schemas/ and variants/. +regen: + #!/usr/bin/env bash + set -euo pipefail + cd "{{ justfile_directory() }}/cue" + # Component definitions from the published JSON Schemas. + for c in {{ schemas }}; do + echo "import schema $c" + {{ cue }} import jsonschema -p schema -l "#$c:" ../schemas/$c.schema.json -o schema/gen_$c.cue -f + done + # Network/role variant data, as open values for presets.cue. Paths are + # repo-root-relative: the per-network overlays live under variants/, but the + # NetworkConfig role overlays live under defaults/NetworkConfig/. + while read -r name path; do + [ -z "$name" ] && continue + echo "import variant $name" + {{ cue }} import "../$path" -p schema -l "$name:" -o "schema/gen_var_$name.cue" -f + done <<'VARIANTS' + protocolMainnet variants/ProtocolConfig/mainnet.json + protocolPreview variants/ProtocolConfig/preview.json + protocolPreprod variants/ProtocolConfig/preprod.json + consensusPreview variants/ConsensusConfig/preview.json + consensusPreprod variants/ConsensusConfig/preprod.json + storagePreview variants/StorageConfig/preview.json + storagePreprod variants/StorageConfig/preprod.json + testingPreview variants/TestingConfig/preview.json + networkRelay defaults/NetworkConfig/relay.json + networkBlockproducer defaults/NetworkConfig/blockproducer.json + VARIANTS + +# Type-check one config/example (structure + hard rules). +vet FILE: + #!/usr/bin/env bash + set -euo pipefail + cd "{{ justfile_directory() }}/cue" + f="{{ FILE }}"; f="${f#cue/}" + {{ cue }} vet "$f" -c + +# Print non-fatal advisories (warnings) for one config. +lint FILE: + #!/usr/bin/env bash + set -euo pipefail + # Exits 0 with warnings; exits 1 only on a warnings-eval error (a bug in + # #warnings, or a malformed config). + cd "{{ justfile_directory() }}/cue" + f="{{ FILE }}"; f="${f#cue/}"; name=$(basename "$f" .cue) + helper=$(mktemp --suffix=.cue); err=$(mktemp) + trap 'rm -f "$helper" "$err"' EXIT + printf 'package main\nimport "github.com/intersectmbo/cardano-config/cue/schema"\nwarnings: (schema.#warnings & {in: node}).out\n' > "$helper" + if out=$({{ cue }} export "$f" "$helper" -e warnings --out json 2>"$err"); then + printf '%s' "$out" | python3 -c 'import json,sys; ws=json.loads(sys.stdin.read() or "[]"); n=sys.argv[1]; print(f"{n}: ok") if not ws else [print(f"{n}: warning: {w}") for w in ws]' "$name" + else + echo "$name: ERROR evaluating warnings:" >&2; sed 's/^/ /' "$err" >&2; exit 1 + fi + +# Emit a single fully-inlined out/.json for one config. +export FILE: + #!/usr/bin/env bash + set -euo pipefail + cd "{{ justfile_directory() }}/cue" + f="{{ FILE }}"; f="${f#cue/}"; name=$(basename "$f" .cue) + mkdir -p ../out + {{ cue }} export "$f" -e node --out json > "../out/$name.json" + echo "wrote out/$name.json" + +# Validate one config against the published config.schema.json (exports it first). +ajv FILE: + #!/usr/bin/env bash + set -euo pipefail + cd "{{ justfile_directory() }}" + just export "{{ FILE }}" >/dev/null + f="{{ FILE }}"; f="${f#cue/}"; name=$(basename "$f" .cue) + # ("unknown format path" is ajv noting a non-standard format it ignores.) + {{ ajv }} validate --spec=draft7 --strict=false -s schemas/config.schema.json -d "out/$name.json" \ + 2> >(grep -v 'unknown format "path"' >&2) + echo "$name: schema-valid" + +# Fully check one config: vet, then schema-validate, then lint. +check FILE: + #!/usr/bin/env bash + set -euo pipefail + just vet "{{ FILE }}" + just ajv "{{ FILE }}" + just lint "{{ FILE }}" + +# Resolve an exported JSON through the library (genesis files must be reachable from its dir). +resolve config: + cabal run -v0 cardano-config -- resolve --config {{ config }} + +# Remove build output. +clean: + rm -rf {{ justfile_directory() }}/out diff --git a/schemas/config.legacy-one-file.schema.json b/schemas/config.legacy-one-file.schema.json index 0daeaa2..81eba19 100644 --- a/schemas/config.legacy-one-file.schema.json +++ b/schemas/config.legacy-one-file.schema.json @@ -236,10 +236,20 @@ "type": "string" }, "HermodTracing": { - "description": "Tracing configuration as a path to a separate file holding it. Consumed by the node tracing system (trace-dispatcher), not parsed or validated by cardano-config.", - "format": "path", - "title": "HermodTracing", - "type": "string" + "anyOf": [ + { + "format": "path", + "title": "String", + "type": "string" + }, + { + "additionalProperties": {}, + "title": "Object", + "type": "object" + } + ], + "description": "Tracing configuration, given as a path to a separate file holding it or as that configuration object inline. Consumed by the node tracing system (trace-dispatcher), which owns its schema; not described further by cardano-config.", + "title": "HermodTracing" }, "LedgerDB": { "description": "The LedgerDB configuration\nLedgerDB", diff --git a/schemas/config.schema.json b/schemas/config.schema.json index 48fc9cb..750de8b 100644 --- a/schemas/config.schema.json +++ b/schemas/config.schema.json @@ -97,10 +97,20 @@ "title": "ConsensusConfig" }, "HermodTracing": { - "description": "Tracing configuration as a path to a separate file holding it. Consumed by the node tracing system (trace-dispatcher), not parsed or validated by cardano-config.", - "format": "path", - "title": "HermodTracing", - "type": "string" + "anyOf": [ + { + "format": "path", + "title": "String", + "type": "string" + }, + { + "additionalProperties": {}, + "title": "Object", + "type": "object" + } + ], + "description": "Tracing configuration, given as a path to a separate file holding it or as that configuration object inline. Consumed by the node tracing system (trace-dispatcher), which owns its schema; not described further by cardano-config.", + "title": "HermodTracing" }, "LocalConnectionsConfig": { "anyOf": [ diff --git a/scripts/cue-ci.sh b/scripts/cue-ci.sh new file mode 100755 index 0000000..1a21f6b --- /dev/null +++ b/scripts/cue-ci.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# CI checks for the CUE front-end (the per-file `just` recipes operate on one +# file; this runs them across everything, plus the generated-file drift gate). +# +# Run from anywhere. Honours $CUE / $AJV like the justfile. +set -euo pipefail + +cd "$(dirname "$0")/.." + +cue=${CUE:-cue} + +# 1. The committed generated CUE must match schemas/ + variants/. +just regen +if ! git diff --exit-code -- cue/schema/gen_*.cue; then + echo "cue/schema/gen_*.cue is stale: run 'just regen' and commit" >&2 + exit 1 +fi + +# 2. The schema package itself type-checks. +(cd cue && "$cue" vet ./schema) + +# 3. Every config and example vets, schema-validates, and lints. +shopt -s nullglob +for f in cue/examples/*.cue cue/configs/*.cue; do + echo "== $f ==" + just vet "$f" + just ajv "$f" + just lint "$f" +done + +echo "cue-ci: OK" diff --git a/src/Cardano/Configuration.hs b/src/Cardano/Configuration.hs index 623a138..79f765b 100644 --- a/src/Cardano/Configuration.hs +++ b/src/Cardano/Configuration.hs @@ -86,6 +86,8 @@ module Cardano.Configuration -- * Configuration file , File.NodeConfigurationFromFile , File.TracingConfiguration (..) + , File.TracingConfigSource (..) + , File.TraceConfig , File.parseConfigurationFiles , File.ConfigWarning (..) , File.renderConfigWarning @@ -123,6 +125,12 @@ data NodeConfiguration = NodeConfiguration , localConnectionsConfig :: File.LocalConnectionsConfig Identity , testingConfiguration :: File.TestingConfiguration Identity , mempoolConfiguration :: File.MempoolConfiguration Identity + , tracingConfiguration :: File.TraceConfig + -- ^ The tracing configuration resolved from the top-level @HermodTracing@ key + -- by @trace-dispatcher@'s parser (see 'File.resolveTracingConfiguration'), or + -- 'File.defaultCardanoTracingConfig' when no @HermodTracing@ key is present. + -- Carried through unchanged from the file-parse result so consumers get the + -- parsed 'File.TraceConfig'. , byronGenesisConfig :: ByronGenesisConfig -- ^ The parsed Byron genesis. , shelleyGenesisConfig :: ShelleyGenesis @@ -302,6 +310,7 @@ resolveConfigurationWith checks cli file = do , localConnectionsConfig = localConnections , testingConfiguration = testing , mempoolConfiguration = mempool + , tracingConfiguration = File.tracingConfiguration file , byronGenesisConfig = File.byronGenesisConfig file , shelleyGenesisConfig = File.shelleyGenesisConfig file , alonzoGenesisConfig = File.alonzoGenesisConfig file diff --git a/src/Cardano/Configuration/File.hs b/src/Cardano/Configuration/File.hs index 1774a5d..e965ee3 100644 --- a/src/Cardano/Configuration/File.hs +++ b/src/Cardano/Configuration/File.hs @@ -31,6 +31,9 @@ module Cardano.Configuration.File , TestingConfiguration (..) , MempoolConfiguration (..) , TracingConfiguration (..) + , TracingConfigSource (..) + , TraceConfig + , defaultCardanoTracingConfig -- * Resolving components , finalizeNetwork @@ -51,8 +54,8 @@ import Cardano.Configuration.File.Consensus import Cardano.Configuration.File.Error (ConfigurationParsingError (..)) import Cardano.Configuration.File.Lint ( ConfigWarning (..) - , checkEnvelope , configWarnings + , inVersion1Format , renderConfigWarning ) import Cardano.Configuration.File.Mempool @@ -64,11 +67,17 @@ import Cardano.Configuration.File.Merge , sectionUserLayer , splitEnvelope ) +import Cardano.Configuration.File.Migrate (migrate) import Cardano.Configuration.File.Network import Cardano.Configuration.File.Protocol import Cardano.Configuration.File.Storage import Cardano.Configuration.File.Testing import Cardano.Configuration.File.Tracing + ( TracingConfigSource (..) + , TracingConfiguration (..) + , defaultCardanoTracingConfig + , resolveTracingConfiguration + ) import Cardano.Configuration.Genesis ( GenesisReadError , genesisErrorFile @@ -83,6 +92,7 @@ import Cardano.Ledger.BaseTypes (StrictMaybe (..), maybeToStrictMaybe, strictMay import Cardano.Ledger.Conway.Genesis (ConwayGenesis) import Cardano.Ledger.Dijkstra.Genesis (DijkstraGenesis) import Cardano.Ledger.Shelley.Genesis (ShelleyGenesis) +import Cardano.Logging.Types (TraceConfig) import Control.Exception (throwIO) import Data.Aeson (FromJSON, Value) import qualified Data.Aeson.Key as K @@ -126,10 +136,14 @@ data NodeConfigurationFromFileF f , localConnectionsConfig :: f (LocalConnectionsConfig StrictMaybe) , testingConfiguration :: f (TestingConfiguration StrictMaybe) , mempoolConfiguration :: f (MempoolConfiguration StrictMaybe) - , tracingConfiguration :: TracingConfiguration - -- ^ Tracing keys, captured opaquely; see 'TracingConfiguration'. Unlike the - -- other components this is never read from a sub-file: the node's tracing - -- system resolves its own @HermodTracing@ file indirection. + , tracingConfiguration :: TraceConfig + -- ^ The tracing configuration referenced by the top-level @HermodTracing@ key, + -- resolved by @trace-dispatcher@'s own parser ('resolveTracingConfiguration'): + -- a @HermodTracing@ file path is read from that file, an inline object is read + -- directly. When no @HermodTracing@ key is present it falls back to + -- 'defaultCardanoTracingConfig', so a tracing configuration is always present. + -- Its schema is owned by @trace-dispatcher@, not described here (see + -- 'TracingConfiguration'). , byronGenesisConfig :: ByronGenesisConfig -- ^ The parsed Byron genesis (read from the @ByronGenesisFile@). , shelleyGenesisConfig :: ShelleyGenesis @@ -171,11 +185,19 @@ componentDefaults = parseConfigurationFiles :: HasCallStack => FilePath -> IO (NodeConfigurationFromFile, [ConfigWarning]) parseConfigurationFiles cfgFile = do - mainValue <- decodeValueFile Nothing cfgFile + rawValue <- decodeValueFile Nothing cfgFile + -- The parser accepts only the Version1 format (a top-level @Configuration@ + -- envelope). A document that is not in it is not parsed as-is: it is migrated + -- to the Version1 format first — with a 'MigratedToVersion1' warning — and the + -- result is parsed. If that migrated document still cannot be parsed, the parse + -- error surfaces as usual (so a non-Version1 document whose migration does not + -- yield a parseable configuration is rejected). + let (mainValue, migrationWarnings) = + if inVersion1Format rawValue + then (rawValue, []) + else (migrate rawValue, [MigratedToVersion1]) (version, minNodeVer, configValue) <- splitEnvelope mainValue - -- 'checkEnvelope' inspects the raw top-level document (is it the Version1 - -- envelope?); the rest inspect the unwrapped configuration object. - let warnings = checkEnvelope mainValue <> configWarnings configValue + let warnings = migrationWarnings <> configWarnings configValue root = takeDirectory cfgFile config <- case version of 1 -> parseConfigurationVersion1 root minNodeVer configValue @@ -210,7 +232,11 @@ parseConfigurationVersion1 root minNodeVer configValue = do localConnections <- parseSection root configValue "LocalConnectionsConfig" testing <- parseSection root configValue "TestingConfig" mempool <- parseSection root configValue "MempoolConfig" + -- The @HermodTracing@ value is captured (as a file path or an inline object) + -- and then handed to trace-dispatcher's own parser, which resolves it to a + -- 'TraceConfig' — reading the referenced file, or the inline object directly. tracing <- runCodec Nothing "Tracing" configValue + traceConfig <- resolveTracingConfiguration root tracing -- The genesis files referenced by the configuration are read and decoded -- here, so that JSON resolution happens entirely within this library. let byronCfg = byronGenesis protocol @@ -238,7 +264,7 @@ parseConfigurationVersion1 root minNodeVer configValue = do , localConnectionsConfig = Identity localConnections , testingConfiguration = Identity testing , mempoolConfiguration = Identity mempool - , tracingConfiguration = tracing + , tracingConfiguration = traceConfig , byronGenesisConfig = byronGenesisData , shelleyGenesisConfig = shelleyGenesisData , alonzoGenesisConfig = alonzoGenesisData diff --git a/src/Cardano/Configuration/File/Lint.hs b/src/Cardano/Configuration/File/Lint.hs index 8da4d69..afc7ae6 100644 --- a/src/Cardano/Configuration/File/Lint.hs +++ b/src/Cardano/Configuration/File/Lint.hs @@ -1,8 +1,6 @@ --- | Linting of the top-level configuration keys: detecting unrecognised keys, --- keys shadowed by a component supplied as its own section, and use of the legacy --- single-file form. This is the only part of the configuration-file handling that --- depends on the schema (for the set of recognised keys and the per-component key --- listing). +-- | Linting of the top-level configuration keys: detecting unrecognised keys. +-- This is the only part of the configuration-file handling that depends on the +-- schema (for the set of recognised keys). -- -- The checks are pure and return structured 'ConfigWarning's; how (or whether) to -- surface them — print, log via a tracer, treat as fatal — is left to the caller. @@ -11,37 +9,29 @@ module Cardano.Configuration.File.Lint , renderConfigWarning , configWarnings , checkUnknownKeys - , checkShadowedKeys - , checkLegacyFormat - , checkEnvelope + , inVersion1Format ) where -import Cardano.Configuration.Schema (componentPropertyNames, recognisedKeys) +import Cardano.Configuration.Schema (recognisedKeys) import Data.Aeson (Value (..)) import qualified Data.Aeson.Key as K import qualified Data.Aeson.KeyMap as KM import Data.List (intercalate) -import Data.Text (Text) -import qualified Data.Text as T -- | A non-fatal observation about a configuration. Returned by the parser so the -- caller decides how to surface it (the @cardano-config@ executable prints them -- to @stderr@; another consumer might log them through its own tracer, or treat -- them as errors). data ConfigWarning - = -- | Top-level keys that no parser recognises (typically typos); they are - -- ignored. + = -- | Top-level keys that no parser recognises: typos, or a component property + -- placed flat under @Configuration@ instead of under its section. They are + -- ignored (not resolved into a section). UnrecognisedKeys [String] - | -- | Top-level keys ignored because their component was also given as its - -- own section (which wins). Each pair is @(section, key)@. - ShadowedKeys [(Text, Text)] - | -- | The configuration uses the legacy single-file form (component keys at the - -- top level) rather than the recommended split-file form. - LegacySingleFileFormat - | -- | The document is not in the recommended Version1 envelope form: one or - -- more of the top-level @$schema@, @Version@, @MinNodeVersion@ and @Configuration@ keys - -- is absent. Carries the missing key names. - NotVersion1Envelope [Text] + | -- | The document was not in the Version1 format (no top-level @Configuration@ + -- envelope), so the parser did not accept it as-is: it was migrated to the + -- Version1 format (see @Cardano.Configuration.File.Migrate.migrate@) before + -- parsing. Run @cardano-config migrate@ to update the file on disk. + MigratedToVersion1 | -- | A consistency check of warning severity did not hold on the resolved -- configuration (e.g. the Mithril snapshot policy under the V2LSM backend -- without an @LSMExportPath@). The configuration is still accepted; the @@ -55,26 +45,27 @@ renderConfigWarning :: ConfigWarning -> String renderConfigWarning = \case UnrecognisedKeys ks -> "unrecognised configuration key(s): " <> intercalate ", " ks <> " (ignored)" - ShadowedKeys sks -> - "top-level configuration key(s) ignored because their component is given as a separate section: " - <> intercalate ", " (map describe sks) - where - describe (section, key) = T.unpack key <> " (shadowed by the " <> T.unpack section <> " section)" - LegacySingleFileFormat -> - "the configuration uses the legacy single-file form (component keys at the top level); " - <> "consider porting it to the split-file form (each component under its section key)" - NotVersion1Envelope missing -> - "the configuration is not in the Version1 envelope form; missing top-level key(s): " - <> intercalate ", " (map T.unpack missing) - <> " (expected { $schema, Version, MinNodeVersion, Configuration })" + MigratedToVersion1 -> + "the configuration was not in the Version1 format (no top-level Configuration envelope); " + <> "it was migrated to the Version1 format before parsing " + <> "(run `cardano-config migrate` to update the file)" ConsistencyWarning msg -> msg -- | All warnings for an (unwrapped) configuration object. +-- +-- With the parser accepting only the Version1 format (a document that is not is +-- migrated first, which groups every component under its section), the only key +-- warning left is for keys that none of the parsers recognise — typos, or a +-- component property placed flat under @Configuration@ rather than under its +-- section. There is no longer any \"shadowed\" or \"legacy single-file\" handling: +-- a misplaced key is simply unrecognised, not resolved. configWarnings :: Value -> [ConfigWarning] -configWarnings value = - checkUnknownKeys value <> checkShadowedKeys value <> checkLegacyFormat value +configWarnings = checkUnknownKeys --- | Top-level keys that none of the parsers recognise. +-- | Top-level keys that none of the parsers recognise. Only the section keys, the +-- tracing keys and the envelope annotations are recognised at the @Configuration@ +-- level; a component's own property names are recognised only under its section, +-- so one placed flat here is reported (and ignored, not resolved). checkUnknownKeys :: Value -> [ConfigWarning] checkUnknownKeys = \case Object o -> @@ -82,48 +73,12 @@ checkUnknownKeys = \case in [UnrecognisedKeys unknown | not (null unknown)] _ -> [] --- | Top-level keys shadowed by a component supplied as its own section: when a --- section key (e.g. @TestingConfig@) is present, that component's keys are read --- from the section, so any sibling top-level key belonging to the same component --- (e.g. a top-level @DijkstraGenesisFile@) is silently ignored. --- --- This looks only at the keys the user wrote in this object; the per-component --- base defaults are merged separately inside --- 'Cardano.Configuration.File.Merge.parseSection' and never appear here, so they --- cannot trigger it. -checkShadowedKeys :: Value -> [ConfigWarning] -checkShadowedKeys = \case - Object o -> - let present = map K.toText (KM.keys o) - shadowed = - [ (section, key) - | (section, keys) <- componentPropertyNames - , section `elem` present - , key <- keys - , key `elem` present - ] - in [ShadowedKeys shadowed | not (null shadowed)] - _ -> [] - --- | Whether the document is in the recommended Version1 envelope form, i.e. has --- the top-level @$schema@, @Version@, @MinNodeVersion@ and @Configuration@ keys. Operates on --- the /raw/ top-level value (before the envelope is split off), unlike the other --- checks here which see the unwrapped configuration object. -checkEnvelope :: Value -> [ConfigWarning] -checkEnvelope = \case - Object o -> - let missing = - [ k | k <- ["$schema", "Version", "MinNodeVersion", "Configuration"], not (KM.member (K.fromText k) o) - ] - in [NotVersion1Envelope missing | not (null missing)] - _ -> [] - --- | Whether the configuration uses the legacy single-file form — i.e. any --- component's keys appear flat at the top level rather than under its section. -checkLegacyFormat :: Value -> [ConfigWarning] -checkLegacyFormat = \case - Object o -> - let present = map K.toText (KM.keys o) - flat = [key | (_, keys) <- componentPropertyNames, key <- keys, key `elem` present] - in [LegacySingleFileFormat | not (null flat)] - _ -> [] +-- | Whether the raw document is in the Version1 format: an object carrying the +-- top-level @Configuration@ envelope. This is the parser's accept\/migrate gate — +-- a document in this form is parsed as-is; one that is not is migrated to it +-- first (see 'MigratedToVersion1'). Operates on the /raw/ top-level value, before +-- the envelope is split off. +inVersion1Format :: Value -> Bool +inVersion1Format = \case + Object o -> KM.member "Configuration" o + _ -> False diff --git a/src/Cardano/Configuration/File/Merge.hs b/src/Cardano/Configuration/File/Merge.hs index 66ed8f8..7b15f33 100644 --- a/src/Cardano/Configuration/File/Merge.hs +++ b/src/Cardano/Configuration/File/Merge.hs @@ -138,15 +138,20 @@ loadBaseDefault section = do then Just <$> decodeValueFile (Just section) fp else pure Nothing --- | The configuration layer the user supplied for a section: the top-level --- object when the section key is absent (its keys live there), an inline object, --- or a referenced sub-file. +-- | The configuration layer the user supplied for a section: an inline object, +-- or a referenced sub-file. A component is read only from its own section key; a +-- section that is absent contributes no user layer (so the component takes its +-- base defaults). Component keys placed flat under @Configuration@ are /not/ +-- resolved into their section — they are left unrecognised (see +-- 'Cardano.Configuration.File.Lint.checkUnknownKeys'). Non-Version1 documents, +-- where the keys are flat, are migrated (grouped into sections) before reaching +-- here. sectionUserLayer :: FilePath -> Value -> String -> IO Value sectionUserLayer root configValue section = case configValue of Object o -> case KM.lookup (K.fromString section) o of - Nothing -> pure configValue + Nothing -> pure (Object KM.empty) Just source -> loadSectionSource root section source _ -> throwIO $ diff --git a/src/Cardano/Configuration/File/Migrate.hs b/src/Cardano/Configuration/File/Migrate.hs new file mode 100644 index 0000000..f04c9d3 --- /dev/null +++ b/src/Cardano/Configuration/File/Migrate.hs @@ -0,0 +1,176 @@ +-- | Reshape an existing configuration into the recommended Version1 envelope: +-- @{ $schema, Version, MinNodeVersion, Configuration }@, with every component +-- grouped under its section key inside @Configuration@. +-- +-- It also rewrites the field names that changed, and drops the fields that were +-- removed, in the rename series that this library no longer parses (see +-- 'renamedFields'\/'removedFields'). This is deliberate: the parser rejects the +-- old names outright, and @migrate@ is the supported way to bring an older +-- configuration up to the current names. +-- +-- Apart from those renames\/removals this is a purely structural migration: it +-- preserves the values as written and does /not/ fill in defaults, inline +-- referenced sub-files, or read genesis files. It is meant to port a legacy +-- single-file (flat) or otherwise non-enveloped configuration to the new layout; +-- resolution and validation are left to a subsequent @resolve@. +-- +-- The reshaping (see 'migrate'): +-- +-- * renamed keys are rewritten to their current names and removed keys are +-- dropped, at every depth (so the grouping below, which keys off the current +-- names, places them correctly); +-- * @$schema@ (the published schema URL) and @Version@ (1) are added when +-- absent; an existing @Version@\/@MinNodeVersion@ is carried through. +-- * a flat top-level property key is nested under the component section that +-- owns it (e.g. @ConsensusMode@ under @ConsensusConfig@, @LedgerDB@ under +-- @StorageConfig@); +-- * a section key (whether an inline object or a path to a sub-file), the +-- @HermodTracing@ key, and any unrecognised key are kept at the +-- @Configuration@ level as-is (so nothing is silently dropped); +-- * a document already in the envelope is reshaped idempotently. +module Cardano.Configuration.File.Migrate + ( migrate + ) where + +import Cardano.Configuration.File.Merge (mergeValues) +import Cardano.Configuration.Schema (componentPropertyNames, schemaId) +import Data.Aeson (Value (..)) +import qualified Data.Aeson.Key as K +import qualified Data.Aeson.KeyMap as KM +import Data.Maybe (fromMaybe) +import Data.Text (Text) + +-- | Migrate a raw configuration value to the Version1 envelope. A value that is +-- not a JSON\/YAML object is returned unchanged. +-- +-- Renames and removals are applied first (recursively, over the whole document) +-- so that the subsequent structural grouping sees only current names. +migrate :: Value -> Value +migrate = reshape . renameLegacy + +-- | The field renames introduced in the current key-naming series, as +-- @(old, new)@. The parser only accepts the new names; @migrate@ rewrites the +-- old ones so a configuration written before the rename can be brought forward. +-- Matching is on the whole key (not a substring), so e.g. +-- @SyncTargetNumberOfRootPeers@ is left untouched by the @TargetNumberOf*@ +-- entries. These names are globally unique, so they are rewritten at any depth; +-- the lower-cased 'acceptedConnectionsLimitFields' are handled separately +-- because those names are too generic to rewrite unconditionally. +renamedFields :: [(Text, Text)] +renamedFields = + [ -- gRPC local-connection keys (were named Rpc) + ("EnableRpc", "EnableGrpc") + , ("RpcSocketPath", "GrpcSocketPath") + , -- Deadline peer targets (gained the Deadline prefix) + ("TargetNumberOfRootPeers", "DeadlineTargetNumberOfRootPeers") + , ("TargetNumberOfKnownPeers", "DeadlineTargetNumberOfKnownPeers") + , ("TargetNumberOfEstablishedPeers", "DeadlineTargetNumberOfEstablishedPeers") + , ("TargetNumberOfActivePeers", "DeadlineTargetNumberOfActivePeers") + , ("TargetNumberOfKnownBigLedgerPeers", "DeadlineTargetNumberOfKnownBigLedgerPeers") + , ("TargetNumberOfEstablishedBigLedgerPeers", "DeadlineTargetNumberOfEstablishedBigLedgerPeers") + , ("TargetNumberOfActiveBigLedgerPeers", "DeadlineTargetNumberOfActiveBigLedgerPeers") + ] + +-- | The @AcceptedConnectionsLimit@ sub-keys that were lower-cased before the +-- rename. Their names (@delay@ especially) are too generic to rewrite wherever +-- they appear, so they are only rewritten inside an @AcceptedConnectionsLimit@ +-- object (see 'renameLegacy'). +acceptedConnectionsLimitFields :: [(Text, Text)] +acceptedConnectionsLimitFields = + [ ("hardLimit", "HardLimit") + , ("softLimit", "SoftLimit") + , ("delay", "Delay") + ] + +-- | The fields removed in the current series. @migrate@ drops them (they are no +-- longer parsed): the Byron @LastKnownBlockVersion-*@ trio and +-- @PBftSignatureThreshold@ now come from consensus defaults rather than config; +-- @Protocol@ is the vestigial protocol selector; and @MaxKnownMajorProtocolVersion@ +-- is a dead key the node never read at all. Unlike a genuine typo (which is kept, +-- so nothing is lost) these are known-obsolete keys, so @migrate@ removes them +-- rather than carry them forward as perpetual unrecognised-key warnings. +removedFields :: [Text] +removedFields = + [ "PBftSignatureThreshold" + , "LastKnownBlockVersion-Major" + , "LastKnownBlockVersion-Minor" + , "LastKnownBlockVersion-Alt" + , "Protocol" + , "MaxKnownMajorProtocolVersion" + ] + +-- | Rewrite renamed keys and drop removed keys, everywhere in the document. +-- Recurses through objects and arrays; leaves scalars unchanged. The generic +-- 'acceptedConnectionsLimitFields' are rewritten only within an +-- @AcceptedConnectionsLimit@ object, not wherever those names happen to appear. +renameLegacy :: Value -> Value +renameLegacy (Object o) = + Object + . KM.fromList + . map rekey + . filter (\(k, _) -> K.toText k `notElem` removedFields) + $ KM.toList o + where + rekey (k, v) = (rename renamedFields k, scoped k (renameLegacy v)) + -- Inside an AcceptedConnectionsLimit object, also rewrite its (generic) direct + -- sub-keys; the recursion above has already handled any deeper nesting. + scoped k v + | K.toText k == "AcceptedConnectionsLimit" = renameTopKeys acceptedConnectionsLimitFields v + | otherwise = v +renameLegacy (Array a) = Array (fmap renameLegacy a) +renameLegacy v = v + +-- | Apply a rename table to the direct keys of an object (only), leaving +-- non-objects and unlisted keys unchanged. +renameTopKeys :: [(Text, Text)] -> Value -> Value +renameTopKeys table (Object o) = + Object (KM.fromList [(rename table k, v) | (k, v) <- KM.toList o]) +renameTopKeys _ v = v + +-- | Look a key up in a rename table, returning it unchanged if absent. +rename :: [(Text, Text)] -> K.Key -> K.Key +rename table k = maybe k K.fromText (lookup (K.toText k) table) + +-- | The structural reshape into the Version1 envelope. A value that is not a +-- JSON\/YAML object is returned unchanged. +reshape :: Value -> Value +reshape (Object top) = + Object $ + KM.insert "$schema" (String (schemaId "config.schema.json")) $ + KM.insert "Version" version $ + withMinNodeVersion $ + KM.singleton "Configuration" (Object configuration) + where + -- Carry an existing Version, otherwise default to the current format (1). + version = fromMaybe (Number 1) (KM.lookup "Version" top) + -- Carry MinNodeVersion through if present; never invent one (it has no + -- default, and its absence is itself a useful warning on the next parse). + withMinNodeVersion = maybe id (KM.insert "MinNodeVersion") (KM.lookup "MinNodeVersion" top) + + -- The configuration body: an already-enveloped document's Configuration + -- object, or (for a legacy\/non-enveloped document) the top-level object + -- minus the envelope annotations. + body = case KM.lookup "Configuration" top of + Just (Object c) -> c + _ -> KM.filterWithKey (\k _ -> K.toText k `notElem` envelopeAnnotations) top + + -- Group each body key under its component section. A flat property key nests + -- under the section that owns it; a section key, HermodTracing or any + -- unrecognised key stays at the Configuration level as-is. A mixed input (a + -- section object plus some of its flat keys) is deep-merged. + configuration = KM.foldrWithKey place KM.empty body + place k v = + case lookup (K.toText k) propertyToSection of + Just section -> KM.insertWith mergeValues (K.fromText section) (Object (KM.singleton k v)) + Nothing -> KM.insertWith mergeValues k v +reshape v = v + +-- | Top-level keys that belong to the envelope, not to the configuration body. +envelopeAnnotations :: [Text] +envelopeAnnotations = ["$schema", "Version", "MinNodeVersion", "Configuration"] + +-- | Each component property name mapped to the section that owns it. Every +-- property belongs to exactly one component (see 'componentPropertyNames'). +propertyToSection :: [(Text, Text)] +propertyToSection = + [(prop, section) | (section, props) <- componentPropertyNames, prop <- props] diff --git a/src/Cardano/Configuration/File/Tracing.hs b/src/Cardano/Configuration/File/Tracing.hs index 75a9bff..cab6f8a 100644 --- a/src/Cardano/Configuration/File/Tracing.hs +++ b/src/Cardano/Configuration/File/Tracing.hs @@ -1,31 +1,74 @@ -- | Tracing configuration. -- -- Tracing is owned by the node's tracing system (hermod / @trace-dispatcher@), --- not by @cardano-config@. The @HermodTracing@ key below is accepted but parsed --- /opaquely/: its contents are neither interpreted nor validated here. The --- authoritative schema for them lives in the @trace-dispatcher@ package. +-- not by @cardano-config@. The configuration is given under a single top-level +-- @HermodTracing@ key, whose value is /either/ a path (a string) to a separate +-- file holding the tracing configuration, /or/ that configuration object inline. -- --- This type exists as an /informational placeholder/, so that the tracing key is --- visible in the configuration schema (rather than silently ignored) and is --- preserved when round-tripping a configuration through the parser. +-- We do not interpret the tracing configuration ourselves: its authoritative +-- schema lives in @trace-dispatcher@. What we do is hand the @HermodTracing@ +-- value to @trace-dispatcher@'s own parser ('readConfigurationWithDefault'), which turns it +-- into a 'TraceConfig' — a file reference via 'FromFile' (after resolving the +-- path to its canonical location), an inline object via 'FromJSONObject'. +-- +-- Correspondingly, the configuration schema describes @HermodTracing@ only as +-- \"a path or a JSON object\"; the shape of that object is @trace-dispatcher@'s +-- responsibility to describe, not this library's. module Cardano.Configuration.File.Tracing ( TracingConfiguration (..) + , TracingConfigSource (..) + , resolveTracingConfiguration + , defaultCardanoTracingConfig ) where import Autodocodec -import Cardano.Configuration.Basic (optionalFieldWithStrict) -import Cardano.Configuration.Common (filePathFormatMarker) -import Cardano.Ledger.BaseTypes (StrictMaybe) -import Data.Aeson (FromJSON, ToJSON) -import Data.Text (Text) +import Cardano.Configuration.Basic (optionalFieldStrict) +import Cardano.Configuration.Common (filePathCodec) +import Cardano.Ledger.BaseTypes (StrictMaybe (..)) +import Cardano.Logging +import Data.Aeson (FromJSON, Object, ToJSON) +import qualified Data.Aeson.KeyMap as KM +import qualified Data.Map.Strict as Map import GHC.Generics (Generic) +import System.Directory (canonicalizePath) +import System.FilePath (()) + +-- | Where the @HermodTracing@ configuration comes from: either a path to a +-- separate file holding it, or the configuration object given inline. Which one +-- was written is decided purely by the JSON shape (a string vs. an object); the +-- contents are captured opaquely and only interpreted by @trace-dispatcher@. +data TracingConfigSource + = -- | @HermodTracing@ was a path (a string) to a separate file. + TracingConfigFile FilePath + | -- | @HermodTracing@ held the tracing configuration object inline. + TracingConfigInline Object + deriving (Generic, Show) + +-- | A single JSON string is read as a file path; a single JSON object is read as +-- an inline configuration. We dispatch on that shape (as 'NodeDatabasePaths' +-- does) so the two never compete in error messages. +instance HasCodec TracingConfigSource where + codec = + matchChoiceCodec + (dimapCodec TracingConfigFile id filePathCodec) + (dimapCodec TracingConfigInline id inlineObjectCodec) + selector + where + selector (TracingConfigFile fp) = Left fp + selector (TracingConfigInline o) = Right o + +-- | A codec for an arbitrary JSON object. Its schema is just @type: object@ with +-- no described properties — this library deliberately says nothing about what +-- the tracing configuration object holds (that is @trace-dispatcher@'s domain). +inlineObjectCodec :: JSONCodec Object +inlineObjectCodec = dimapCodec KM.fromMapText KM.toMapText (mapCodec valueCodec) --- | The tracing configuration is given under a single @HermodTracing@ key, --- whose value is a path (a string) to a separate file holding that object. It --- is captured opaquely; see the module documentation. +-- | The tracing configuration is given under a single @HermodTracing@ key. It is +-- captured (as a path or an inline object) but not interpreted here; see the +-- module documentation and 'resolveTracingConfiguration'. newtype TracingConfiguration = TracingConfiguration - { hermodTracing :: StrictMaybe Text - -- ^ A path to a file holding the tracing configuration. + { hermodTracing :: StrictMaybe TracingConfigSource + -- ^ The @HermodTracing@ value: a path to a file, or an inline object. } deriving (Generic, Show) deriving (FromJSON, ToJSON) via (Autodocodec TracingConfiguration) @@ -34,10 +77,144 @@ instance HasCodec TracingConfiguration where codec = object "TracingConfiguration" $ TracingConfiguration - <$> optionalFieldWithStrict + <$> optionalFieldStrict "HermodTracing" - (codec @Text filePathFormatMarker) - ( "Tracing configuration as a path to a separate file holding it. " - <> "Consumed by the node tracing system (trace-dispatcher), not parsed or validated by cardano-config." + ( "Tracing configuration, given as a path to a separate file holding it " + <> "or as that configuration object inline. Consumed by the node tracing " + <> "system (trace-dispatcher), which owns its schema; not described further " + <> "by cardano-config." ) .= hermodTracing + +-- | Resolve the captured @HermodTracing@ value into a 'TraceConfig' by handing it +-- to @trace-dispatcher@'s own parser ('readConfigurationWithDefault'), with +-- 'defaultCardanoTracingConfig' supplying defaults for any top-level fields the +-- source leaves unspecified: +-- +-- * a file reference is resolved to its canonical location (relative paths are +-- taken against the configuration directory @root@) and read via 'FromFile'; +-- * an inline object is read via 'FromJSONObject'. +-- +-- When no @HermodTracing@ key is present, the tracing system still needs a +-- configuration, so we fall back to 'defaultCardanoTracingConfig' unchanged +-- rather than to no configuration at all. +resolveTracingConfiguration :: + -- | The directory a relative @HermodTracing@ file path is resolved against. + FilePath -> + TracingConfiguration -> + IO TraceConfig +resolveTracingConfiguration root (TracingConfiguration mSource) = + case mSource of + SNothing -> pure defaultCardanoTracingConfig + SJust (TracingConfigFile path) -> do + canonPath <- canonicalizePath (root path) + readConfigurationWithDefault (FromFile canonPath) defaultCardanoTracingConfig + SJust (TracingConfigInline obj) -> + readConfigurationWithDefault (FromJSONObject obj) defaultCardanoTracingConfig + +defaultCardanoTracingConfig :: TraceConfig +defaultCardanoTracingConfig = + emptyTraceConfig + { tcMetricsPrefix = Just "cardano.node.metrics." + , tcLedgerMetricsFrequency = Nothing -- discard the default from 'trace-dispatcher'; Cardano has own ones, different for block producers and relays + , tcOptions = + Map.fromList + [ + ( [] + , + [ ConfSeverity (SeverityF (Just Notice)) + , ConfDetail DNormal + , ConfBackend + [ Stdout MachineFormat + , EKGBackend + ] + ] + ) + , -- more important tracers going here + + ( ["BlockFetch", "Decision"] + , [ConfSeverity (SeverityF Nothing)] + ) + , + ( ["ChainDB"] + , [ConfSeverity (SeverityF (Just Info))] + ) + , + ( ["ChainDB", "AddBlockEvent", "AddBlockValidation"] + , [ConfSeverity (SeverityF Nothing)] + ) + , + ( ["ChainSync", "Client"] + , [ConfSeverity (SeverityF (Just Warning))] + ) + , + ( ["Net", "ConnectionManager", "Remote"] + , [ConfSeverity (SeverityF (Just Info))] + ) + , + ( ["Startup", "DiffusionInit"] + , [ConfSeverity (SeverityF (Just Info))] + ) + , + ( ["Net", "ErrorPolicy"] + , [ConfSeverity (SeverityF (Just Info))] + ) + , + ( ["Forge", "Loop"] + , [ConfSeverity (SeverityF (Just Info))] + ) + , + ( ["Forge", "StateInfo"] + , [ConfSeverity (SeverityF (Just Info))] + ) + , + ( ["Net", "InboundGovernor", "Remote"] + , [ConfSeverity (SeverityF (Just Info))] + ) + , + ( ["Mempool"] + , [ConfSeverity (SeverityF (Just Info))] + ) + , + ( ["Net", "Mux", "Remote"] + , [ConfSeverity (SeverityF (Just Info))] + ) + , + ( ["Net", "InboundGovernor"] + , [ConfSeverity (SeverityF (Just Warning))] + ) + , + ( ["Net", "PeerSelection"] + , [ConfSeverity (SeverityF Nothing)] + ) + , + ( ["LedgerMetrics"] + , [ConfSeverity (SeverityF Nothing)] + ) + , + ( ["Resources"] + , [ConfSeverity (SeverityF Nothing)] + ) + , -- Limiters + + ( ["ChainDB", "AddBlockEvent", "AddedBlockToQueue"] + , [ConfLimiter 2.0] + ) + , + ( ["ChainDB", "AddBlockEvent", "AddedBlockToVolatileDB"] + , [ConfLimiter 2.0] + ) + , + ( ["ChainDB", "AddBlockEvent", "AddBlockValidation", "ValidCandidate"] + , [ConfLimiter 2.0] + ) + , + ( ["ChainDB", "CopyToImmutableDBEvent", "CopiedBlockToImmutableDB"] + , [ConfLimiter 2.0] + ) + , + ( ["BlockFetch", "Client", "CompletedBlockFetch"] + , [ConfLimiter 2.0] + ) + ] + } diff --git a/src/Cardano/Configuration/Render.hs b/src/Cardano/Configuration/Render.hs index 59466e8..9dd2e69 100644 --- a/src/Cardano/Configuration/Render.hs +++ b/src/Cardano/Configuration/Render.hs @@ -24,6 +24,7 @@ import qualified Cardano.Configuration.CliArgs as CLI import qualified Cardano.Configuration.File as File import Cardano.Configuration.Genesis.Byron (byronGenesisToJSON) import Cardano.Ledger.BaseTypes (StrictMaybe (..), strictMaybeToMaybe) +import Cardano.Logging.ConfigurationParser () import Data.Aeson (Value, object, toJSON, (.=)) import Data.Functor.Identity (Identity, runIdentity) import Data.Maybe (mapMaybe) @@ -53,8 +54,15 @@ nodeConfigurationToJSON geneses nc = , "TestingConfig" .= toJSON (weakenTesting (testingConfiguration nc)) , "Runtime" .= runtimeValue nc ] + <> tracingFields <> genesisFields where + -- The tracing configuration resolved by trace-dispatcher, rendered under the + -- same @HermodTracing@ key it is read from (as an inline object, via + -- trace-dispatcher's own 'ToJSON'). Always present: absent a @HermodTracing@ + -- key it holds 'File.defaultCardanoTracingConfig'. + tracingFields = + ["HermodTracing" .= tracingConfiguration nc] -- The resolved (parsed) era geneses, rendered through the ledger's @aeson@ -- 'toJSON' instances (and, for Byron, its canonical-JSON form), so the dump -- shows the decoded genesis content rather than just the file reference and diff --git a/src/Cardano/Configuration/Schema.hs b/src/Cardano/Configuration/Schema.hs index 702e923..65946d0 100644 --- a/src/Cardano/Configuration/Schema.hs +++ b/src/Cardano/Configuration/Schema.hs @@ -45,6 +45,7 @@ module Cardano.Configuration.Schema , localConnectionsSchema , mempoolSchema , testingSchema + , schemaId ) where import Autodocodec.Schema (jsonSchemaViaCodec) @@ -291,14 +292,16 @@ schemaRef = ) ] --- | Every top-level configuration key the parsers recognise: the keys of all --- components (read at the top level in the single-file form), the section keys --- used to reference split sub-files, and the envelope keys. Used to detect --- unrecognised (e.g. misspelled) keys. +-- | Every key the parsers recognise at the @Configuration@ level: the section +-- keys (used to reference each component, inline or as a split sub-file), the +-- tracing keys and the envelope keys. Used to detect unrecognised keys (typos, or +-- a component property placed flat here rather than under its section — such a key +-- is reported and ignored, not resolved). A component's own property names are +-- recognised only inside its section, so they are deliberately /not/ listed here. recognisedKeys :: [Text] recognisedKeys = nub $ - envelopeKeys <> sectionKeys <> tracingKeys <> concatMap snd componentPropertyNames + envelopeKeys <> sectionKeys <> tracingKeys where envelopeKeys = ["$schema", "Version", "MinNodeVersion", "Configuration"] sectionKeys = map fst componentPropertyNames diff --git a/test/Main.hs b/test/Main.hs index b87a5d9..f9ba73b 100644 --- a/test/Main.hs +++ b/test/Main.hs @@ -18,6 +18,7 @@ import Cardano.Configuration (resolveConfiguration) import qualified Cardano.Configuration as C import Cardano.Configuration.CliArgs (CliArgs, parseCliArgs) import Cardano.Configuration.File +import Cardano.Configuration.File.Migrate (migrate) import Cardano.Configuration.File.Storage ( LedgerDbBackendSelector (..) , LedgerDbConfiguration (..) @@ -82,9 +83,14 @@ cases = , parseCase "test/examples/fullconfig.json" , parseCase "test/examples/split.json" , parseCase "test/examples/split-all.json" - , shadowWarnCase - , envelopeWarningCase + , tracingCase + , tracingDefaultParityCase + , misplacedKeyCase + , migrationWarningCase + , migrationErrorCase , splitSubfileSchemaCase + , migrateCase + , migrateRenameCase , subfilePathConfinementCase , minNodeVersionCase , resolveCase @@ -171,47 +177,59 @@ subfilePathConfinementCase = Left (e :: SomeException) -> Just (show e) Right _ -> Nothing --- | A top-level key belonging to a component that is also supplied as its own --- section (here a top-level @DijkstraGenesisFile@ alongside a @TestingConfig@ --- section) is shadowed. Parsing still succeeds, and a 'ShadowedKeys' warning --- naming the offending key is returned for the caller to surface. -shadowWarnCase :: TestTree -shadowWarnCase = - testCase "test/examples/shadow.json (shadowed top-level key returns a warning, still parses)" $ do +-- | A component property placed flat under @Configuration@ (here a +-- @DijkstraGenesisFile@ alongside the @TestingConfig@ section that owns it) is not +-- resolved into that section: it is an unrecognised key. Parsing still succeeds +-- (the component is read from its section) and an 'UnrecognisedKeys' warning names +-- the misplaced key. +misplacedKeyCase :: TestTree +misplacedKeyCase = + testCase "test/examples/shadow.json (a misplaced component key is unrecognised, still parses)" $ do path <- getDataFileName "test/examples/shadow.json" res <- try (parseConfigurationFiles path) expectOk $ case res of Left (e :: SomeException) -> Just (show e) Right (_, warnings) - | any shadowsDijkstra warnings -> Nothing + | any mentionsDijkstra warnings -> Nothing | otherwise -> - Just ("expected a ShadowedKeys warning for DijkstraGenesisFile, got " <> show warnings) + Just ("expected an UnrecognisedKeys warning for DijkstraGenesisFile, got " <> show warnings) where - shadowsDijkstra (ShadowedKeys sks) = - (T.pack "TestingConfig", T.pack "DijkstraGenesisFile") `elem` sks - shadowsDijkstra _ = False - --- | A document not wrapped in the @{ Version, MinNodeVersion, Configuration }@ --- envelope returns a 'NotVersion1Envelope' warning; an enveloped one does not. -envelopeWarningCase :: TestTree -envelopeWarningCase = - testCase "non-enveloped config warns (NotVersion1Envelope); enveloped does not" $ do - flatPath <- getDataFileName "test/examples/split.json" + mentionsDijkstra (UnrecognisedKeys ks) = "DijkstraGenesisFile" `elem` ks + mentionsDijkstra _ = False + +-- | The parser accepts only the Version1 format. A document that is not in it +-- (no top-level @Configuration@ envelope) is migrated to it before parsing and a +-- 'MigratedToVersion1' warning is returned; a document already in the envelope is +-- parsed as-is, with no such warning. +migrationWarningCase :: TestTree +migrationWarningCase = + testCase "non-Version1 config is migrated (warns MigratedToVersion1) and parses; Version1 is not" $ do + legacyPath <- getDataFileName "test/examples/fullconfig.json" envPath <- getDataFileName "test/examples/min-node-version.json" - (_, flatWarnings) <- parseConfigurationFiles flatPath + (_, legacyWarnings) <- parseConfigurationFiles legacyPath (_, envWarnings) <- parseConfigurationFiles envPath expectOk $ - if any isEnvelope flatWarnings && not (any isEnvelope envWarnings) + if MigratedToVersion1 `elem` legacyWarnings && MigratedToVersion1 `notElem` envWarnings then Nothing else Just $ - "expected NotVersion1Envelope only for the non-enveloped config: flat=" - <> show flatWarnings + "expected MigratedToVersion1 only for the non-Version1 config: legacy=" + <> show legacyWarnings <> " enveloped=" <> show envWarnings - where - isEnvelope NotVersion1Envelope{} = True - isEnvelope _ = False + +-- | A document that is not in the Version1 format /and/ whose migration still +-- does not yield a parseable configuration is rejected (the parse error +-- surfaces). Here a legacy document with an ill-typed @ConsensusMode@ migrates to +-- a Version1 envelope, but the component codec then rejects the value. +migrationErrorCase :: TestTree +migrationErrorCase = + testCase "a non-Version1 document whose migration is still unparseable is rejected" $ do + path <- getDataFileName "test/examples/migration-unparseable.json" + res <- try (parseConfigurationFiles path >>= \c -> evaluate (length (show c))) + expectOk $ case res of + Left (_ :: SomeException) -> Nothing + Right _ -> Just "expected a parse error for a document whose migration is unparseable" -- | Each per-component split sub-file declares a @$schema@ pointing to that -- component's schema, and the component schema in turn declares a @$schema@ @@ -251,6 +269,104 @@ splitSubfileSchemaCase = properties (Object o) | Just (Object p) <- KM.lookup (K.fromString "properties") o = p properties _ = KM.empty +-- | 'migrate' reshapes a legacy flat config into the Version1 envelope: the +-- envelope keys appear at the top, each component's flat keys are grouped under +-- its section (e.g. ConsensusMode under ConsensusConfig), a removed key +-- (MaxKnownMajorProtocolVersion) is dropped, and the result is idempotent. +migrateCase :: TestTree +migrateCase = + testCase "migrate test/examples/fullconfig.json (legacy flat -> Version1 envelope)" $ do + res <- decodeData "test/examples/fullconfig.json" :: IO (Either String Value) + expectOk $ case res of + Left err -> Just ("could not read fixture: " <> err) + Right raw -> case migrate raw of + m@(Object top) + | not (all (`KM.member` top) (map K.fromString envelopeKeys)) -> + Just ("missing envelope keys; got " <> show (KM.keys top)) + | otherwise -> case KM.lookup (K.fromString "Configuration") top of + Just (Object cfg) + | not (nested cfg "ProtocolConfig" "ByronGenesisFile") -> + Just "ProtocolConfig.ByronGenesisFile not grouped" + | not (nested cfg "ConsensusConfig" "ConsensusMode") -> + Just "ConsensusConfig.ConsensusMode not grouped" + | not (nested cfg "StorageConfig" "LedgerDB") -> + Just "StorageConfig.LedgerDB not grouped" + | KM.member (K.fromString "MaxKnownMajorProtocolVersion") cfg -> + Just "removed key MaxKnownMajorProtocolVersion survived (should be dropped)" + | migrate m /= m -> Just "migrate is not idempotent" + | otherwise -> Nothing + _ -> Just "Configuration is not an object" + _ -> Just "migrate did not produce an object" + where + envelopeKeys = ["$schema", "Version", "MinNodeVersion", "Configuration"] + nested cfg section key = case KM.lookup (K.fromString section) cfg of + Just (Object s) -> KM.member (K.fromString key) s + _ -> False + +-- | 'migrate' rewrites the renamed fields to their current names and drops the +-- removed ones. Renamed flat keys must end up grouped under their section using +-- the /new/ name (a peer target under NetworkConfig, EnableGrpc under +-- LocalConnectionsConfig); AcceptedConnectionsLimit's sub-keys are renamed in +-- place, but that rename is scoped — a stray @delay@ elsewhere is left alone; +-- no removed key survives anywhere (including @Protocol@ and +-- @MaxKnownMajorProtocolVersion@); a genuinely-unrecognised key is kept; and the +-- result is still idempotent. +migrateRenameCase :: TestTree +migrateRenameCase = + testCase "migrate rewrites renamed fields and drops removed ones" $ do + res <- decodeData "test/examples/legacy-renamed-fields.json" :: IO (Either String Value) + expectOk $ case res of + Left err -> Just ("could not read fixture: " <> err) + Right raw -> case migrate raw of + m@(Object top) + | any (`elem` removed) (allKeys m) -> + Just ("a removed key survived; keys: " <> show (allKeys m)) + | any (`elem` oldNames) (allKeys m) -> + Just ("an old name survived; keys: " <> show (allKeys m)) + | otherwise -> case KM.lookup (K.fromString "Configuration") top of + Just (Object cfg) + | not (nested cfg "NetworkConfig" "DeadlineTargetNumberOfRootPeers") -> + Just "renamed peer target not grouped under NetworkConfig" + | not (nested cfg "LocalConnectionsConfig" "EnableGrpc") -> + Just "EnableGrpc not grouped under LocalConnectionsConfig" + | not (deepNested cfg "NetworkConfig" "AcceptedConnectionsLimit" "HardLimit") -> + Just "AcceptedConnectionsLimit.HardLimit not renamed in place" + | deepNested cfg "NetworkConfig" "AcceptedConnectionsLimit" "hardLimit" -> + Just "AcceptedConnectionsLimit.hardLimit not renamed" + -- The rename is scoped: a stray top-level "delay" is not touched. + | not (KM.member (K.fromString "delay") cfg) -> + Just "a stray 'delay' outside AcceptedConnectionsLimit was renamed (should be scoped)" + -- A genuinely-unrecognised key (a typo) is kept, not dropped. + | not (KM.member (K.fromString "SomeUnrecognisedKey") cfg) -> + Just "a genuinely-unrecognised key was dropped (should be kept)" + | migrate m /= m -> Just "migrate is not idempotent" + | otherwise -> Nothing + _ -> Just "Configuration is not an object" + _ -> Just "migrate did not produce an object" + where + removed = + [ "PBftSignatureThreshold" + , "LastKnownBlockVersion-Major" + , "LastKnownBlockVersion-Minor" + , "LastKnownBlockVersion-Alt" + , "Protocol" + , "MaxKnownMajorProtocolVersion" + ] + -- Globally-unique old names that must never survive (the generic + -- AcceptedConnectionsLimit sub-keys are checked in place above, since a stray + -- one is deliberately left unchanged). + oldNames = ["EnableRpc", "RpcSocketPath", "TargetNumberOfRootPeers"] + nested cfg section key = case KM.lookup (K.fromString section) cfg of + Just (Object s) -> KM.member (K.fromString key) s + _ -> False + deepNested cfg section sub key = case KM.lookup (K.fromString section) cfg of + Just (Object s) -> nested s sub key + _ -> False + -- Every object key appearing anywhere in the document. + allKeys (Object o) = map K.toString (KM.keys o) <> concatMap allKeys (KM.elems o) + allKeys (Array a) = concatMap allKeys a + allKeys _ = [] + -- | The optional top-level @MinNodeVersion@ annotation is read from the same -- level as @Version@: from inside the @{ Version, Configuration }@ envelope, and -- — when the document is not enveloped — from the top level alongside the @@ -295,6 +411,76 @@ resolveCase = Left err -> assertFailure (show err) Right (nc, _) -> () <$ evaluate (length (show nc)) +-- | The top-level @HermodTracing@ key is resolved by trace-dispatcher's parser +-- into a 'TraceConfig' — whether given inline (an object) or as a path to a +-- separate file — and surfaced both on the parse result and, when the resolved +-- configuration is dumped, back under a @HermodTracing@ key. A configuration +-- without the key falls back to 'defaultCardanoTracingConfig', which is likewise +-- surfaced and rendered (so the @HermodTracing@ key always appears). +tracingCase :: TestTree +tracingCase = + testCase "HermodTracing resolves to a TraceConfig (inline, file, default) and is always rendered" $ do + inline <- parsed "test/examples/tracing-inline.json" + fromFile <- parsed "test/examples/tracing-file.json" + absent <- parsed "test/examples/fullconfig.json" + renderedInline <- rendersTracing "test/examples/tracing-inline.json" + renderedAbsent <- rendersTracing "test/examples/fullconfig.json" + let asJSON = toJSON . tracingConfiguration + deflt = toJSON defaultCardanoTracingConfig + expectOk $ + if asJSON inline /= deflt -- the inline object was applied + && asJSON fromFile /= deflt -- the referenced file was applied + && asJSON absent == deflt -- no key falls back to the default + && renderedInline == Right True + && renderedAbsent == Right True -- rendered even without a key + then Nothing + else + Just $ + "unexpected tracing resolution: inlineIsDefault=" + <> show (asJSON inline == deflt) + <> " fileIsDefault=" + <> show (asJSON fromFile == deflt) + <> " absentIsDefault=" + <> show (asJSON absent == deflt) + <> " renderedInline=" + <> show renderedInline + <> " renderedAbsent=" + <> show renderedAbsent + where + parsed fp = getDataFileName fp >>= fmap fst . parseConfigurationFiles + -- Whether the resolved configuration renders a HermodTracing key. + rendersTracing fp = do + path <- getDataFileName fp + (cfg, _) <- parseConfigurationFiles path + pure $ case cliArgs [] of + Nothing -> Left "could not build CLI arguments" + Just cli -> case resolveConfiguration cli cfg of + Left e -> Left ("resolve failed: " <> show e) + Right (nc, _) -> case nodeConfigurationToJSON OmitGeneses nc of + Object o -> Right (KM.member (K.fromString "HermodTracing") o) + _ -> Left "rendered configuration was not an object" + +-- | The committed @defaults/HermodTracing.json@ must equal the JSON of the +-- in-tree 'defaultCardanoTracingConfig' literal (encoded through +-- trace-dispatcher's own 'TraceConfig' codec), so the checked-in default cannot +-- drift from the Haskell source. Regenerate the file from +-- 'defaultCardanoTracingConfig' if this fails. +tracingDefaultParityCase :: TestTree +tracingDefaultParityCase = + testCase "defaults/HermodTracing.json matches defaultCardanoTracingConfig" $ do + path <- getDataFileName "defaults/HermodTracing.json" + committed <- eitherDecodeFileStrict' path :: IO (Either String Value) + expectOk $ case committed of + Left e -> Just ("could not read defaults/HermodTracing.json: " <> e) + Right v + | toJSON defaultCardanoTracingConfig == v -> Nothing + | otherwise -> + Just $ + "defaults/HermodTracing.json is out of date; regenerate from defaultCardanoTracingConfig: " + <> show (toJSON defaultCardanoTracingConfig) + <> " /= " + <> show v + -- | With 'IncludeGeneses' the resolved configuration renders the decoded value -- of every era genesis (Byron via its canonical-JSON form, the rest via the -- ledger's aeson instances), not just the file references; with 'OmitGeneses' diff --git a/test/examples/legacy-renamed-fields.json b/test/examples/legacy-renamed-fields.json new file mode 100644 index 0000000..d6f6a10 --- /dev/null +++ b/test/examples/legacy-renamed-fields.json @@ -0,0 +1,24 @@ +{ + "PBftSignatureThreshold": 0.6, + "LastKnownBlockVersion-Major": 3, + "LastKnownBlockVersion-Minor": 0, + "LastKnownBlockVersion-Alt": 0, + "Protocol": "Cardano", + "MaxKnownMajorProtocolVersion": 2, + "SomeUnrecognisedKey": true, + "EnableRpc": true, + "RpcSocketPath": "/run/node.sock", + "TargetNumberOfRootPeers": 60, + "TargetNumberOfKnownPeers": 85, + "TargetNumberOfEstablishedPeers": 30, + "TargetNumberOfActivePeers": 15, + "TargetNumberOfKnownBigLedgerPeers": 15, + "TargetNumberOfEstablishedBigLedgerPeers": 10, + "TargetNumberOfActiveBigLedgerPeers": 5, + "AcceptedConnectionsLimit": { + "hardLimit": 512, + "softLimit": 384, + "delay": 5 + }, + "delay": 99 +} diff --git a/test/examples/migration-unparseable.json b/test/examples/migration-unparseable.json new file mode 100644 index 0000000..7b23522 --- /dev/null +++ b/test/examples/migration-unparseable.json @@ -0,0 +1,3 @@ +{ + "ConsensusMode": 12345 +} diff --git a/test/examples/shadow.json b/test/examples/shadow.json index 8d7cad6..966bee4 100644 --- a/test/examples/shadow.json +++ b/test/examples/shadow.json @@ -1,10 +1,14 @@ { - "StorageConfig": "storage.json", - "ConsensusConfig": "consensus.json", - "ProtocolConfig": "protocol.json", - "NetworkConfig": "network.json", - "LocalConnectionsConfig": "localconnections.json", - "TestingConfig": "testing.json", - "MempoolConfig": "mempool.json", - "DijkstraGenesisFile": "dijkstra-genesis.json" + "$schema": "https://raw.githubusercontent.com/IntersectMBO/cardano-config/main/schemas/config.schema.json", + "Version": 1, + "Configuration": { + "StorageConfig": "storage.json", + "ConsensusConfig": "consensus.json", + "ProtocolConfig": "protocol.json", + "NetworkConfig": "network.json", + "LocalConnectionsConfig": "localconnections.json", + "TestingConfig": "testing.json", + "MempoolConfig": "mempool.json", + "DijkstraGenesisFile": "dijkstra-genesis.json" + } } diff --git a/test/examples/tracing-file.json b/test/examples/tracing-file.json new file mode 100644 index 0000000..b0da48d --- /dev/null +++ b/test/examples/tracing-file.json @@ -0,0 +1,31 @@ +{ + "AlonzoGenesisFile": "mainnet-alonzo-genesis.json", + "AlonzoGenesisHash": "7e94a15f55d1e82d10f09203fa1d40f8eede58fd8066542cf6566008068ed874", + "ByronGenesisFile": "mainnet-byron-genesis.json", + "ByronGenesisHash": "5f20df933584822601f9e3f8c024eb5eb252fe8cefb24d1317dc3d432e940ebb", + "CheckpointsFile": "mainnet-checkpoints.json", + "CheckpointsFileHash": "3e6dee5bae7acc6d870187e72674b37c929be8c66e62a552cf6a876b1af31ade", + "ConsensusMode": "PraosMode", + "ConwayGenesisFile": "mainnet-conway-genesis.json", + "ConwayGenesisHash": "15a199f895e461ec0ffc6dd4e4028af28a492ab4e806d39cb674c88f7643ef62", + "LastKnownBlockVersion-Alt": 0, + "LastKnownBlockVersion-Major": 3, + "LastKnownBlockVersion-Minor": 0, + "LedgerDB": { + "Backend": "V2LSM", + "LSMDatabasePath": "lsm", + "LSMExportPath": "lsm-export", + "QueryBatchSize": 100000, + "Snapshots": { + "NumOfDiskSnapshots": 2, + "SlotOffset": 388800, + "SnapshotInterval": 432000 + } + }, + "MaxKnownMajorProtocolVersion": 2, + "MinNodeVersion": "10.6.0", + "RequiresNetworkMagic": "RequiresNoMagic", + "ShelleyGenesisFile": "mainnet-shelley-genesis.json", + "ShelleyGenesisHash": "1a3be38bcbb7911969283716ad7aa550250226b76a61fc51cc9a9a35d9276d81", + "HermodTracing": "tracing-sub.json" +} diff --git a/test/examples/tracing-inline.json b/test/examples/tracing-inline.json new file mode 100644 index 0000000..34282c5 --- /dev/null +++ b/test/examples/tracing-inline.json @@ -0,0 +1,45 @@ +{ + "AlonzoGenesisFile": "mainnet-alonzo-genesis.json", + "AlonzoGenesisHash": "7e94a15f55d1e82d10f09203fa1d40f8eede58fd8066542cf6566008068ed874", + "ByronGenesisFile": "mainnet-byron-genesis.json", + "ByronGenesisHash": "5f20df933584822601f9e3f8c024eb5eb252fe8cefb24d1317dc3d432e940ebb", + "CheckpointsFile": "mainnet-checkpoints.json", + "CheckpointsFileHash": "3e6dee5bae7acc6d870187e72674b37c929be8c66e62a552cf6a876b1af31ade", + "ConsensusMode": "PraosMode", + "ConwayGenesisFile": "mainnet-conway-genesis.json", + "ConwayGenesisHash": "15a199f895e461ec0ffc6dd4e4028af28a492ab4e806d39cb674c88f7643ef62", + "LastKnownBlockVersion-Alt": 0, + "LastKnownBlockVersion-Major": 3, + "LastKnownBlockVersion-Minor": 0, + "LedgerDB": { + "Backend": "V2LSM", + "LSMDatabasePath": "lsm", + "LSMExportPath": "lsm-export", + "QueryBatchSize": 100000, + "Snapshots": { + "NumOfDiskSnapshots": 2, + "SlotOffset": 388800, + "SnapshotInterval": 432000 + } + }, + "MaxKnownMajorProtocolVersion": 2, + "MinNodeVersion": "10.6.0", + "RequiresNetworkMagic": "RequiresNoMagic", + "ShelleyGenesisFile": "mainnet-shelley-genesis.json", + "ShelleyGenesisHash": "1a3be38bcbb7911969283716ad7aa550250226b76a61fc51cc9a9a35d9276d81", + "HermodTracing": { + "TraceOptions": { + "": { + "severity": "Info", + "detail": "DNormal", + "backends": [ + "Stdout MachineFormat" + ] + }, + "ChainDB": { + "severity": "Debug" + } + }, + "TraceOptionNodeName": "inline-node" + } +} diff --git a/test/examples/tracing-sub.json b/test/examples/tracing-sub.json new file mode 100644 index 0000000..af5858c --- /dev/null +++ b/test/examples/tracing-sub.json @@ -0,0 +1,12 @@ +{ + "Options": { + "": { + "severity": "Notice", + "detail": "DNormal", + "backends": [ + "Stdout MachineFormat" + ] + } + }, + "ApplicationName": "node-from-file" +}