Prover: compatibility check between zkevmbin and lt trace file (#745)

* update `go-corset` to `v0.9.4`

* feat: add binfile / tracefile compatibility check

This adds a compatibility check between the zkevm.bin file and the lt
trace file.  The compatibility check extracts the constraints commit
used to generate the respective asset, and ensures a match.  If not, the
code panics with an error.  Likewise, if the metadata is missing then
code will panic with an error.

* support "relaxed mode"

This intention here is to enable the strong compatibility check to be
disabled.  It seems sensible to have an option to turn it off, and a
flag is added to the `bin/checker` to disable it.

* remove file-based constraints version check

* update to later version of `go-corset`

In order to allow embedded line count information (as requested
separately from this PR), go-corset now uses a structured form of
metadata.  This simply updates this PR to use the revised API.

* rename "RelaxedMode" => "IgnoreCompabitilityCheck"

This simply renames the "relaxed mode" to something more direct, namely
"IgnoreCompatiblityCheck" which does what it says on the tin.

* update `go-corset` to v1.0.0

* add IgnoreCompatibilityCheck option to prover config

* remove unnecessary variable

* Update compatibility check to fail early on incompatibility

* add log when IgnoreCompatibilityCheck is enabled

---------

Co-authored-by: gusiri <dreamerty@postech.ac.kr>
This commit is contained in:
David Pearce
2025-03-07 12:45:54 +13:00
committed by GitHub
parent 9cd4ace5ba
commit bf71b6b5ca
17 changed files with 135 additions and 114 deletions

View File

@@ -44,11 +44,7 @@ corset:
## Build and bundle the Corset trace-expander dependency
##
zkevm/arithmetization/zkevm.bin: corset
cd ../constraints && \
TAGS=$$(git tag --points-at HEAD) && \
echo "$$TAGS" > ../prover/backend/execution/constraints-versions.txt && \
$(CORSET) && make zkevm.bin && \
mv zkevm.bin ../prover/zkevm/arithmetization
cd ../constraints && $(CORSET) make zkevm.bin && mv zkevm.bin ../prover/zkevm/arithmetization
##
## Generate the setup for the execution prover (to be run with S3 access)

View File

@@ -1 +0,0 @@
beta-v1.3-rc3

View File

@@ -2,14 +2,9 @@ package execution
import (
"bytes"
_ "embed"
"fmt"
"path"
"regexp"
"strings"
public_input "github.com/consensys/linea-monorepo/prover/public-input"
"github.com/sirupsen/logrus"
"github.com/consensys/linea-monorepo/prover/backend/ethereum"
"github.com/consensys/linea-monorepo/prover/backend/execution/bridge"
@@ -24,26 +19,12 @@ import (
"github.com/consensys/linea-monorepo/prover/zkevm"
)
// Embed the constraints-versions.txt file at compile time
//
//go:embed constraints-versions.txt
var constraintsVersionsStr string
// Craft prover's functional inputs
func CraftProverOutput(
cfg *config.Config,
req *Request,
) Response {
// Split the embedded file contents into a string slice
// constraintsVersions := strings.Split(strings.TrimSpace(constraintsVersionsStr), "\n")
// Check the arithmetization version used to generate the trace is contained in the prover request
// and fail fast if the constraint version is not supported
// if err := checkArithmetizationVersion(req.ConflatedExecutionTracesFile, req.TracesEngineVersion, constraintsVersions); err != nil {
// panic(err.Error())
// }
var (
l2BridgeAddress = cfg.Layer2.MsgSvcContract
blocks = req.Blocks()
@@ -252,42 +233,6 @@ func mimcHashLooselyPacked(b []byte) types.Bytes32 {
return types.AsBytes32(buf[:])
}
// verifies the arithmetization version used to generate the trace file against the list of versions
// specified by the constraints in the file path.
func checkArithmetizationVersion(traceFileName, tracesEngineVersion string, constraintsVersions []string) error {
logrus.Info("Verifying the arithmetization version for generating the trace file is supported by the constraints version")
traceFileVersion, err := validateAndExtractVersion(traceFileName)
if err != nil {
return err
}
if strings.Compare(traceFileVersion, tracesEngineVersion) != 0 {
return fmt.Errorf("version specified in the conflated trace file: %s does not match with the trace engine version: %s", traceFileVersion, tracesEngineVersion)
}
for _, version := range constraintsVersions {
if version != "" && strings.Compare(traceFileVersion, version) == 0 && strings.Compare(tracesEngineVersion, version) == 0 {
return nil
}
}
return fmt.Errorf("unsupported arithmetization version:%s found in the conflated trace file: %s", traceFileVersion, traceFileName)
}
func validateAndExtractVersion(traceFileName string) (string, error) {
logrus.Info("Validating and extracting the version from conflated trace files")
// Define the regex pattern with a capturing group for the version part
// The pattern accounts for an optional directory path
traceFilePattern := `^(?:.*/)?\d+-\d+\.conflated\.(v\d+\.\d+\.\d+-[^.]+)\.lt$`
re := regexp.MustCompile(traceFilePattern)
// Check if the file name matches the pattern and extract the version part
matches := re.FindStringSubmatch(traceFileName)
if len(matches) > 1 {
return matches[1], nil
}
return "", fmt.Errorf("conflated trace file: %s not in the appropriate format or version not found", traceFileName)
}
func getBlockHashList(rsp *Response) []types.FullBytes32 {
res := []types.FullBytes32{}
for i := range rsp.BlocksData {

View File

@@ -84,7 +84,7 @@ func mustProveAndPass(
// And run the partial-prover with only the main steps. The generated
// proof is sanity-checked to ensure that the prover never outputs
// invalid proofs.
partial := zkevm.FullZkEVMCheckOnly(traces)
partial := zkevm.FullZkEVMCheckOnly(traces, cfg)
proof := partial.ProveInner(w.ZkEVM)
if err := partial.VerifyInner(proof); err != nil {
utils.Panic("The prover did not pass: %v", err)
@@ -108,7 +108,7 @@ func mustProveAndPass(
// Run the full prover to obtain the intermediate proof
logrus.Info("Get Full IOP")
fullZkEvm := zkevm.FullZkEvm(traces)
fullZkEvm := zkevm.FullZkEvm(traces, cfg)
var (
setup circuits.Setup
@@ -157,7 +157,7 @@ func mustProveAndPass(
// Run the full prover to obtain the intermediate proof
logrus.Info("Get Full IOP")
fullZkEvm := zkevm.FullZkEvm(traces)
fullZkEvm := zkevm.FullZkEvm(traces, cfg)
// Generates the inner-proof and sanity-check it so that we ensure that
// the prover nevers outputs invalid proofs.
@@ -171,7 +171,7 @@ func mustProveAndPass(
case config.ProverModeCheckOnly:
fullZkEvm := zkevm.FullZkEVMCheckOnly(traces)
fullZkEvm := zkevm.FullZkEVMCheckOnly(traces, cfg)
// this will panic to alert errors, so there is no need to handle or
// sanity-check anything.
logrus.Infof("Prover starting the prover")

View File

@@ -22,9 +22,6 @@ func init() {
}
func getParamsFromCLI() (cfg *config.Config, optConfig *mir.OptimisationConfig, traceFPath string, err error) {
flag.Parse()
if len(configFPathCLI) == 0 {
return nil, nil, "", fmt.Errorf("could not find the config path, got %++v", configFPathCLI)
}

View File

@@ -12,8 +12,9 @@ var globalArith *arithmetization.Arithmetization
func MakeDefine(cfg *config.Config, optConfig *mir.OptimisationConfig) wizard.DefineFunc {
return func(build *wizard.Builder) {
globalArith = arithmetization.NewArithmetization(build, arithmetization.Settings{
Limits: &cfg.TracesLimits,
OptimisationLevel: optConfig,
Limits: &cfg.TracesLimits,
OptimisationLevel: optConfig,
IgnoreCompatibilityCheck: &cfg.Execution.IgnoreCompatibilityCheck,
})
}

View File

@@ -112,7 +112,7 @@ func Setup(context context.Context, args SetupArgs) error {
limits = cfg.TracesLimitsLarge
}
extraFlags["cfg_checksum"] = limits.Checksum()
zkEvm := zkevm.FullZkEvm(&limits)
zkEvm := zkevm.FullZkEvm(&limits, cfg)
builder = execution.NewBuilder(zkEvm)
case circuits.BlobDecompressionV0CircuitID:

View File

@@ -2,12 +2,13 @@ package config
import (
"fmt"
"github.com/consensys/linea-monorepo/prover/lib/compressor/blob/dictionary"
"os"
"path"
"path/filepath"
"text/template"
"github.com/consensys/linea-monorepo/prover/lib/compressor/blob/dictionary"
"github.com/ethereum/go-ethereum/common"
"github.com/go-playground/validator/v10"
"github.com/sirupsen/logrus"
@@ -211,6 +212,12 @@ type Execution struct {
// ConflatedTracesDir stores the directory where the conflation traces are stored.
ConflatedTracesDir string `mapstructure:"conflated_traces_dir" validate:"required"`
// IgnoreCompatiblityCheck indicates whether to ignore constaints version checking between
// trace files and zkevm.bin constraint files. Specifically, this check ensures that the zkevm.bin file
// used within the prover was generated from the same commit of linea-constraints as the generated lt trace file.
// Set this to true to disable compatibility checks (default: false).
IgnoreCompatibilityCheck bool `mapstructure:"ignore_compatibility_check"`
}
type BlobDecompression struct {

View File

@@ -28,6 +28,8 @@ func setDefaultValues() {
viper.SetDefault("controller.worker_cmd_tmpl", "prover prove --config {{.ConfFile}} --in {{.InFile}} --out {{.OutFile}}")
viper.SetDefault("controller.worker_cmd_large_tmpl", "prover prove --config {{.ConfFile}} --in {{.InFile}} --out {{.OutFile}} --large")
viper.SetDefault("execution.ignore_compatibility_check", false)
}
func setDefaultPaths() {

View File

@@ -9,7 +9,7 @@ require (
github.com/consensys/compress v0.2.5
github.com/consensys/gnark v0.11.1-0.20250107100237-2cb190338a01
github.com/consensys/gnark-crypto v0.14.1-0.20250117145449-0493a37cc361
github.com/consensys/go-corset v0.0.0-20250219023519-757eed939609
github.com/consensys/go-corset v1.0.0
github.com/crate-crypto/go-kzg-4844 v1.1.0
github.com/dlclark/regexp2 v1.11.2
github.com/fxamacker/cbor/v2 v2.7.0

View File

@@ -104,6 +104,12 @@ github.com/consensys/go-corset v0.0.0-20250217020957-ab7f2d548fa8 h1:q6LG3JTvcx9
github.com/consensys/go-corset v0.0.0-20250217020957-ab7f2d548fa8/go.mod h1:rNP3hMR2Sjy5EdQNTHINwaM5kD08E3CSw8CCKhljjO8=
github.com/consensys/go-corset v0.0.0-20250219023519-757eed939609 h1:UW+f/2XpsFc3FahhhDfK3bvmzpS0LriEm/1NzPctkDU=
github.com/consensys/go-corset v0.0.0-20250219023519-757eed939609/go.mod h1:rNP3hMR2Sjy5EdQNTHINwaM5kD08E3CSw8CCKhljjO8=
github.com/consensys/go-corset v0.9.4 h1:CinUmoXM6pJ9qcFQqCMBQVbyCn1LO9F6MutR9JsQTEo=
github.com/consensys/go-corset v0.9.4/go.mod h1:JKJTywyjBE0Goco4DokW4BAkF6R+jtoo3XgkICwxrcw=
github.com/consensys/go-corset v0.9.5-0.20250304221812-db318ec38ec6 h1:Di3sxD2ntZO4UAfQl8T9zz8RfXl3JXpAven8AdyEWn4=
github.com/consensys/go-corset v0.9.5-0.20250304221812-db318ec38ec6/go.mod h1:JKJTywyjBE0Goco4DokW4BAkF6R+jtoo3XgkICwxrcw=
github.com/consensys/go-corset v1.0.0 h1:kPm/kF0le+9b4eASFu6PbY0SR50wgiiVAUkZqRLklCw=
github.com/consensys/go-corset v1.0.0/go.mod h1:JKJTywyjBE0Goco4DokW4BAkF6R+jtoo3XgkICwxrcw=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=

View File

@@ -1,18 +1,28 @@
package arithmetization
import (
"errors"
"fmt"
"strings"
"github.com/consensys/go-corset/pkg/air"
"github.com/consensys/go-corset/pkg/mir"
"github.com/consensys/go-corset/pkg/schema"
"github.com/consensys/go-corset/pkg/util/collection/typed"
"github.com/consensys/linea-monorepo/prover/backend/files"
"github.com/consensys/linea-monorepo/prover/config"
"github.com/consensys/linea-monorepo/prover/protocol/wizard"
"github.com/sirupsen/logrus"
)
// Settings specifies the parameters for the arithmetization part of the zkEVM.
type Settings struct {
Limits *config.TracesLimits
// IgnoreCompatibilityCheck disables the strong compatibility check.
// Specifically, it does not require the constraints and the trace file to
// have both originated from the same commit. By default, the compability
// check should be enabled.
IgnoreCompatibilityCheck *bool
// OptimisationLevel determines the optimisation level which go-corset will
// apply when compiling the zkevm.bin file to AIR constraints. If in doubt,
// use mir.DEFAULT_OPTIMISATION_LEVEL.
@@ -25,13 +35,18 @@ type Settings struct {
// signature verification.
type Arithmetization struct {
Settings *Settings
Schema *air.Schema
// Schema defines the columns, constraints and computations used to expand a
// given trace, and to subsequently to check satisfiability.
Schema *air.Schema
// Metadata embedded in the zkevm.bin file, as needed to check
// compatibility. Guaranteed non-nil.
Metadata typed.Map
}
// NewArithmetization is the function that declares all the columns and the constraints of
// the zkEVM in the input builder object.
func NewArithmetization(builder *wizard.Builder, settings Settings) *Arithmetization {
schema, _, errS := ReadZkevmBin(settings.OptimisationLevel)
schema, metadata, errS := ReadZkevmBin(settings.OptimisationLevel)
if errS != nil {
panic(errS)
}
@@ -41,19 +56,61 @@ func NewArithmetization(builder *wizard.Builder, settings Settings) *Arithmetiza
return &Arithmetization{
Schema: schema,
Settings: &settings,
Metadata: metadata,
}
}
// Assign the arithmetization related columns. Namely, it will open the file
// specified in the witness object, call corset and assign the prover runtime
// columns.
// columns. As part of the assignment processs, the original trace is expanded
// according to the given schema. The expansion process is about filling in
// computed columns with concrete values, such for determining multiplicative
// inverses, etc.
func (a *Arithmetization) Assign(run *wizard.ProverRuntime, traceFile string) {
traceF := files.MustRead(traceFile)
trace, errT := ReadLtTraces(traceF, a.Schema)
// Parse trace file and extract raw column data.
rawColumns, metadata, errT := ReadLtTraces(traceF, a.Schema)
// Performs a compatibility check by comparing the constraints
// commit of zkevm.bin with the constraints commit of the trace file.
// Panics if an incompatibility is detected.
if *a.Settings.IgnoreCompatibilityCheck == false {
var errors []string
zkevmBinCommit, ok := a.Metadata.String("commit")
if !ok {
errors = append(errors, "missing constraints commit metadata in 'zkevm.bin'")
}
traceFileCommit, ok := metadata.String("commit")
if !ok {
errors = append(errors, "missing constraints commit metadata in '.lt' file")
}
// Check commit mismatch
if zkevmBinCommit != traceFileCommit {
errors = append(errors, fmt.Sprintf(
"zkevm.bin incompatible with trace file (commit %s vs %s)",
zkevmBinCommit, traceFileCommit,
))
}
// Panic only if there are errors
if len(errors) > 0 {
logrus.Panic("compatibility check failed with error message:\n" + strings.Join(errors, "\n"))
}
} else {
logrus.Info("Skip constraints compatibility check between zkevm.bin and trace file")
}
if errT != nil {
fmt.Printf("error loading the trace fpath=%q err=%v", traceFile, errT.Error())
}
AssignFromLtTraces(run, a.Schema, trace, a.Settings.Limits)
// Perform trace expansion
expandedTrace, errs := schema.NewTraceBuilder(a.Schema).Build(rawColumns)
if len(errs) > 0 {
logrus.Warnf("corset expansion gave the following errors: %v", errors.Join(errs...).Error())
}
// Passed
AssignFromLtTraces(run, a.Schema, expandedTrace, a.Settings.Limits)
}

View File

@@ -11,10 +11,9 @@ import (
"github.com/consensys/go-corset/pkg/binfile"
"github.com/consensys/go-corset/pkg/corset"
"github.com/consensys/go-corset/pkg/mir"
"github.com/consensys/go-corset/pkg/schema"
"github.com/consensys/go-corset/pkg/trace"
"github.com/consensys/go-corset/pkg/trace/lt"
"github.com/sirupsen/logrus"
"github.com/consensys/go-corset/pkg/util/collection/typed"
)
const TraceOverflowExitCode = 77
@@ -38,7 +37,7 @@ var zkevmStr string
// This additionally extracts the metadata map from the zkevm.bin file. This
// contains information which can be used to cross-check the zkevm.bin file,
// such as the git commit of the enclosing repository when it was built.
func ReadZkevmBin(optConfig *mir.OptimisationConfig) (schema *air.Schema, metadata map[string]string, err error) {
func ReadZkevmBin(optConfig *mir.OptimisationConfig) (schema *air.Schema, metadata typed.Map, err error) {
var (
binf binfile.BinaryFile
buf []byte = []byte(zkevmStr)
@@ -49,34 +48,43 @@ func ReadZkevmBin(optConfig *mir.OptimisationConfig) (schema *air.Schema, metada
err = binf.UnmarshalBinary(buf)
// Sanity check for errors
if err != nil {
return nil, nil, fmt.Errorf("could not parse the read bytes of the 'zkevm.bin' file into an hir.Schema: %w", err)
return nil, metadata, fmt.Errorf("could not parse the read bytes of the 'zkevm.bin' file into an hir.Schema: %w", err)
}
// Extract schema
hirSchema := &binf.Schema
// Attempt to extract metadata from bin file.
metadata, err = binf.Header.GetMetaData()
// Attempt to extract metadata from bin file, and sanity check constraints
// commit information is available.
if metadata, err = binf.Header.GetMetaData(); metadata.IsEmpty() {
return nil, metadata, errors.New("missing metatdata from 'zkevm.bin' file")
}
// This performs the corset compilation
return hirSchema.LowerToMir().LowerToAir(*optConfig), metadata, err
}
func ReadLtTraces(f io.ReadCloser, sch *air.Schema) (trace.Trace, error) {
// ReadLtTraces reads a given LT trace file which contains (unexpanded) column
// data, and additionally extracts the metadata map from the zkevm.bin file. The
// metadata contains information which can be used to cross-check the zkevm.bin
// file, such as the git commit of the enclosing repository when it was built.
func ReadLtTraces(f io.ReadCloser, sch *air.Schema) (rawColumns []trace.RawColumn, metadata typed.Map, err error) {
var (
traceFile lt.TraceFile
ok bool
)
defer f.Close()
// Read the trace file, including any metadata embedded within.
readBytes, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("failed reading the file: %w", err)
return nil, metadata, fmt.Errorf("failed reading the file: %w", err)
} else if err = traceFile.UnmarshalBinary(readBytes); err != nil {
return nil, metadata, fmt.Errorf("failed parsing the bytes of the raw trace '.lt' file: %w", err)
}
rawTraces, err := lt.FromBytes(readBytes)
if err != nil {
return nil, fmt.Errorf("failed parsing the bytes of the raw trace '.lt' file: %w", err)
// Attempt to extract metadata from trace file, and sanity check the
// constraints commit information is present.
if metadata, err = traceFile.Header.GetMetaData(); metadata.IsEmpty() {
return nil, metadata, errors.New("missing metatdata from '.lt' file")
} else if metadata, ok = metadata.Map("constraints"); !ok {
return nil, metadata, errors.New("missing constraints metatdata from '.lt' file")
}
expTraces, errs := schema.NewTraceBuilder(sch).Build(rawTraces)
if len(errs) > 0 {
logrus.Warnf("corset expansion gave the following errors: %v", errors.Join(errs...).Error())
}
return expTraces, nil
// Done
return traceFile.Columns, metadata, nil
}

View File

@@ -26,12 +26,13 @@ var (
// that the provided prover inputs are correct. It typically is used to audit
// the traces of the arithmetization. Currently, it does not include the keccaks
// nor does it include the state-management checks.
func CheckerZkEvm(tl *config.TracesLimits) *ZkEvm {
func CheckerZkEvm(tl *config.TracesLimits, cfg *config.Config) *ZkEvm {
onceCheckerZkEvm.Do(func() {
settings := Settings{
Arithmetization: arithmetization.Settings{
Limits: tl,
OptimisationLevel: &mir.DEFAULT_OPTIMISATION_LEVEL,
Limits: tl,
OptimisationLevel: &mir.DEFAULT_OPTIMISATION_LEVEL,
IgnoreCompatibilityCheck: &cfg.Execution.IgnoreCompatibilityCheck,
},
CompilationSuite: checkerCompilationSuite,
Metadata: wizard.VersionMetadata{

View File

@@ -101,35 +101,36 @@ var (
// behavior is motivated by the fact that the compilation process takes time
// and we don't want to spend the compilation time twice, plus in practice we
// won't need to call it with different configuration parameters.
func FullZkEvm(tl *config.TracesLimits) *ZkEvm {
func FullZkEvm(tl *config.TracesLimits, cfg *config.Config) *ZkEvm {
onceFullZkEvm.Do(func() {
// Initialize the Full zkEVM arithmetization
fullZkEvm = fullZKEVMWithSuite(tl, fullCompilationSuite)
fullZkEvm = fullZKEVMWithSuite(tl, fullCompilationSuite, cfg)
})
return fullZkEvm
}
func FullZkEVMCheckOnly(tl *config.TracesLimits) *ZkEvm {
func FullZkEVMCheckOnly(tl *config.TracesLimits, cfg *config.Config) *ZkEvm {
onceFullZkEvmCheckOnly.Do(func() {
// Initialize the Full zkEVM arithmetization
fullZkEvmCheckOnly = fullZKEVMWithSuite(tl, dummyCompilationSuite)
fullZkEvmCheckOnly = fullZKEVMWithSuite(tl, dummyCompilationSuite, cfg)
})
return fullZkEvmCheckOnly
}
func fullZKEVMWithSuite(tl *config.TracesLimits, suite compilationSuite) *ZkEvm {
func fullZKEVMWithSuite(tl *config.TracesLimits, suite compilationSuite, cfg *config.Config) *ZkEvm {
// @Alex: only set mandatory parameters here. aka, the one that are not
// actually feature-gated.
settings := Settings{
CompilationSuite: suite,
Arithmetization: arithmetization.Settings{
Limits: tl,
OptimisationLevel: &mir.DEFAULT_OPTIMISATION_LEVEL,
Limits: tl,
OptimisationLevel: &mir.DEFAULT_OPTIMISATION_LEVEL,
IgnoreCompatibilityCheck: &cfg.Execution.IgnoreCompatibilityCheck,
},
Statemanager: statemanager.Settings{
AccSettings: accumulator.Settings{

View File

@@ -35,7 +35,7 @@ var (
// ignore the configuration options and directly return the previously compiled
// object. It therefore means that it should not be called twice with different
// config options.
func PartialZkEvm(tl *config.TracesLimits) *ZkEvm {
func PartialZkEvm(tl *config.TracesLimits, cfg *config.Config) *ZkEvm {
// This is hidden behind a once, because the compilation time can take a
// significant amount of time and we want it to be only triggered when we
@@ -48,8 +48,9 @@ func PartialZkEvm(tl *config.TracesLimits) *ZkEvm {
// modules to verify keccak or the state-manager traces.
settings := Settings{
Arithmetization: arithmetization.Settings{
Limits: tl,
OptimisationLevel: &mir.DEFAULT_OPTIMISATION_LEVEL,
Limits: tl,
OptimisationLevel: &mir.DEFAULT_OPTIMISATION_LEVEL,
IgnoreCompatibilityCheck: &cfg.Execution.IgnoreCompatibilityCheck,
},
CompilationSuite: partialCompilationSuite,
Metadata: wizard.VersionMetadata{