From fe24adc3171f04fd64013995a7c014fe9350cc4e Mon Sep 17 00:00:00 2001 From: Sergey Timoshin Date: Sun, 14 Jun 2026 16:38:43 +0100 Subject: [PATCH 1/2] feat: add mmap dump support for groth16 proving keys --- backend/groth16/bls12-377/mmapdump.go | 689 ++++++++++++++++++ backend/groth16/bls12-377/mmapdump_test.go | 316 ++++++++ backend/groth16/bls12-381/mmapdump.go | 689 ++++++++++++++++++ backend/groth16/bls12-381/mmapdump_test.go | 316 ++++++++ backend/groth16/bn254/mmapdump.go | 689 ++++++++++++++++++ backend/groth16/bn254/mmapdump_test.go | 316 ++++++++ backend/groth16/bw6-761/mmapdump.go | 689 ++++++++++++++++++ backend/groth16/bw6-761/mmapdump_test.go | 316 ++++++++ backend/groth16/groth16.go | 191 ++++- backend/groth16/mmapdump_test.go | 81 ++ internal/backend/ioutils/mmap/mmap_unix.go | 58 ++ .../backend/ioutils/mmap/mmap_unsupported.go | 31 + internal/generator/backend/main.go | 2 + .../groth16/groth16.mmapdump.go.tmpl | 681 +++++++++++++++++ .../groth16/tests/groth16.mmapdump.go.tmpl | 307 ++++++++ 15 files changed, 5367 insertions(+), 4 deletions(-) create mode 100644 backend/groth16/bls12-377/mmapdump.go create mode 100644 backend/groth16/bls12-377/mmapdump_test.go create mode 100644 backend/groth16/bls12-381/mmapdump.go create mode 100644 backend/groth16/bls12-381/mmapdump_test.go create mode 100644 backend/groth16/bn254/mmapdump.go create mode 100644 backend/groth16/bn254/mmapdump_test.go create mode 100644 backend/groth16/bw6-761/mmapdump.go create mode 100644 backend/groth16/bw6-761/mmapdump_test.go create mode 100644 backend/groth16/mmapdump_test.go create mode 100644 internal/backend/ioutils/mmap/mmap_unix.go create mode 100644 internal/backend/ioutils/mmap/mmap_unsupported.go create mode 100644 internal/generator/backend/template/zkpschemes/groth16/groth16.mmapdump.go.tmpl create mode 100644 internal/generator/backend/template/zkpschemes/groth16/tests/groth16.mmapdump.go.tmpl diff --git a/backend/groth16/bls12-377/mmapdump.go b/backend/groth16/bls12-377/mmapdump.go new file mode 100644 index 0000000000..cf652f49c4 --- /dev/null +++ b/backend/groth16/bls12-377/mmapdump.go @@ -0,0 +1,689 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package groth16 + +import ( + "bytes" + "encoding/binary" + "encoding/json" + "fmt" + "io" + "os" + "runtime" + "runtime/debug" + "unsafe" + + curve "github.com/consensys/gnark-crypto/ecc/bls12-377" + + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/pedersen" + "github.com/consensys/gnark/internal/backend/ioutils/mmap" +) + +const ( + mmapProvingKeyFormat = "gnark.groth16.bls12-377.proving_key.mmap" + mmapProvingKeyVersion = 1 + mmapProvingKeyAlign = 64 +) + +var mmapProvingKeyMagic = [16]byte{'g', 'n', 'a', 'r', 'k', 'p', 'k', 'm', 'm', 'a', 'p', 'v', '1', '\r', '\n', 0} + +// MmapDumpOption configures ReadMmapDump. +type MmapDumpOption func(*mmapDumpConfig) + +type mmapDumpConfig struct { + disableDomainPrecompute bool + domainNoPrecomputeThreshold uint64 +} + +// WithMmapDumpNoDomainPrecompute disables FFT domain precomputation while +// loading a mapped dump once the serialized domain cardinality is greater than +// or equal to threshold. Passing 0 disables precomputation for every domain. +func WithMmapDumpNoDomainPrecompute(threshold uint64) MmapDumpOption { + return func(cfg *mmapDumpConfig) { + cfg.disableDomainPrecompute = true + cfg.domainNoPrecomputeThreshold = threshold + } +} + +// MmapProvingKey is a Groth16 BLS12-377 proving key backed by a read-only memory +// mapping. Close must not be called while the proving key is in use. +type MmapProvingKey struct { + ProvingKey + + mapping io.Closer +} + +// Close releases the memory mapping backing pk. The key must not be used after +// Close returns. +func (pk *MmapProvingKey) Close() error { + if pk == nil || pk.mapping == nil { + return nil + } + err := pk.mapping.Close() + pk.mapping = nil + pk.ProvingKey = ProvingKey{} + return err +} + +type mmapDumpMeta struct { + Format string `json:"format"` + Version int `json:"version"` + Curve string `json:"curve"` + GOOS string `json:"goos"` + GOARCH string `json:"goarch"` + GoCompiler string `json:"go_compiler"` + GoVersion string `json:"go_version"` + Endian string `json:"endian"` + PointerSize int `json:"pointer_size"` + + GnarkVersion string `json:"gnark_version,omitempty"` + GnarkCryptoVersion string `json:"gnark_crypto_version,omitempty"` + + Sizes mmapDumpSizes `json:"sizes"` + + NbInfinityA uint64 `json:"nb_infinity_a"` + NbInfinityB uint64 `json:"nb_infinity_b"` + + Domain mmapDumpSection `json:"domain"` + + AlphaG1 mmapDumpSection `json:"alpha_g1"` + BetaG1 mmapDumpSection `json:"beta_g1"` + DeltaG1 mmapDumpSection `json:"delta_g1"` + BetaG2 mmapDumpSection `json:"beta_g2"` + DeltaG2 mmapDumpSection `json:"delta_g2"` + + G1A mmapDumpSection `json:"g1_a"` + G1B mmapDumpSection `json:"g1_b"` + G1Z mmapDumpSection `json:"g1_z"` + G1K mmapDumpSection `json:"g1_k"` + G2B mmapDumpSection `json:"g2_b"` + + InfinityA mmapDumpSection `json:"infinity_a"` + InfinityB mmapDumpSection `json:"infinity_b"` + + CommitmentKeys []mmapDumpCommitmentKey `json:"commitment_keys,omitempty"` +} + +type mmapDumpSizes struct { + G1Affine int `json:"g1_affine"` + G2Affine int `json:"g2_affine"` + Bool int `json:"bool"` +} + +type mmapDumpSection struct { + Offset int64 `json:"offset"` + Len int64 `json:"len"` + ElementSize int64 `json:"element_size"` +} + +type mmapDumpCommitmentKey struct { + Basis mmapDumpSection `json:"basis"` + BasisExpSigma mmapDumpSection `json:"basis_exp_sigma"` +} + +// WriteMmapDump writes pk to path using an aligned, file-backed dump format. +// +// The dump stores large proving-key slices as raw Go memory. It is intended for +// trusted local artifacts only and is not portable across incompatible gnark +// versions, gnark-crypto versions, architectures, endianness, or Go type +// layouts. +func (pk *ProvingKey) WriteMmapDump(path string) (err error) { + if !mmap.Supported() { + return fmt.Errorf("mmap is unsupported on this platform") + } + + f, err := os.Create(path) + if err != nil { + return err + } + defer func() { + if closeErr := f.Close(); err == nil { + err = closeErr + } + }() + + w := mmapDumpWriter{w: f} + meta := newMmapDumpMeta(pk) + + var domain bytes.Buffer + if _, err := pk.Domain.WriteTo(&domain); err != nil { + return fmt.Errorf("write domain: %w", err) + } + + if meta.Domain, err = w.writeBytes(domain.Bytes(), 1); err != nil { + return fmt.Errorf("write domain section: %w", err) + } + + if meta.AlphaG1, err = writeMmapDumpValue(&w, &pk.G1.Alpha); err != nil { + return fmt.Errorf("write G1.Alpha: %w", err) + } + if meta.BetaG1, err = writeMmapDumpValue(&w, &pk.G1.Beta); err != nil { + return fmt.Errorf("write G1.Beta: %w", err) + } + if meta.DeltaG1, err = writeMmapDumpValue(&w, &pk.G1.Delta); err != nil { + return fmt.Errorf("write G1.Delta: %w", err) + } + if meta.BetaG2, err = writeMmapDumpValue(&w, &pk.G2.Beta); err != nil { + return fmt.Errorf("write G2.Beta: %w", err) + } + if meta.DeltaG2, err = writeMmapDumpValue(&w, &pk.G2.Delta); err != nil { + return fmt.Errorf("write G2.Delta: %w", err) + } + + if meta.G1A, err = writeMmapDumpSlice(&w, pk.G1.A); err != nil { + return fmt.Errorf("write G1.A: %w", err) + } + if meta.G1B, err = writeMmapDumpSlice(&w, pk.G1.B); err != nil { + return fmt.Errorf("write G1.B: %w", err) + } + if meta.G1Z, err = writeMmapDumpSlice(&w, pk.G1.Z); err != nil { + return fmt.Errorf("write G1.Z: %w", err) + } + if meta.G1K, err = writeMmapDumpSlice(&w, pk.G1.K); err != nil { + return fmt.Errorf("write G1.K: %w", err) + } + if meta.G2B, err = writeMmapDumpSlice(&w, pk.G2.B); err != nil { + return fmt.Errorf("write G2.B: %w", err) + } + if meta.InfinityA, err = writeMmapDumpSlice(&w, pk.InfinityA); err != nil { + return fmt.Errorf("write InfinityA: %w", err) + } + if meta.InfinityB, err = writeMmapDumpSlice(&w, pk.InfinityB); err != nil { + return fmt.Errorf("write InfinityB: %w", err) + } + + if len(pk.CommitmentKeys) > 0 { + meta.CommitmentKeys = make([]mmapDumpCommitmentKey, len(pk.CommitmentKeys)) + for i := range pk.CommitmentKeys { + if meta.CommitmentKeys[i].Basis, err = writeMmapDumpSlice(&w, pk.CommitmentKeys[i].Basis); err != nil { + return fmt.Errorf("write commitment key %d basis: %w", i, err) + } + if meta.CommitmentKeys[i].BasisExpSigma, err = writeMmapDumpSlice(&w, pk.CommitmentKeys[i].BasisExpSigma); err != nil { + return fmt.Errorf("write commitment key %d basis exp sigma: %w", i, err) + } + } + } + + metaBytes, err := json.Marshal(meta) + if err != nil { + return fmt.Errorf("marshal metadata: %w", err) + } + if _, err := f.Write(metaBytes); err != nil { + return fmt.Errorf("write metadata: %w", err) + } + if err := binary.Write(f, binary.LittleEndian, uint64(len(metaBytes))); err != nil { + return fmt.Errorf("write metadata length: %w", err) + } + if _, err := f.Write(mmapProvingKeyMagic[:]); err != nil { + return fmt.Errorf("write magic: %w", err) + } + + return nil +} + +// ReadMmapDump maps path into memory and returns a proving key whose large +// slices are backed by the mapped file. The caller must keep the returned key +// open while it is used and call Close after proving is complete. +// +// Mmap dumps are unsafe raw-memory artifacts. ReadMmapDump validates the dump +// metadata against the current process, but it does not validate curve points or +// subgroup membership. +func ReadMmapDump(path string, opts ...MmapDumpOption) (*MmapProvingKey, error) { + var cfg mmapDumpConfig + for _, opt := range opts { + opt(&cfg) + } + + mapping, err := mmap.Open(path) + if err != nil { + return nil, err + } + pk := &MmapProvingKey{mapping: mapping} + if err := pk.ProvingKey.readMmapDump(mapping.Data, cfg); err != nil { + _ = mapping.Close() + return nil, err + } + return pk, nil +} + +func (pk *ProvingKey) readMmapDump(data []byte, cfg mmapDumpConfig) error { + meta, payloadLen, err := readMmapDumpMeta(data) + if err != nil { + return err + } + if err := validateMmapDumpMeta(meta); err != nil { + return err + } + if err := validateMmapDumpSections(meta, payloadLen); err != nil { + return err + } + payload := data[:payloadLen] + + pk.NbInfinityA = meta.NbInfinityA + pk.NbInfinityB = meta.NbInfinityB + + domainBytes, err := sectionBytes(payload, meta.Domain) + if err != nil { + return fmt.Errorf("domain section: %w", err) + } + domainBytes = append([]byte(nil), domainBytes...) + if cfg.disableDomainPrecompute && len(domainBytes) > 0 { + disableDomainPrecompute(domainBytes, cfg.domainNoPrecomputeThreshold) + } + if _, err := pk.Domain.ReadFrom(bytes.NewReader(domainBytes)); err != nil { + return fmt.Errorf("read domain: %w", err) + } + + if pk.G1.Alpha, err = sectionValue[curve.G1Affine](payload, meta.AlphaG1); err != nil { + return fmt.Errorf("read G1.Alpha: %w", err) + } + if pk.G1.Beta, err = sectionValue[curve.G1Affine](payload, meta.BetaG1); err != nil { + return fmt.Errorf("read G1.Beta: %w", err) + } + if pk.G1.Delta, err = sectionValue[curve.G1Affine](payload, meta.DeltaG1); err != nil { + return fmt.Errorf("read G1.Delta: %w", err) + } + if pk.G2.Beta, err = sectionValue[curve.G2Affine](payload, meta.BetaG2); err != nil { + return fmt.Errorf("read G2.Beta: %w", err) + } + if pk.G2.Delta, err = sectionValue[curve.G2Affine](payload, meta.DeltaG2); err != nil { + return fmt.Errorf("read G2.Delta: %w", err) + } + + if pk.G1.A, err = sectionSlice[curve.G1Affine](payload, meta.G1A); err != nil { + return fmt.Errorf("read G1.A: %w", err) + } + if pk.G1.B, err = sectionSlice[curve.G1Affine](payload, meta.G1B); err != nil { + return fmt.Errorf("read G1.B: %w", err) + } + if pk.G1.Z, err = sectionSlice[curve.G1Affine](payload, meta.G1Z); err != nil { + return fmt.Errorf("read G1.Z: %w", err) + } + if pk.G1.K, err = sectionSlice[curve.G1Affine](payload, meta.G1K); err != nil { + return fmt.Errorf("read G1.K: %w", err) + } + if pk.G2.B, err = sectionSlice[curve.G2Affine](payload, meta.G2B); err != nil { + return fmt.Errorf("read G2.B: %w", err) + } + if pk.InfinityA, err = sectionSlice[bool](payload, meta.InfinityA); err != nil { + return fmt.Errorf("read InfinityA: %w", err) + } + if pk.InfinityB, err = sectionSlice[bool](payload, meta.InfinityB); err != nil { + return fmt.Errorf("read InfinityB: %w", err) + } + + if len(meta.CommitmentKeys) > 0 { + pk.CommitmentKeys = make([]pedersen.ProvingKey, len(meta.CommitmentKeys)) + for i := range meta.CommitmentKeys { + if pk.CommitmentKeys[i].Basis, err = sectionSlice[curve.G1Affine](payload, meta.CommitmentKeys[i].Basis); err != nil { + return fmt.Errorf("read commitment key %d basis: %w", i, err) + } + if pk.CommitmentKeys[i].BasisExpSigma, err = sectionSlice[curve.G1Affine](payload, meta.CommitmentKeys[i].BasisExpSigma); err != nil { + return fmt.Errorf("read commitment key %d basis exp sigma: %w", i, err) + } + } + } else { + pk.CommitmentKeys = nil + } + + return validateMmapDumpProvingKey(pk) +} + +type mmapDumpWriter struct { + w io.Writer + pos int64 +} + +func writeMmapDumpValue[T any](w *mmapDumpWriter, v *T) (mmapDumpSection, error) { + var zero T + size := int64(unsafe.Sizeof(zero)) + b := unsafe.Slice((*byte)(unsafe.Pointer(v)), int(size)) + return w.writeBytes(b, size) +} + +func writeMmapDumpSlice[S ~[]E, E any](w *mmapDumpWriter, s S) (mmapDumpSection, error) { + var zero E + size := int64(unsafe.Sizeof(zero)) + if len(s) == 0 { + return w.writeBytes(nil, size) + } + b := unsafe.Slice((*byte)(unsafe.Pointer(&s[0])), len(s)*int(size)) + return w.writeBytes(b, size) +} + +func (w *mmapDumpWriter) writeBytes(b []byte, elementSize int64) (mmapDumpSection, error) { + if elementSize <= 0 { + return mmapDumpSection{}, fmt.Errorf("invalid element size %d", elementSize) + } + if err := w.padTo(mmapProvingKeyAlign); err != nil { + return mmapDumpSection{}, err + } + section := mmapDumpSection{ + Offset: w.pos, + ElementSize: elementSize, + } + if len(b)%int(elementSize) != 0 { + return mmapDumpSection{}, fmt.Errorf("section length %d is not a multiple of element size %d", len(b), elementSize) + } + section.Len = int64(len(b)) / elementSize + if len(b) == 0 { + return section, nil + } + n, err := w.w.Write(b) + w.pos += int64(n) + if err != nil { + return mmapDumpSection{}, err + } + if n != len(b) { + return mmapDumpSection{}, io.ErrShortWrite + } + return section, nil +} + +func (w *mmapDumpWriter) padTo(alignment int64) error { + rem := w.pos % alignment + if rem == 0 { + return nil + } + padding := make([]byte, alignment-rem) + n, err := w.w.Write(padding) + w.pos += int64(n) + if err != nil { + return err + } + if n != len(padding) { + return io.ErrShortWrite + } + return nil +} + +func newMmapDumpMeta(pk *ProvingKey) mmapDumpMeta { + gnarkVersion, gnarkCryptoVersion := dependencyVersions() + return mmapDumpMeta{ + Format: mmapProvingKeyFormat, + Version: mmapProvingKeyVersion, + Curve: "bls12-377", + GOOS: runtime.GOOS, + GOARCH: runtime.GOARCH, + GoCompiler: runtime.Compiler, + GoVersion: runtime.Version(), + Endian: nativeEndian(), + PointerSize: int(unsafe.Sizeof(uintptr(0))), + GnarkVersion: gnarkVersion, + GnarkCryptoVersion: gnarkCryptoVersion, + Sizes: currentMmapDumpSizes(), + NbInfinityA: pk.NbInfinityA, + NbInfinityB: pk.NbInfinityB, + CommitmentKeys: nil, + } +} + +func currentMmapDumpSizes() mmapDumpSizes { + return mmapDumpSizes{ + G1Affine: int(unsafe.Sizeof(curve.G1Affine{})), + G2Affine: int(unsafe.Sizeof(curve.G2Affine{})), + Bool: int(unsafe.Sizeof(false)), + } +} + +func readMmapDumpMeta(data []byte) (mmapDumpMeta, int, error) { + var meta mmapDumpMeta + trailerLen := 8 + len(mmapProvingKeyMagic) + if len(data) < trailerLen { + return meta, 0, fmt.Errorf("file too small") + } + magicOff := len(data) - len(mmapProvingKeyMagic) + if !bytes.Equal(data[magicOff:], mmapProvingKeyMagic[:]) { + return meta, 0, fmt.Errorf("invalid mmap dump magic") + } + metaLenOff := magicOff - 8 + metaLen := binary.LittleEndian.Uint64(data[metaLenOff:magicOff]) + if metaLen == 0 || metaLen > uint64(metaLenOff) { + return meta, 0, fmt.Errorf("invalid metadata length %d", metaLen) + } + metaOff := metaLenOff - int(metaLen) + if err := json.Unmarshal(data[metaOff:metaLenOff], &meta); err != nil { + return meta, 0, fmt.Errorf("unmarshal metadata: %w", err) + } + return meta, metaOff, nil +} + +func validateMmapDumpMeta(meta mmapDumpMeta) error { + if meta.Format != mmapProvingKeyFormat { + return fmt.Errorf("unsupported format %q", meta.Format) + } + if meta.Version != mmapProvingKeyVersion { + return fmt.Errorf("unsupported mmap dump version %d", meta.Version) + } + if meta.Curve != "bls12-377" { + return fmt.Errorf("unsupported curve %q", meta.Curve) + } + if meta.GOOS != runtime.GOOS { + return fmt.Errorf("goos mismatch: dump=%s runtime=%s", meta.GOOS, runtime.GOOS) + } + if meta.GOARCH != runtime.GOARCH { + return fmt.Errorf("goarch mismatch: dump=%s runtime=%s", meta.GOARCH, runtime.GOARCH) + } + if meta.GoCompiler != "" && meta.GoCompiler != runtime.Compiler { + return fmt.Errorf("go compiler mismatch: dump=%s runtime=%s", meta.GoCompiler, runtime.Compiler) + } + if meta.GoVersion != "" && meta.GoVersion != runtime.Version() { + return fmt.Errorf("go version mismatch: dump=%s runtime=%s", meta.GoVersion, runtime.Version()) + } + if meta.Endian != nativeEndian() { + return fmt.Errorf("endianness mismatch: dump=%s runtime=%s", meta.Endian, nativeEndian()) + } + if meta.PointerSize != int(unsafe.Sizeof(uintptr(0))) { + return fmt.Errorf("pointer size mismatch: dump=%d runtime=%d", meta.PointerSize, unsafe.Sizeof(uintptr(0))) + } + if meta.Sizes != currentMmapDumpSizes() { + return fmt.Errorf("type size mismatch: dump=%+v runtime=%+v", meta.Sizes, currentMmapDumpSizes()) + } + gnarkVersion, gnarkCryptoVersion := dependencyVersions() + if meta.GnarkVersion != "" && gnarkVersion != "" && meta.GnarkVersion != gnarkVersion { + return fmt.Errorf("gnark version mismatch: dump=%s runtime=%s", meta.GnarkVersion, gnarkVersion) + } + if meta.GnarkCryptoVersion != "" && gnarkCryptoVersion != "" && meta.GnarkCryptoVersion != gnarkCryptoVersion { + return fmt.Errorf("gnark-crypto version mismatch: dump=%s runtime=%s", meta.GnarkCryptoVersion, gnarkCryptoVersion) + } + return nil +} + +type namedMmapDumpSection struct { + name string + section mmapDumpSection +} + +func validateMmapDumpSections(meta mmapDumpMeta, payloadLen int) error { + sections := []namedMmapDumpSection{ + {"domain", meta.Domain}, + {"alpha_g1", meta.AlphaG1}, + {"beta_g1", meta.BetaG1}, + {"delta_g1", meta.DeltaG1}, + {"beta_g2", meta.BetaG2}, + {"delta_g2", meta.DeltaG2}, + {"g1_a", meta.G1A}, + {"g1_b", meta.G1B}, + {"g1_z", meta.G1Z}, + {"g1_k", meta.G1K}, + {"g2_b", meta.G2B}, + {"infinity_a", meta.InfinityA}, + {"infinity_b", meta.InfinityB}, + } + for i := range meta.CommitmentKeys { + sections = append(sections, + namedMmapDumpSection{fmt.Sprintf("commitment_keys[%d].basis", i), meta.CommitmentKeys[i].Basis}, + namedMmapDumpSection{fmt.Sprintf("commitment_keys[%d].basis_exp_sigma", i), meta.CommitmentKeys[i].BasisExpSigma}, + ) + } + + ranges := make([]struct { + name string + start, end int64 + }, 0, len(sections)) + for _, named := range sections { + start, end, err := mmapDumpSectionRange(named.name, named.section, payloadLen) + if err != nil { + return err + } + if start == end { + continue + } + for _, prev := range ranges { + if start < prev.end && prev.start < end { + return fmt.Errorf("section %s overlaps section %s", named.name, prev.name) + } + } + ranges = append(ranges, struct { + name string + start, end int64 + }{named.name, start, end}) + } + return nil +} + +func mmapDumpSectionRange(name string, section mmapDumpSection, payloadLen int) (int64, int64, error) { + if section.Offset < 0 || section.Len < 0 || section.ElementSize <= 0 { + return 0, 0, fmt.Errorf("invalid section %s: %+v", name, section) + } + if section.Offset%mmapProvingKeyAlign != 0 { + return 0, 0, fmt.Errorf("section %s offset %d is not aligned to %d", name, section.Offset, mmapProvingKeyAlign) + } + if section.Offset > int64(payloadLen) { + return 0, 0, fmt.Errorf("section %s starts outside payload: offset=%d payload=%d", name, section.Offset, payloadLen) + } + if section.Len == 0 { + return section.Offset, section.Offset, nil + } + const maxInt64 = int64(^uint64(0) >> 1) + if section.Len > maxInt64/section.ElementSize { + return 0, 0, fmt.Errorf("section %s size overflow", name) + } + size := section.Len * section.ElementSize + if size > int64(payloadLen)-section.Offset { + return 0, 0, fmt.Errorf("section %s out of bounds: offset=%d len=%d element_size=%d payload=%d", name, section.Offset, section.Len, section.ElementSize, payloadLen) + } + return section.Offset, section.Offset + size, nil +} + +func validateMmapDumpProvingKey(pk *ProvingKey) error { + if pk.NbInfinityA > uint64(len(pk.InfinityA)) { + return fmt.Errorf("NbInfinityA=%d exceeds InfinityA length %d", pk.NbInfinityA, len(pk.InfinityA)) + } + if pk.NbInfinityB > uint64(len(pk.InfinityB)) { + return fmt.Errorf("NbInfinityB=%d exceeds InfinityB length %d", pk.NbInfinityB, len(pk.InfinityB)) + } + if uint64(len(pk.G1.A))+pk.NbInfinityA != uint64(len(pk.InfinityA)) { + return fmt.Errorf("inconsistent G1.A and InfinityA lengths: len(G1.A)=%d NbInfinityA=%d len(InfinityA)=%d", len(pk.G1.A), pk.NbInfinityA, len(pk.InfinityA)) + } + if uint64(len(pk.G1.B))+pk.NbInfinityB != uint64(len(pk.InfinityB)) { + return fmt.Errorf("inconsistent G1.B and InfinityB lengths: len(G1.B)=%d NbInfinityB=%d len(InfinityB)=%d", len(pk.G1.B), pk.NbInfinityB, len(pk.InfinityB)) + } + if len(pk.G2.B) != len(pk.G1.B) { + return fmt.Errorf("inconsistent B vector lengths: len(G1.B)=%d len(G2.B)=%d", len(pk.G1.B), len(pk.G2.B)) + } + if uint64(len(pk.G1.Z))+1 != pk.Domain.Cardinality { + return fmt.Errorf("inconsistent Z length: len(G1.Z)=%d domain cardinality=%d", len(pk.G1.Z), pk.Domain.Cardinality) + } + for i := range pk.CommitmentKeys { + if len(pk.CommitmentKeys[i].Basis) != len(pk.CommitmentKeys[i].BasisExpSigma) { + return fmt.Errorf("inconsistent commitment key %d lengths: len(Basis)=%d len(BasisExpSigma)=%d", i, len(pk.CommitmentKeys[i].Basis), len(pk.CommitmentKeys[i].BasisExpSigma)) + } + } + return nil +} + +func sectionValue[T any](data []byte, section mmapDumpSection) (T, error) { + var zero T + s, err := sectionSlice[T](data, section) + if err != nil { + return zero, err + } + if len(s) != 1 { + return zero, fmt.Errorf("expected one element, got %d", len(s)) + } + return s[0], nil +} + +func sectionSlice[T any](data []byte, section mmapDumpSection) ([]T, error) { + var zero T + size := int64(unsafe.Sizeof(zero)) + align := int64(unsafe.Alignof(zero)) + if section.ElementSize != size { + return nil, fmt.Errorf("element size mismatch: section=%d type=%d", section.ElementSize, size) + } + if section.Len == 0 { + return nil, nil + } + if section.Offset%align != 0 { + return nil, fmt.Errorf("section offset %d is not aligned to %d", section.Offset, align) + } + b, err := sectionBytes(data, section) + if err != nil { + return nil, err + } + if uintptr(unsafe.Pointer(&b[0]))%uintptr(align) != 0 { + return nil, fmt.Errorf("mapped section address is not aligned to %d", align) + } + return unsafe.Slice((*T)(unsafe.Pointer(&b[0])), int(section.Len)), nil +} + +func sectionBytes(data []byte, section mmapDumpSection) ([]byte, error) { + if section.Offset < 0 || section.Len < 0 || section.ElementSize <= 0 { + return nil, fmt.Errorf("invalid section %+v", section) + } + if section.Len == 0 { + return nil, nil + } + const maxInt64 = int64(^uint64(0) >> 1) + if section.Len > maxInt64/section.ElementSize { + return nil, fmt.Errorf("section size overflow") + } + size := section.Len * section.ElementSize + if section.Offset > int64(len(data)) || size > int64(len(data))-section.Offset { + return nil, fmt.Errorf("section out of bounds: offset=%d len=%d element_size=%d file=%d", section.Offset, section.Len, section.ElementSize, len(data)) + } + return data[section.Offset : section.Offset+size], nil +} + +func disableDomainPrecompute(domain []byte, threshold uint64) { + if len(domain) < 9 { + return + } + cardinality := binary.BigEndian.Uint64(domain[:8]) + if cardinality >= threshold { + // fft.Domain.WriteTo serializes the withPrecompute bool as the final byte. + domain[len(domain)-1] = 0 + } +} + +func nativeEndian() string { + var x uint16 = 1 + if *(*byte)(unsafe.Pointer(&x)) == 1 { + return "little" + } + return "big" +} + +func dependencyVersions() (string, string) { + info, ok := debug.ReadBuildInfo() + if !ok { + return "", "" + } + var gnarkVersion, gnarkCryptoVersion string + if info.Main.Path == "github.com/consensys/gnark" { + gnarkVersion = info.Main.Version + } + for _, dep := range info.Deps { + switch dep.Path { + case "github.com/consensys/gnark": + gnarkVersion = dep.Version + case "github.com/consensys/gnark-crypto": + gnarkCryptoVersion = dep.Version + } + } + return gnarkVersion, gnarkCryptoVersion +} diff --git a/backend/groth16/bls12-377/mmapdump_test.go b/backend/groth16/bls12-377/mmapdump_test.go new file mode 100644 index 0000000000..2fde552d0f --- /dev/null +++ b/backend/groth16/bls12-377/mmapdump_test.go @@ -0,0 +1,316 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package groth16 + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/consensys/gnark-crypto/ecc" + + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" + "github.com/consensys/gnark/backend/witness" + + cs "github.com/consensys/gnark/constraint/bls12-377" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/frontend/cs/r1cs" + "github.com/consensys/gnark/internal/backend/ioutils/mmap" +) + +func skipMmapDumpUnsupported(t testing.TB) { + t.Helper() + if !mmap.Supported() { + t.Skip("mmap is unsupported on this platform") + } +} + +func TestMmapDumpUnsupportedPlatform(t *testing.T) { + if mmap.Supported() { + t.Skip("mmap is supported on this platform") + } + + var pk ProvingKey + path := filepath.Join(t.TempDir(), "proving_key.mmap") + if err := pk.WriteMmapDump(path); err == nil { + t.Fatal("expected error") + } + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatalf("expected no dump file to be created, got %v", err) + } +} + +type mmapDumpCircuit struct { + X frontend.Variable + Y frontend.Variable `gnark:",public"` +} + +func (c *mmapDumpCircuit) Define(api frontend.API) error { + api.AssertIsEqual(api.Mul(c.X, c.X, c.X), c.Y) + return nil +} + +type mmapDumpCommitmentCircuit struct { + One frontend.Variable + Two frontend.Variable `gnark:",public"` +} + +func (c *mmapDumpCommitmentCircuit) Define(api frontend.API) error { + commitCompiler, ok := api.(frontend.Committer) + if !ok { + return fmt.Errorf("compiler does not commit") + } + commitment, err := commitCompiler.Commit(c.One, c.Two) + if err != nil { + return err + } + api.AssertIsDifferent(commitment, 0) + api.AssertIsEqual(c.One, 1) + api.AssertIsEqual(c.Two, 2) + return nil +} + +const mmapDumpBenchmarkCircuitSize = 2048 + +type mmapDumpBenchmarkCircuit struct { + X [mmapDumpBenchmarkCircuitSize]frontend.Variable + Y frontend.Variable `gnark:",public"` +} + +func (c *mmapDumpBenchmarkCircuit) Define(api frontend.API) error { + acc := frontend.Variable(1) + for i := range c.X { + acc = api.Mul(acc, api.Add(c.X[i], 1)) + } + api.AssertIsEqual(acc, c.Y) + return nil +} + +func TestMmapDumpProvingKeyRoundTrip(t *testing.T) { + skipMmapDumpUnsupported(t) + + ccs, err := frontend.Compile(ecc.BLS12_377.ScalarField(), r1cs.NewBuilder, &mmapDumpCircuit{}) + if err != nil { + t.Fatal(err) + } + r1cs := ccs.(*cs.R1CS) + + var pk ProvingKey + var vk VerifyingKey + if err := Setup(r1cs, &pk, &vk); err != nil { + t.Fatal(err) + } + + path := filepath.Join(t.TempDir(), "proving_key.mmap") + if err := pk.WriteMmapDump(path); err != nil { + t.Fatal(err) + } + + mappedPK, err := ReadMmapDump(path, WithMmapDumpNoDomainPrecompute(1)) + if err != nil { + t.Fatal(err) + } + defer mappedPK.Close() + + fullWitness, publicWitness := mmapDumpWitnesses(t) + proof, err := Prove(r1cs, &mappedPK.ProvingKey, fullWitness) + if err != nil { + t.Fatal(err) + } + if err := Verify(proof, &vk, publicWitness.Vector().(fr.Vector)); err != nil { + t.Fatal(err) + } + + if err := mappedPK.Close(); err != nil { + t.Fatal(err) + } + if mappedPK.G1.A != nil { + t.Fatal("expected mapped key to be cleared after close") + } +} + +func TestMmapDumpProvingKeyWithCommitment(t *testing.T) { + skipMmapDumpUnsupported(t) + + ccs, err := frontend.Compile(ecc.BLS12_377.ScalarField(), r1cs.NewBuilder, &mmapDumpCommitmentCircuit{}) + if err != nil { + t.Fatal(err) + } + r1cs := ccs.(*cs.R1CS) + + var pk ProvingKey + var vk VerifyingKey + if err := Setup(r1cs, &pk, &vk); err != nil { + t.Fatal(err) + } + if len(pk.CommitmentKeys) == 0 { + t.Fatal("expected commitment proving keys") + } + + path := filepath.Join(t.TempDir(), "proving_key.mmap") + if err := pk.WriteMmapDump(path); err != nil { + t.Fatal(err) + } + + mappedPK, err := ReadMmapDump(path) + if err != nil { + t.Fatal(err) + } + defer mappedPK.Close() + + assignment := mmapDumpCommitmentCircuit{One: 1, Two: 2} + fullWitness, err := frontend.NewWitness(&assignment, ecc.BLS12_377.ScalarField()) + if err != nil { + t.Fatal(err) + } + publicWitness, err := fullWitness.Public() + if err != nil { + t.Fatal(err) + } + + proof, err := Prove(r1cs, &mappedPK.ProvingKey, fullWitness) + if err != nil { + t.Fatal(err) + } + if err := Verify(proof, &vk, publicWitness.Vector().(fr.Vector)); err != nil { + t.Fatal(err) + } +} + +func TestMmapDumpPreservesDomainByDefault(t *testing.T) { + skipMmapDumpUnsupported(t) + + ccs, err := frontend.Compile(ecc.BLS12_377.ScalarField(), r1cs.NewBuilder, &mmapDumpCircuit{}) + if err != nil { + t.Fatal(err) + } + r1cs := ccs.(*cs.R1CS) + + var pk ProvingKey + var vk VerifyingKey + if err := Setup(r1cs, &pk, &vk); err != nil { + t.Fatal(err) + } + + path := filepath.Join(t.TempDir(), "proving_key.mmap") + if err := pk.WriteMmapDump(path); err != nil { + t.Fatal(err) + } + + mappedPK, err := ReadMmapDump(path) + if err != nil { + t.Fatal(err) + } + defer mappedPK.Close() + + var originalDomain, mappedDomain bytes.Buffer + if _, err := pk.Domain.WriteTo(&originalDomain); err != nil { + t.Fatal(err) + } + if _, err := mappedPK.Domain.WriteTo(&mappedDomain); err != nil { + t.Fatal(err) + } + if !bytes.Equal(originalDomain.Bytes(), mappedDomain.Bytes()) { + t.Fatal("expected default mmap dump load to preserve serialized domain") + } +} + +func mmapDumpWitnesses(t *testing.T) (witness.Witness, witness.Witness) { + t.Helper() + + assignment := mmapDumpCircuit{X: 3, Y: 27} + fullWitness, err := frontend.NewWitness(&assignment, ecc.BLS12_377.ScalarField()) + if err != nil { + t.Fatal(err) + } + publicWitness, err := fullWitness.Public() + if err != nil { + t.Fatal(err) + } + return fullWitness, publicWitness +} + +func BenchmarkMmapDumpProvingKeyLoad(b *testing.B) { + skipMmapDumpUnsupported(b) + + pk := mmapDumpBenchmarkProvingKey(b) + + var dump bytes.Buffer + if err := pk.WriteDump(&dump); err != nil { + b.Fatal(err) + } + + mmapPath := filepath.Join(b.TempDir(), "proving_key.mmap") + if err := pk.WriteMmapDump(mmapPath); err != nil { + b.Fatal(err) + } + + dumpBytes := dump.Bytes() + + b.Run("ReadDump", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + var loaded ProvingKey + if err := loaded.ReadDump(bytes.NewReader(dumpBytes)); err != nil { + b.Fatal(err) + } + if len(loaded.G1.A) == 0 { + b.Fatal("empty proving key") + } + } + }) + + b.Run("ReadMmapDump", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + loaded, err := ReadMmapDump(mmapPath) + if err != nil { + b.Fatal(err) + } + if len(loaded.G1.A) == 0 { + b.Fatal("empty proving key") + } + if err := loaded.Close(); err != nil { + b.Fatal(err) + } + } + }) + + b.Run("ReadMmapDumpNoDomainPrecompute", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + loaded, err := ReadMmapDump(mmapPath, WithMmapDumpNoDomainPrecompute(1)) + if err != nil { + b.Fatal(err) + } + if len(loaded.G1.A) == 0 { + b.Fatal("empty proving key") + } + if err := loaded.Close(); err != nil { + b.Fatal(err) + } + } + }) +} + +func mmapDumpBenchmarkProvingKey(b *testing.B) *ProvingKey { + b.Helper() + + ccs, err := frontend.Compile(ecc.BLS12_377.ScalarField(), r1cs.NewBuilder, &mmapDumpBenchmarkCircuit{}) + if err != nil { + b.Fatal(err) + } + r1cs := ccs.(*cs.R1CS) + + var pk ProvingKey + var vk VerifyingKey + if err := Setup(r1cs, &pk, &vk); err != nil { + b.Fatal(err) + } + return &pk +} diff --git a/backend/groth16/bls12-381/mmapdump.go b/backend/groth16/bls12-381/mmapdump.go new file mode 100644 index 0000000000..5436ba9169 --- /dev/null +++ b/backend/groth16/bls12-381/mmapdump.go @@ -0,0 +1,689 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package groth16 + +import ( + "bytes" + "encoding/binary" + "encoding/json" + "fmt" + "io" + "os" + "runtime" + "runtime/debug" + "unsafe" + + curve "github.com/consensys/gnark-crypto/ecc/bls12-381" + + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/pedersen" + "github.com/consensys/gnark/internal/backend/ioutils/mmap" +) + +const ( + mmapProvingKeyFormat = "gnark.groth16.bls12-381.proving_key.mmap" + mmapProvingKeyVersion = 1 + mmapProvingKeyAlign = 64 +) + +var mmapProvingKeyMagic = [16]byte{'g', 'n', 'a', 'r', 'k', 'p', 'k', 'm', 'm', 'a', 'p', 'v', '1', '\r', '\n', 0} + +// MmapDumpOption configures ReadMmapDump. +type MmapDumpOption func(*mmapDumpConfig) + +type mmapDumpConfig struct { + disableDomainPrecompute bool + domainNoPrecomputeThreshold uint64 +} + +// WithMmapDumpNoDomainPrecompute disables FFT domain precomputation while +// loading a mapped dump once the serialized domain cardinality is greater than +// or equal to threshold. Passing 0 disables precomputation for every domain. +func WithMmapDumpNoDomainPrecompute(threshold uint64) MmapDumpOption { + return func(cfg *mmapDumpConfig) { + cfg.disableDomainPrecompute = true + cfg.domainNoPrecomputeThreshold = threshold + } +} + +// MmapProvingKey is a Groth16 BLS12-381 proving key backed by a read-only memory +// mapping. Close must not be called while the proving key is in use. +type MmapProvingKey struct { + ProvingKey + + mapping io.Closer +} + +// Close releases the memory mapping backing pk. The key must not be used after +// Close returns. +func (pk *MmapProvingKey) Close() error { + if pk == nil || pk.mapping == nil { + return nil + } + err := pk.mapping.Close() + pk.mapping = nil + pk.ProvingKey = ProvingKey{} + return err +} + +type mmapDumpMeta struct { + Format string `json:"format"` + Version int `json:"version"` + Curve string `json:"curve"` + GOOS string `json:"goos"` + GOARCH string `json:"goarch"` + GoCompiler string `json:"go_compiler"` + GoVersion string `json:"go_version"` + Endian string `json:"endian"` + PointerSize int `json:"pointer_size"` + + GnarkVersion string `json:"gnark_version,omitempty"` + GnarkCryptoVersion string `json:"gnark_crypto_version,omitempty"` + + Sizes mmapDumpSizes `json:"sizes"` + + NbInfinityA uint64 `json:"nb_infinity_a"` + NbInfinityB uint64 `json:"nb_infinity_b"` + + Domain mmapDumpSection `json:"domain"` + + AlphaG1 mmapDumpSection `json:"alpha_g1"` + BetaG1 mmapDumpSection `json:"beta_g1"` + DeltaG1 mmapDumpSection `json:"delta_g1"` + BetaG2 mmapDumpSection `json:"beta_g2"` + DeltaG2 mmapDumpSection `json:"delta_g2"` + + G1A mmapDumpSection `json:"g1_a"` + G1B mmapDumpSection `json:"g1_b"` + G1Z mmapDumpSection `json:"g1_z"` + G1K mmapDumpSection `json:"g1_k"` + G2B mmapDumpSection `json:"g2_b"` + + InfinityA mmapDumpSection `json:"infinity_a"` + InfinityB mmapDumpSection `json:"infinity_b"` + + CommitmentKeys []mmapDumpCommitmentKey `json:"commitment_keys,omitempty"` +} + +type mmapDumpSizes struct { + G1Affine int `json:"g1_affine"` + G2Affine int `json:"g2_affine"` + Bool int `json:"bool"` +} + +type mmapDumpSection struct { + Offset int64 `json:"offset"` + Len int64 `json:"len"` + ElementSize int64 `json:"element_size"` +} + +type mmapDumpCommitmentKey struct { + Basis mmapDumpSection `json:"basis"` + BasisExpSigma mmapDumpSection `json:"basis_exp_sigma"` +} + +// WriteMmapDump writes pk to path using an aligned, file-backed dump format. +// +// The dump stores large proving-key slices as raw Go memory. It is intended for +// trusted local artifacts only and is not portable across incompatible gnark +// versions, gnark-crypto versions, architectures, endianness, or Go type +// layouts. +func (pk *ProvingKey) WriteMmapDump(path string) (err error) { + if !mmap.Supported() { + return fmt.Errorf("mmap is unsupported on this platform") + } + + f, err := os.Create(path) + if err != nil { + return err + } + defer func() { + if closeErr := f.Close(); err == nil { + err = closeErr + } + }() + + w := mmapDumpWriter{w: f} + meta := newMmapDumpMeta(pk) + + var domain bytes.Buffer + if _, err := pk.Domain.WriteTo(&domain); err != nil { + return fmt.Errorf("write domain: %w", err) + } + + if meta.Domain, err = w.writeBytes(domain.Bytes(), 1); err != nil { + return fmt.Errorf("write domain section: %w", err) + } + + if meta.AlphaG1, err = writeMmapDumpValue(&w, &pk.G1.Alpha); err != nil { + return fmt.Errorf("write G1.Alpha: %w", err) + } + if meta.BetaG1, err = writeMmapDumpValue(&w, &pk.G1.Beta); err != nil { + return fmt.Errorf("write G1.Beta: %w", err) + } + if meta.DeltaG1, err = writeMmapDumpValue(&w, &pk.G1.Delta); err != nil { + return fmt.Errorf("write G1.Delta: %w", err) + } + if meta.BetaG2, err = writeMmapDumpValue(&w, &pk.G2.Beta); err != nil { + return fmt.Errorf("write G2.Beta: %w", err) + } + if meta.DeltaG2, err = writeMmapDumpValue(&w, &pk.G2.Delta); err != nil { + return fmt.Errorf("write G2.Delta: %w", err) + } + + if meta.G1A, err = writeMmapDumpSlice(&w, pk.G1.A); err != nil { + return fmt.Errorf("write G1.A: %w", err) + } + if meta.G1B, err = writeMmapDumpSlice(&w, pk.G1.B); err != nil { + return fmt.Errorf("write G1.B: %w", err) + } + if meta.G1Z, err = writeMmapDumpSlice(&w, pk.G1.Z); err != nil { + return fmt.Errorf("write G1.Z: %w", err) + } + if meta.G1K, err = writeMmapDumpSlice(&w, pk.G1.K); err != nil { + return fmt.Errorf("write G1.K: %w", err) + } + if meta.G2B, err = writeMmapDumpSlice(&w, pk.G2.B); err != nil { + return fmt.Errorf("write G2.B: %w", err) + } + if meta.InfinityA, err = writeMmapDumpSlice(&w, pk.InfinityA); err != nil { + return fmt.Errorf("write InfinityA: %w", err) + } + if meta.InfinityB, err = writeMmapDumpSlice(&w, pk.InfinityB); err != nil { + return fmt.Errorf("write InfinityB: %w", err) + } + + if len(pk.CommitmentKeys) > 0 { + meta.CommitmentKeys = make([]mmapDumpCommitmentKey, len(pk.CommitmentKeys)) + for i := range pk.CommitmentKeys { + if meta.CommitmentKeys[i].Basis, err = writeMmapDumpSlice(&w, pk.CommitmentKeys[i].Basis); err != nil { + return fmt.Errorf("write commitment key %d basis: %w", i, err) + } + if meta.CommitmentKeys[i].BasisExpSigma, err = writeMmapDumpSlice(&w, pk.CommitmentKeys[i].BasisExpSigma); err != nil { + return fmt.Errorf("write commitment key %d basis exp sigma: %w", i, err) + } + } + } + + metaBytes, err := json.Marshal(meta) + if err != nil { + return fmt.Errorf("marshal metadata: %w", err) + } + if _, err := f.Write(metaBytes); err != nil { + return fmt.Errorf("write metadata: %w", err) + } + if err := binary.Write(f, binary.LittleEndian, uint64(len(metaBytes))); err != nil { + return fmt.Errorf("write metadata length: %w", err) + } + if _, err := f.Write(mmapProvingKeyMagic[:]); err != nil { + return fmt.Errorf("write magic: %w", err) + } + + return nil +} + +// ReadMmapDump maps path into memory and returns a proving key whose large +// slices are backed by the mapped file. The caller must keep the returned key +// open while it is used and call Close after proving is complete. +// +// Mmap dumps are unsafe raw-memory artifacts. ReadMmapDump validates the dump +// metadata against the current process, but it does not validate curve points or +// subgroup membership. +func ReadMmapDump(path string, opts ...MmapDumpOption) (*MmapProvingKey, error) { + var cfg mmapDumpConfig + for _, opt := range opts { + opt(&cfg) + } + + mapping, err := mmap.Open(path) + if err != nil { + return nil, err + } + pk := &MmapProvingKey{mapping: mapping} + if err := pk.ProvingKey.readMmapDump(mapping.Data, cfg); err != nil { + _ = mapping.Close() + return nil, err + } + return pk, nil +} + +func (pk *ProvingKey) readMmapDump(data []byte, cfg mmapDumpConfig) error { + meta, payloadLen, err := readMmapDumpMeta(data) + if err != nil { + return err + } + if err := validateMmapDumpMeta(meta); err != nil { + return err + } + if err := validateMmapDumpSections(meta, payloadLen); err != nil { + return err + } + payload := data[:payloadLen] + + pk.NbInfinityA = meta.NbInfinityA + pk.NbInfinityB = meta.NbInfinityB + + domainBytes, err := sectionBytes(payload, meta.Domain) + if err != nil { + return fmt.Errorf("domain section: %w", err) + } + domainBytes = append([]byte(nil), domainBytes...) + if cfg.disableDomainPrecompute && len(domainBytes) > 0 { + disableDomainPrecompute(domainBytes, cfg.domainNoPrecomputeThreshold) + } + if _, err := pk.Domain.ReadFrom(bytes.NewReader(domainBytes)); err != nil { + return fmt.Errorf("read domain: %w", err) + } + + if pk.G1.Alpha, err = sectionValue[curve.G1Affine](payload, meta.AlphaG1); err != nil { + return fmt.Errorf("read G1.Alpha: %w", err) + } + if pk.G1.Beta, err = sectionValue[curve.G1Affine](payload, meta.BetaG1); err != nil { + return fmt.Errorf("read G1.Beta: %w", err) + } + if pk.G1.Delta, err = sectionValue[curve.G1Affine](payload, meta.DeltaG1); err != nil { + return fmt.Errorf("read G1.Delta: %w", err) + } + if pk.G2.Beta, err = sectionValue[curve.G2Affine](payload, meta.BetaG2); err != nil { + return fmt.Errorf("read G2.Beta: %w", err) + } + if pk.G2.Delta, err = sectionValue[curve.G2Affine](payload, meta.DeltaG2); err != nil { + return fmt.Errorf("read G2.Delta: %w", err) + } + + if pk.G1.A, err = sectionSlice[curve.G1Affine](payload, meta.G1A); err != nil { + return fmt.Errorf("read G1.A: %w", err) + } + if pk.G1.B, err = sectionSlice[curve.G1Affine](payload, meta.G1B); err != nil { + return fmt.Errorf("read G1.B: %w", err) + } + if pk.G1.Z, err = sectionSlice[curve.G1Affine](payload, meta.G1Z); err != nil { + return fmt.Errorf("read G1.Z: %w", err) + } + if pk.G1.K, err = sectionSlice[curve.G1Affine](payload, meta.G1K); err != nil { + return fmt.Errorf("read G1.K: %w", err) + } + if pk.G2.B, err = sectionSlice[curve.G2Affine](payload, meta.G2B); err != nil { + return fmt.Errorf("read G2.B: %w", err) + } + if pk.InfinityA, err = sectionSlice[bool](payload, meta.InfinityA); err != nil { + return fmt.Errorf("read InfinityA: %w", err) + } + if pk.InfinityB, err = sectionSlice[bool](payload, meta.InfinityB); err != nil { + return fmt.Errorf("read InfinityB: %w", err) + } + + if len(meta.CommitmentKeys) > 0 { + pk.CommitmentKeys = make([]pedersen.ProvingKey, len(meta.CommitmentKeys)) + for i := range meta.CommitmentKeys { + if pk.CommitmentKeys[i].Basis, err = sectionSlice[curve.G1Affine](payload, meta.CommitmentKeys[i].Basis); err != nil { + return fmt.Errorf("read commitment key %d basis: %w", i, err) + } + if pk.CommitmentKeys[i].BasisExpSigma, err = sectionSlice[curve.G1Affine](payload, meta.CommitmentKeys[i].BasisExpSigma); err != nil { + return fmt.Errorf("read commitment key %d basis exp sigma: %w", i, err) + } + } + } else { + pk.CommitmentKeys = nil + } + + return validateMmapDumpProvingKey(pk) +} + +type mmapDumpWriter struct { + w io.Writer + pos int64 +} + +func writeMmapDumpValue[T any](w *mmapDumpWriter, v *T) (mmapDumpSection, error) { + var zero T + size := int64(unsafe.Sizeof(zero)) + b := unsafe.Slice((*byte)(unsafe.Pointer(v)), int(size)) + return w.writeBytes(b, size) +} + +func writeMmapDumpSlice[S ~[]E, E any](w *mmapDumpWriter, s S) (mmapDumpSection, error) { + var zero E + size := int64(unsafe.Sizeof(zero)) + if len(s) == 0 { + return w.writeBytes(nil, size) + } + b := unsafe.Slice((*byte)(unsafe.Pointer(&s[0])), len(s)*int(size)) + return w.writeBytes(b, size) +} + +func (w *mmapDumpWriter) writeBytes(b []byte, elementSize int64) (mmapDumpSection, error) { + if elementSize <= 0 { + return mmapDumpSection{}, fmt.Errorf("invalid element size %d", elementSize) + } + if err := w.padTo(mmapProvingKeyAlign); err != nil { + return mmapDumpSection{}, err + } + section := mmapDumpSection{ + Offset: w.pos, + ElementSize: elementSize, + } + if len(b)%int(elementSize) != 0 { + return mmapDumpSection{}, fmt.Errorf("section length %d is not a multiple of element size %d", len(b), elementSize) + } + section.Len = int64(len(b)) / elementSize + if len(b) == 0 { + return section, nil + } + n, err := w.w.Write(b) + w.pos += int64(n) + if err != nil { + return mmapDumpSection{}, err + } + if n != len(b) { + return mmapDumpSection{}, io.ErrShortWrite + } + return section, nil +} + +func (w *mmapDumpWriter) padTo(alignment int64) error { + rem := w.pos % alignment + if rem == 0 { + return nil + } + padding := make([]byte, alignment-rem) + n, err := w.w.Write(padding) + w.pos += int64(n) + if err != nil { + return err + } + if n != len(padding) { + return io.ErrShortWrite + } + return nil +} + +func newMmapDumpMeta(pk *ProvingKey) mmapDumpMeta { + gnarkVersion, gnarkCryptoVersion := dependencyVersions() + return mmapDumpMeta{ + Format: mmapProvingKeyFormat, + Version: mmapProvingKeyVersion, + Curve: "bls12-381", + GOOS: runtime.GOOS, + GOARCH: runtime.GOARCH, + GoCompiler: runtime.Compiler, + GoVersion: runtime.Version(), + Endian: nativeEndian(), + PointerSize: int(unsafe.Sizeof(uintptr(0))), + GnarkVersion: gnarkVersion, + GnarkCryptoVersion: gnarkCryptoVersion, + Sizes: currentMmapDumpSizes(), + NbInfinityA: pk.NbInfinityA, + NbInfinityB: pk.NbInfinityB, + CommitmentKeys: nil, + } +} + +func currentMmapDumpSizes() mmapDumpSizes { + return mmapDumpSizes{ + G1Affine: int(unsafe.Sizeof(curve.G1Affine{})), + G2Affine: int(unsafe.Sizeof(curve.G2Affine{})), + Bool: int(unsafe.Sizeof(false)), + } +} + +func readMmapDumpMeta(data []byte) (mmapDumpMeta, int, error) { + var meta mmapDumpMeta + trailerLen := 8 + len(mmapProvingKeyMagic) + if len(data) < trailerLen { + return meta, 0, fmt.Errorf("file too small") + } + magicOff := len(data) - len(mmapProvingKeyMagic) + if !bytes.Equal(data[magicOff:], mmapProvingKeyMagic[:]) { + return meta, 0, fmt.Errorf("invalid mmap dump magic") + } + metaLenOff := magicOff - 8 + metaLen := binary.LittleEndian.Uint64(data[metaLenOff:magicOff]) + if metaLen == 0 || metaLen > uint64(metaLenOff) { + return meta, 0, fmt.Errorf("invalid metadata length %d", metaLen) + } + metaOff := metaLenOff - int(metaLen) + if err := json.Unmarshal(data[metaOff:metaLenOff], &meta); err != nil { + return meta, 0, fmt.Errorf("unmarshal metadata: %w", err) + } + return meta, metaOff, nil +} + +func validateMmapDumpMeta(meta mmapDumpMeta) error { + if meta.Format != mmapProvingKeyFormat { + return fmt.Errorf("unsupported format %q", meta.Format) + } + if meta.Version != mmapProvingKeyVersion { + return fmt.Errorf("unsupported mmap dump version %d", meta.Version) + } + if meta.Curve != "bls12-381" { + return fmt.Errorf("unsupported curve %q", meta.Curve) + } + if meta.GOOS != runtime.GOOS { + return fmt.Errorf("goos mismatch: dump=%s runtime=%s", meta.GOOS, runtime.GOOS) + } + if meta.GOARCH != runtime.GOARCH { + return fmt.Errorf("goarch mismatch: dump=%s runtime=%s", meta.GOARCH, runtime.GOARCH) + } + if meta.GoCompiler != "" && meta.GoCompiler != runtime.Compiler { + return fmt.Errorf("go compiler mismatch: dump=%s runtime=%s", meta.GoCompiler, runtime.Compiler) + } + if meta.GoVersion != "" && meta.GoVersion != runtime.Version() { + return fmt.Errorf("go version mismatch: dump=%s runtime=%s", meta.GoVersion, runtime.Version()) + } + if meta.Endian != nativeEndian() { + return fmt.Errorf("endianness mismatch: dump=%s runtime=%s", meta.Endian, nativeEndian()) + } + if meta.PointerSize != int(unsafe.Sizeof(uintptr(0))) { + return fmt.Errorf("pointer size mismatch: dump=%d runtime=%d", meta.PointerSize, unsafe.Sizeof(uintptr(0))) + } + if meta.Sizes != currentMmapDumpSizes() { + return fmt.Errorf("type size mismatch: dump=%+v runtime=%+v", meta.Sizes, currentMmapDumpSizes()) + } + gnarkVersion, gnarkCryptoVersion := dependencyVersions() + if meta.GnarkVersion != "" && gnarkVersion != "" && meta.GnarkVersion != gnarkVersion { + return fmt.Errorf("gnark version mismatch: dump=%s runtime=%s", meta.GnarkVersion, gnarkVersion) + } + if meta.GnarkCryptoVersion != "" && gnarkCryptoVersion != "" && meta.GnarkCryptoVersion != gnarkCryptoVersion { + return fmt.Errorf("gnark-crypto version mismatch: dump=%s runtime=%s", meta.GnarkCryptoVersion, gnarkCryptoVersion) + } + return nil +} + +type namedMmapDumpSection struct { + name string + section mmapDumpSection +} + +func validateMmapDumpSections(meta mmapDumpMeta, payloadLen int) error { + sections := []namedMmapDumpSection{ + {"domain", meta.Domain}, + {"alpha_g1", meta.AlphaG1}, + {"beta_g1", meta.BetaG1}, + {"delta_g1", meta.DeltaG1}, + {"beta_g2", meta.BetaG2}, + {"delta_g2", meta.DeltaG2}, + {"g1_a", meta.G1A}, + {"g1_b", meta.G1B}, + {"g1_z", meta.G1Z}, + {"g1_k", meta.G1K}, + {"g2_b", meta.G2B}, + {"infinity_a", meta.InfinityA}, + {"infinity_b", meta.InfinityB}, + } + for i := range meta.CommitmentKeys { + sections = append(sections, + namedMmapDumpSection{fmt.Sprintf("commitment_keys[%d].basis", i), meta.CommitmentKeys[i].Basis}, + namedMmapDumpSection{fmt.Sprintf("commitment_keys[%d].basis_exp_sigma", i), meta.CommitmentKeys[i].BasisExpSigma}, + ) + } + + ranges := make([]struct { + name string + start, end int64 + }, 0, len(sections)) + for _, named := range sections { + start, end, err := mmapDumpSectionRange(named.name, named.section, payloadLen) + if err != nil { + return err + } + if start == end { + continue + } + for _, prev := range ranges { + if start < prev.end && prev.start < end { + return fmt.Errorf("section %s overlaps section %s", named.name, prev.name) + } + } + ranges = append(ranges, struct { + name string + start, end int64 + }{named.name, start, end}) + } + return nil +} + +func mmapDumpSectionRange(name string, section mmapDumpSection, payloadLen int) (int64, int64, error) { + if section.Offset < 0 || section.Len < 0 || section.ElementSize <= 0 { + return 0, 0, fmt.Errorf("invalid section %s: %+v", name, section) + } + if section.Offset%mmapProvingKeyAlign != 0 { + return 0, 0, fmt.Errorf("section %s offset %d is not aligned to %d", name, section.Offset, mmapProvingKeyAlign) + } + if section.Offset > int64(payloadLen) { + return 0, 0, fmt.Errorf("section %s starts outside payload: offset=%d payload=%d", name, section.Offset, payloadLen) + } + if section.Len == 0 { + return section.Offset, section.Offset, nil + } + const maxInt64 = int64(^uint64(0) >> 1) + if section.Len > maxInt64/section.ElementSize { + return 0, 0, fmt.Errorf("section %s size overflow", name) + } + size := section.Len * section.ElementSize + if size > int64(payloadLen)-section.Offset { + return 0, 0, fmt.Errorf("section %s out of bounds: offset=%d len=%d element_size=%d payload=%d", name, section.Offset, section.Len, section.ElementSize, payloadLen) + } + return section.Offset, section.Offset + size, nil +} + +func validateMmapDumpProvingKey(pk *ProvingKey) error { + if pk.NbInfinityA > uint64(len(pk.InfinityA)) { + return fmt.Errorf("NbInfinityA=%d exceeds InfinityA length %d", pk.NbInfinityA, len(pk.InfinityA)) + } + if pk.NbInfinityB > uint64(len(pk.InfinityB)) { + return fmt.Errorf("NbInfinityB=%d exceeds InfinityB length %d", pk.NbInfinityB, len(pk.InfinityB)) + } + if uint64(len(pk.G1.A))+pk.NbInfinityA != uint64(len(pk.InfinityA)) { + return fmt.Errorf("inconsistent G1.A and InfinityA lengths: len(G1.A)=%d NbInfinityA=%d len(InfinityA)=%d", len(pk.G1.A), pk.NbInfinityA, len(pk.InfinityA)) + } + if uint64(len(pk.G1.B))+pk.NbInfinityB != uint64(len(pk.InfinityB)) { + return fmt.Errorf("inconsistent G1.B and InfinityB lengths: len(G1.B)=%d NbInfinityB=%d len(InfinityB)=%d", len(pk.G1.B), pk.NbInfinityB, len(pk.InfinityB)) + } + if len(pk.G2.B) != len(pk.G1.B) { + return fmt.Errorf("inconsistent B vector lengths: len(G1.B)=%d len(G2.B)=%d", len(pk.G1.B), len(pk.G2.B)) + } + if uint64(len(pk.G1.Z))+1 != pk.Domain.Cardinality { + return fmt.Errorf("inconsistent Z length: len(G1.Z)=%d domain cardinality=%d", len(pk.G1.Z), pk.Domain.Cardinality) + } + for i := range pk.CommitmentKeys { + if len(pk.CommitmentKeys[i].Basis) != len(pk.CommitmentKeys[i].BasisExpSigma) { + return fmt.Errorf("inconsistent commitment key %d lengths: len(Basis)=%d len(BasisExpSigma)=%d", i, len(pk.CommitmentKeys[i].Basis), len(pk.CommitmentKeys[i].BasisExpSigma)) + } + } + return nil +} + +func sectionValue[T any](data []byte, section mmapDumpSection) (T, error) { + var zero T + s, err := sectionSlice[T](data, section) + if err != nil { + return zero, err + } + if len(s) != 1 { + return zero, fmt.Errorf("expected one element, got %d", len(s)) + } + return s[0], nil +} + +func sectionSlice[T any](data []byte, section mmapDumpSection) ([]T, error) { + var zero T + size := int64(unsafe.Sizeof(zero)) + align := int64(unsafe.Alignof(zero)) + if section.ElementSize != size { + return nil, fmt.Errorf("element size mismatch: section=%d type=%d", section.ElementSize, size) + } + if section.Len == 0 { + return nil, nil + } + if section.Offset%align != 0 { + return nil, fmt.Errorf("section offset %d is not aligned to %d", section.Offset, align) + } + b, err := sectionBytes(data, section) + if err != nil { + return nil, err + } + if uintptr(unsafe.Pointer(&b[0]))%uintptr(align) != 0 { + return nil, fmt.Errorf("mapped section address is not aligned to %d", align) + } + return unsafe.Slice((*T)(unsafe.Pointer(&b[0])), int(section.Len)), nil +} + +func sectionBytes(data []byte, section mmapDumpSection) ([]byte, error) { + if section.Offset < 0 || section.Len < 0 || section.ElementSize <= 0 { + return nil, fmt.Errorf("invalid section %+v", section) + } + if section.Len == 0 { + return nil, nil + } + const maxInt64 = int64(^uint64(0) >> 1) + if section.Len > maxInt64/section.ElementSize { + return nil, fmt.Errorf("section size overflow") + } + size := section.Len * section.ElementSize + if section.Offset > int64(len(data)) || size > int64(len(data))-section.Offset { + return nil, fmt.Errorf("section out of bounds: offset=%d len=%d element_size=%d file=%d", section.Offset, section.Len, section.ElementSize, len(data)) + } + return data[section.Offset : section.Offset+size], nil +} + +func disableDomainPrecompute(domain []byte, threshold uint64) { + if len(domain) < 9 { + return + } + cardinality := binary.BigEndian.Uint64(domain[:8]) + if cardinality >= threshold { + // fft.Domain.WriteTo serializes the withPrecompute bool as the final byte. + domain[len(domain)-1] = 0 + } +} + +func nativeEndian() string { + var x uint16 = 1 + if *(*byte)(unsafe.Pointer(&x)) == 1 { + return "little" + } + return "big" +} + +func dependencyVersions() (string, string) { + info, ok := debug.ReadBuildInfo() + if !ok { + return "", "" + } + var gnarkVersion, gnarkCryptoVersion string + if info.Main.Path == "github.com/consensys/gnark" { + gnarkVersion = info.Main.Version + } + for _, dep := range info.Deps { + switch dep.Path { + case "github.com/consensys/gnark": + gnarkVersion = dep.Version + case "github.com/consensys/gnark-crypto": + gnarkCryptoVersion = dep.Version + } + } + return gnarkVersion, gnarkCryptoVersion +} diff --git a/backend/groth16/bls12-381/mmapdump_test.go b/backend/groth16/bls12-381/mmapdump_test.go new file mode 100644 index 0000000000..9562d6e351 --- /dev/null +++ b/backend/groth16/bls12-381/mmapdump_test.go @@ -0,0 +1,316 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package groth16 + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/consensys/gnark-crypto/ecc" + + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + "github.com/consensys/gnark/backend/witness" + + cs "github.com/consensys/gnark/constraint/bls12-381" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/frontend/cs/r1cs" + "github.com/consensys/gnark/internal/backend/ioutils/mmap" +) + +func skipMmapDumpUnsupported(t testing.TB) { + t.Helper() + if !mmap.Supported() { + t.Skip("mmap is unsupported on this platform") + } +} + +func TestMmapDumpUnsupportedPlatform(t *testing.T) { + if mmap.Supported() { + t.Skip("mmap is supported on this platform") + } + + var pk ProvingKey + path := filepath.Join(t.TempDir(), "proving_key.mmap") + if err := pk.WriteMmapDump(path); err == nil { + t.Fatal("expected error") + } + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatalf("expected no dump file to be created, got %v", err) + } +} + +type mmapDumpCircuit struct { + X frontend.Variable + Y frontend.Variable `gnark:",public"` +} + +func (c *mmapDumpCircuit) Define(api frontend.API) error { + api.AssertIsEqual(api.Mul(c.X, c.X, c.X), c.Y) + return nil +} + +type mmapDumpCommitmentCircuit struct { + One frontend.Variable + Two frontend.Variable `gnark:",public"` +} + +func (c *mmapDumpCommitmentCircuit) Define(api frontend.API) error { + commitCompiler, ok := api.(frontend.Committer) + if !ok { + return fmt.Errorf("compiler does not commit") + } + commitment, err := commitCompiler.Commit(c.One, c.Two) + if err != nil { + return err + } + api.AssertIsDifferent(commitment, 0) + api.AssertIsEqual(c.One, 1) + api.AssertIsEqual(c.Two, 2) + return nil +} + +const mmapDumpBenchmarkCircuitSize = 2048 + +type mmapDumpBenchmarkCircuit struct { + X [mmapDumpBenchmarkCircuitSize]frontend.Variable + Y frontend.Variable `gnark:",public"` +} + +func (c *mmapDumpBenchmarkCircuit) Define(api frontend.API) error { + acc := frontend.Variable(1) + for i := range c.X { + acc = api.Mul(acc, api.Add(c.X[i], 1)) + } + api.AssertIsEqual(acc, c.Y) + return nil +} + +func TestMmapDumpProvingKeyRoundTrip(t *testing.T) { + skipMmapDumpUnsupported(t) + + ccs, err := frontend.Compile(ecc.BLS12_381.ScalarField(), r1cs.NewBuilder, &mmapDumpCircuit{}) + if err != nil { + t.Fatal(err) + } + r1cs := ccs.(*cs.R1CS) + + var pk ProvingKey + var vk VerifyingKey + if err := Setup(r1cs, &pk, &vk); err != nil { + t.Fatal(err) + } + + path := filepath.Join(t.TempDir(), "proving_key.mmap") + if err := pk.WriteMmapDump(path); err != nil { + t.Fatal(err) + } + + mappedPK, err := ReadMmapDump(path, WithMmapDumpNoDomainPrecompute(1)) + if err != nil { + t.Fatal(err) + } + defer mappedPK.Close() + + fullWitness, publicWitness := mmapDumpWitnesses(t) + proof, err := Prove(r1cs, &mappedPK.ProvingKey, fullWitness) + if err != nil { + t.Fatal(err) + } + if err := Verify(proof, &vk, publicWitness.Vector().(fr.Vector)); err != nil { + t.Fatal(err) + } + + if err := mappedPK.Close(); err != nil { + t.Fatal(err) + } + if mappedPK.G1.A != nil { + t.Fatal("expected mapped key to be cleared after close") + } +} + +func TestMmapDumpProvingKeyWithCommitment(t *testing.T) { + skipMmapDumpUnsupported(t) + + ccs, err := frontend.Compile(ecc.BLS12_381.ScalarField(), r1cs.NewBuilder, &mmapDumpCommitmentCircuit{}) + if err != nil { + t.Fatal(err) + } + r1cs := ccs.(*cs.R1CS) + + var pk ProvingKey + var vk VerifyingKey + if err := Setup(r1cs, &pk, &vk); err != nil { + t.Fatal(err) + } + if len(pk.CommitmentKeys) == 0 { + t.Fatal("expected commitment proving keys") + } + + path := filepath.Join(t.TempDir(), "proving_key.mmap") + if err := pk.WriteMmapDump(path); err != nil { + t.Fatal(err) + } + + mappedPK, err := ReadMmapDump(path) + if err != nil { + t.Fatal(err) + } + defer mappedPK.Close() + + assignment := mmapDumpCommitmentCircuit{One: 1, Two: 2} + fullWitness, err := frontend.NewWitness(&assignment, ecc.BLS12_381.ScalarField()) + if err != nil { + t.Fatal(err) + } + publicWitness, err := fullWitness.Public() + if err != nil { + t.Fatal(err) + } + + proof, err := Prove(r1cs, &mappedPK.ProvingKey, fullWitness) + if err != nil { + t.Fatal(err) + } + if err := Verify(proof, &vk, publicWitness.Vector().(fr.Vector)); err != nil { + t.Fatal(err) + } +} + +func TestMmapDumpPreservesDomainByDefault(t *testing.T) { + skipMmapDumpUnsupported(t) + + ccs, err := frontend.Compile(ecc.BLS12_381.ScalarField(), r1cs.NewBuilder, &mmapDumpCircuit{}) + if err != nil { + t.Fatal(err) + } + r1cs := ccs.(*cs.R1CS) + + var pk ProvingKey + var vk VerifyingKey + if err := Setup(r1cs, &pk, &vk); err != nil { + t.Fatal(err) + } + + path := filepath.Join(t.TempDir(), "proving_key.mmap") + if err := pk.WriteMmapDump(path); err != nil { + t.Fatal(err) + } + + mappedPK, err := ReadMmapDump(path) + if err != nil { + t.Fatal(err) + } + defer mappedPK.Close() + + var originalDomain, mappedDomain bytes.Buffer + if _, err := pk.Domain.WriteTo(&originalDomain); err != nil { + t.Fatal(err) + } + if _, err := mappedPK.Domain.WriteTo(&mappedDomain); err != nil { + t.Fatal(err) + } + if !bytes.Equal(originalDomain.Bytes(), mappedDomain.Bytes()) { + t.Fatal("expected default mmap dump load to preserve serialized domain") + } +} + +func mmapDumpWitnesses(t *testing.T) (witness.Witness, witness.Witness) { + t.Helper() + + assignment := mmapDumpCircuit{X: 3, Y: 27} + fullWitness, err := frontend.NewWitness(&assignment, ecc.BLS12_381.ScalarField()) + if err != nil { + t.Fatal(err) + } + publicWitness, err := fullWitness.Public() + if err != nil { + t.Fatal(err) + } + return fullWitness, publicWitness +} + +func BenchmarkMmapDumpProvingKeyLoad(b *testing.B) { + skipMmapDumpUnsupported(b) + + pk := mmapDumpBenchmarkProvingKey(b) + + var dump bytes.Buffer + if err := pk.WriteDump(&dump); err != nil { + b.Fatal(err) + } + + mmapPath := filepath.Join(b.TempDir(), "proving_key.mmap") + if err := pk.WriteMmapDump(mmapPath); err != nil { + b.Fatal(err) + } + + dumpBytes := dump.Bytes() + + b.Run("ReadDump", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + var loaded ProvingKey + if err := loaded.ReadDump(bytes.NewReader(dumpBytes)); err != nil { + b.Fatal(err) + } + if len(loaded.G1.A) == 0 { + b.Fatal("empty proving key") + } + } + }) + + b.Run("ReadMmapDump", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + loaded, err := ReadMmapDump(mmapPath) + if err != nil { + b.Fatal(err) + } + if len(loaded.G1.A) == 0 { + b.Fatal("empty proving key") + } + if err := loaded.Close(); err != nil { + b.Fatal(err) + } + } + }) + + b.Run("ReadMmapDumpNoDomainPrecompute", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + loaded, err := ReadMmapDump(mmapPath, WithMmapDumpNoDomainPrecompute(1)) + if err != nil { + b.Fatal(err) + } + if len(loaded.G1.A) == 0 { + b.Fatal("empty proving key") + } + if err := loaded.Close(); err != nil { + b.Fatal(err) + } + } + }) +} + +func mmapDumpBenchmarkProvingKey(b *testing.B) *ProvingKey { + b.Helper() + + ccs, err := frontend.Compile(ecc.BLS12_381.ScalarField(), r1cs.NewBuilder, &mmapDumpBenchmarkCircuit{}) + if err != nil { + b.Fatal(err) + } + r1cs := ccs.(*cs.R1CS) + + var pk ProvingKey + var vk VerifyingKey + if err := Setup(r1cs, &pk, &vk); err != nil { + b.Fatal(err) + } + return &pk +} diff --git a/backend/groth16/bn254/mmapdump.go b/backend/groth16/bn254/mmapdump.go new file mode 100644 index 0000000000..e7f2b1245f --- /dev/null +++ b/backend/groth16/bn254/mmapdump.go @@ -0,0 +1,689 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package groth16 + +import ( + "bytes" + "encoding/binary" + "encoding/json" + "fmt" + "io" + "os" + "runtime" + "runtime/debug" + "unsafe" + + curve "github.com/consensys/gnark-crypto/ecc/bn254" + + "github.com/consensys/gnark-crypto/ecc/bn254/fr/pedersen" + "github.com/consensys/gnark/internal/backend/ioutils/mmap" +) + +const ( + mmapProvingKeyFormat = "gnark.groth16.bn254.proving_key.mmap" + mmapProvingKeyVersion = 1 + mmapProvingKeyAlign = 64 +) + +var mmapProvingKeyMagic = [16]byte{'g', 'n', 'a', 'r', 'k', 'p', 'k', 'm', 'm', 'a', 'p', 'v', '1', '\r', '\n', 0} + +// MmapDumpOption configures ReadMmapDump. +type MmapDumpOption func(*mmapDumpConfig) + +type mmapDumpConfig struct { + disableDomainPrecompute bool + domainNoPrecomputeThreshold uint64 +} + +// WithMmapDumpNoDomainPrecompute disables FFT domain precomputation while +// loading a mapped dump once the serialized domain cardinality is greater than +// or equal to threshold. Passing 0 disables precomputation for every domain. +func WithMmapDumpNoDomainPrecompute(threshold uint64) MmapDumpOption { + return func(cfg *mmapDumpConfig) { + cfg.disableDomainPrecompute = true + cfg.domainNoPrecomputeThreshold = threshold + } +} + +// MmapProvingKey is a Groth16 BN254 proving key backed by a read-only memory +// mapping. Close must not be called while the proving key is in use. +type MmapProvingKey struct { + ProvingKey + + mapping io.Closer +} + +// Close releases the memory mapping backing pk. The key must not be used after +// Close returns. +func (pk *MmapProvingKey) Close() error { + if pk == nil || pk.mapping == nil { + return nil + } + err := pk.mapping.Close() + pk.mapping = nil + pk.ProvingKey = ProvingKey{} + return err +} + +type mmapDumpMeta struct { + Format string `json:"format"` + Version int `json:"version"` + Curve string `json:"curve"` + GOOS string `json:"goos"` + GOARCH string `json:"goarch"` + GoCompiler string `json:"go_compiler"` + GoVersion string `json:"go_version"` + Endian string `json:"endian"` + PointerSize int `json:"pointer_size"` + + GnarkVersion string `json:"gnark_version,omitempty"` + GnarkCryptoVersion string `json:"gnark_crypto_version,omitempty"` + + Sizes mmapDumpSizes `json:"sizes"` + + NbInfinityA uint64 `json:"nb_infinity_a"` + NbInfinityB uint64 `json:"nb_infinity_b"` + + Domain mmapDumpSection `json:"domain"` + + AlphaG1 mmapDumpSection `json:"alpha_g1"` + BetaG1 mmapDumpSection `json:"beta_g1"` + DeltaG1 mmapDumpSection `json:"delta_g1"` + BetaG2 mmapDumpSection `json:"beta_g2"` + DeltaG2 mmapDumpSection `json:"delta_g2"` + + G1A mmapDumpSection `json:"g1_a"` + G1B mmapDumpSection `json:"g1_b"` + G1Z mmapDumpSection `json:"g1_z"` + G1K mmapDumpSection `json:"g1_k"` + G2B mmapDumpSection `json:"g2_b"` + + InfinityA mmapDumpSection `json:"infinity_a"` + InfinityB mmapDumpSection `json:"infinity_b"` + + CommitmentKeys []mmapDumpCommitmentKey `json:"commitment_keys,omitempty"` +} + +type mmapDumpSizes struct { + G1Affine int `json:"g1_affine"` + G2Affine int `json:"g2_affine"` + Bool int `json:"bool"` +} + +type mmapDumpSection struct { + Offset int64 `json:"offset"` + Len int64 `json:"len"` + ElementSize int64 `json:"element_size"` +} + +type mmapDumpCommitmentKey struct { + Basis mmapDumpSection `json:"basis"` + BasisExpSigma mmapDumpSection `json:"basis_exp_sigma"` +} + +// WriteMmapDump writes pk to path using an aligned, file-backed dump format. +// +// The dump stores large proving-key slices as raw Go memory. It is intended for +// trusted local artifacts only and is not portable across incompatible gnark +// versions, gnark-crypto versions, architectures, endianness, or Go type +// layouts. +func (pk *ProvingKey) WriteMmapDump(path string) (err error) { + if !mmap.Supported() { + return fmt.Errorf("mmap is unsupported on this platform") + } + + f, err := os.Create(path) + if err != nil { + return err + } + defer func() { + if closeErr := f.Close(); err == nil { + err = closeErr + } + }() + + w := mmapDumpWriter{w: f} + meta := newMmapDumpMeta(pk) + + var domain bytes.Buffer + if _, err := pk.Domain.WriteTo(&domain); err != nil { + return fmt.Errorf("write domain: %w", err) + } + + if meta.Domain, err = w.writeBytes(domain.Bytes(), 1); err != nil { + return fmt.Errorf("write domain section: %w", err) + } + + if meta.AlphaG1, err = writeMmapDumpValue(&w, &pk.G1.Alpha); err != nil { + return fmt.Errorf("write G1.Alpha: %w", err) + } + if meta.BetaG1, err = writeMmapDumpValue(&w, &pk.G1.Beta); err != nil { + return fmt.Errorf("write G1.Beta: %w", err) + } + if meta.DeltaG1, err = writeMmapDumpValue(&w, &pk.G1.Delta); err != nil { + return fmt.Errorf("write G1.Delta: %w", err) + } + if meta.BetaG2, err = writeMmapDumpValue(&w, &pk.G2.Beta); err != nil { + return fmt.Errorf("write G2.Beta: %w", err) + } + if meta.DeltaG2, err = writeMmapDumpValue(&w, &pk.G2.Delta); err != nil { + return fmt.Errorf("write G2.Delta: %w", err) + } + + if meta.G1A, err = writeMmapDumpSlice(&w, pk.G1.A); err != nil { + return fmt.Errorf("write G1.A: %w", err) + } + if meta.G1B, err = writeMmapDumpSlice(&w, pk.G1.B); err != nil { + return fmt.Errorf("write G1.B: %w", err) + } + if meta.G1Z, err = writeMmapDumpSlice(&w, pk.G1.Z); err != nil { + return fmt.Errorf("write G1.Z: %w", err) + } + if meta.G1K, err = writeMmapDumpSlice(&w, pk.G1.K); err != nil { + return fmt.Errorf("write G1.K: %w", err) + } + if meta.G2B, err = writeMmapDumpSlice(&w, pk.G2.B); err != nil { + return fmt.Errorf("write G2.B: %w", err) + } + if meta.InfinityA, err = writeMmapDumpSlice(&w, pk.InfinityA); err != nil { + return fmt.Errorf("write InfinityA: %w", err) + } + if meta.InfinityB, err = writeMmapDumpSlice(&w, pk.InfinityB); err != nil { + return fmt.Errorf("write InfinityB: %w", err) + } + + if len(pk.CommitmentKeys) > 0 { + meta.CommitmentKeys = make([]mmapDumpCommitmentKey, len(pk.CommitmentKeys)) + for i := range pk.CommitmentKeys { + if meta.CommitmentKeys[i].Basis, err = writeMmapDumpSlice(&w, pk.CommitmentKeys[i].Basis); err != nil { + return fmt.Errorf("write commitment key %d basis: %w", i, err) + } + if meta.CommitmentKeys[i].BasisExpSigma, err = writeMmapDumpSlice(&w, pk.CommitmentKeys[i].BasisExpSigma); err != nil { + return fmt.Errorf("write commitment key %d basis exp sigma: %w", i, err) + } + } + } + + metaBytes, err := json.Marshal(meta) + if err != nil { + return fmt.Errorf("marshal metadata: %w", err) + } + if _, err := f.Write(metaBytes); err != nil { + return fmt.Errorf("write metadata: %w", err) + } + if err := binary.Write(f, binary.LittleEndian, uint64(len(metaBytes))); err != nil { + return fmt.Errorf("write metadata length: %w", err) + } + if _, err := f.Write(mmapProvingKeyMagic[:]); err != nil { + return fmt.Errorf("write magic: %w", err) + } + + return nil +} + +// ReadMmapDump maps path into memory and returns a proving key whose large +// slices are backed by the mapped file. The caller must keep the returned key +// open while it is used and call Close after proving is complete. +// +// Mmap dumps are unsafe raw-memory artifacts. ReadMmapDump validates the dump +// metadata against the current process, but it does not validate curve points or +// subgroup membership. +func ReadMmapDump(path string, opts ...MmapDumpOption) (*MmapProvingKey, error) { + var cfg mmapDumpConfig + for _, opt := range opts { + opt(&cfg) + } + + mapping, err := mmap.Open(path) + if err != nil { + return nil, err + } + pk := &MmapProvingKey{mapping: mapping} + if err := pk.ProvingKey.readMmapDump(mapping.Data, cfg); err != nil { + _ = mapping.Close() + return nil, err + } + return pk, nil +} + +func (pk *ProvingKey) readMmapDump(data []byte, cfg mmapDumpConfig) error { + meta, payloadLen, err := readMmapDumpMeta(data) + if err != nil { + return err + } + if err := validateMmapDumpMeta(meta); err != nil { + return err + } + if err := validateMmapDumpSections(meta, payloadLen); err != nil { + return err + } + payload := data[:payloadLen] + + pk.NbInfinityA = meta.NbInfinityA + pk.NbInfinityB = meta.NbInfinityB + + domainBytes, err := sectionBytes(payload, meta.Domain) + if err != nil { + return fmt.Errorf("domain section: %w", err) + } + domainBytes = append([]byte(nil), domainBytes...) + if cfg.disableDomainPrecompute && len(domainBytes) > 0 { + disableDomainPrecompute(domainBytes, cfg.domainNoPrecomputeThreshold) + } + if _, err := pk.Domain.ReadFrom(bytes.NewReader(domainBytes)); err != nil { + return fmt.Errorf("read domain: %w", err) + } + + if pk.G1.Alpha, err = sectionValue[curve.G1Affine](payload, meta.AlphaG1); err != nil { + return fmt.Errorf("read G1.Alpha: %w", err) + } + if pk.G1.Beta, err = sectionValue[curve.G1Affine](payload, meta.BetaG1); err != nil { + return fmt.Errorf("read G1.Beta: %w", err) + } + if pk.G1.Delta, err = sectionValue[curve.G1Affine](payload, meta.DeltaG1); err != nil { + return fmt.Errorf("read G1.Delta: %w", err) + } + if pk.G2.Beta, err = sectionValue[curve.G2Affine](payload, meta.BetaG2); err != nil { + return fmt.Errorf("read G2.Beta: %w", err) + } + if pk.G2.Delta, err = sectionValue[curve.G2Affine](payload, meta.DeltaG2); err != nil { + return fmt.Errorf("read G2.Delta: %w", err) + } + + if pk.G1.A, err = sectionSlice[curve.G1Affine](payload, meta.G1A); err != nil { + return fmt.Errorf("read G1.A: %w", err) + } + if pk.G1.B, err = sectionSlice[curve.G1Affine](payload, meta.G1B); err != nil { + return fmt.Errorf("read G1.B: %w", err) + } + if pk.G1.Z, err = sectionSlice[curve.G1Affine](payload, meta.G1Z); err != nil { + return fmt.Errorf("read G1.Z: %w", err) + } + if pk.G1.K, err = sectionSlice[curve.G1Affine](payload, meta.G1K); err != nil { + return fmt.Errorf("read G1.K: %w", err) + } + if pk.G2.B, err = sectionSlice[curve.G2Affine](payload, meta.G2B); err != nil { + return fmt.Errorf("read G2.B: %w", err) + } + if pk.InfinityA, err = sectionSlice[bool](payload, meta.InfinityA); err != nil { + return fmt.Errorf("read InfinityA: %w", err) + } + if pk.InfinityB, err = sectionSlice[bool](payload, meta.InfinityB); err != nil { + return fmt.Errorf("read InfinityB: %w", err) + } + + if len(meta.CommitmentKeys) > 0 { + pk.CommitmentKeys = make([]pedersen.ProvingKey, len(meta.CommitmentKeys)) + for i := range meta.CommitmentKeys { + if pk.CommitmentKeys[i].Basis, err = sectionSlice[curve.G1Affine](payload, meta.CommitmentKeys[i].Basis); err != nil { + return fmt.Errorf("read commitment key %d basis: %w", i, err) + } + if pk.CommitmentKeys[i].BasisExpSigma, err = sectionSlice[curve.G1Affine](payload, meta.CommitmentKeys[i].BasisExpSigma); err != nil { + return fmt.Errorf("read commitment key %d basis exp sigma: %w", i, err) + } + } + } else { + pk.CommitmentKeys = nil + } + + return validateMmapDumpProvingKey(pk) +} + +type mmapDumpWriter struct { + w io.Writer + pos int64 +} + +func writeMmapDumpValue[T any](w *mmapDumpWriter, v *T) (mmapDumpSection, error) { + var zero T + size := int64(unsafe.Sizeof(zero)) + b := unsafe.Slice((*byte)(unsafe.Pointer(v)), int(size)) + return w.writeBytes(b, size) +} + +func writeMmapDumpSlice[S ~[]E, E any](w *mmapDumpWriter, s S) (mmapDumpSection, error) { + var zero E + size := int64(unsafe.Sizeof(zero)) + if len(s) == 0 { + return w.writeBytes(nil, size) + } + b := unsafe.Slice((*byte)(unsafe.Pointer(&s[0])), len(s)*int(size)) + return w.writeBytes(b, size) +} + +func (w *mmapDumpWriter) writeBytes(b []byte, elementSize int64) (mmapDumpSection, error) { + if elementSize <= 0 { + return mmapDumpSection{}, fmt.Errorf("invalid element size %d", elementSize) + } + if err := w.padTo(mmapProvingKeyAlign); err != nil { + return mmapDumpSection{}, err + } + section := mmapDumpSection{ + Offset: w.pos, + ElementSize: elementSize, + } + if len(b)%int(elementSize) != 0 { + return mmapDumpSection{}, fmt.Errorf("section length %d is not a multiple of element size %d", len(b), elementSize) + } + section.Len = int64(len(b)) / elementSize + if len(b) == 0 { + return section, nil + } + n, err := w.w.Write(b) + w.pos += int64(n) + if err != nil { + return mmapDumpSection{}, err + } + if n != len(b) { + return mmapDumpSection{}, io.ErrShortWrite + } + return section, nil +} + +func (w *mmapDumpWriter) padTo(alignment int64) error { + rem := w.pos % alignment + if rem == 0 { + return nil + } + padding := make([]byte, alignment-rem) + n, err := w.w.Write(padding) + w.pos += int64(n) + if err != nil { + return err + } + if n != len(padding) { + return io.ErrShortWrite + } + return nil +} + +func newMmapDumpMeta(pk *ProvingKey) mmapDumpMeta { + gnarkVersion, gnarkCryptoVersion := dependencyVersions() + return mmapDumpMeta{ + Format: mmapProvingKeyFormat, + Version: mmapProvingKeyVersion, + Curve: "bn254", + GOOS: runtime.GOOS, + GOARCH: runtime.GOARCH, + GoCompiler: runtime.Compiler, + GoVersion: runtime.Version(), + Endian: nativeEndian(), + PointerSize: int(unsafe.Sizeof(uintptr(0))), + GnarkVersion: gnarkVersion, + GnarkCryptoVersion: gnarkCryptoVersion, + Sizes: currentMmapDumpSizes(), + NbInfinityA: pk.NbInfinityA, + NbInfinityB: pk.NbInfinityB, + CommitmentKeys: nil, + } +} + +func currentMmapDumpSizes() mmapDumpSizes { + return mmapDumpSizes{ + G1Affine: int(unsafe.Sizeof(curve.G1Affine{})), + G2Affine: int(unsafe.Sizeof(curve.G2Affine{})), + Bool: int(unsafe.Sizeof(false)), + } +} + +func readMmapDumpMeta(data []byte) (mmapDumpMeta, int, error) { + var meta mmapDumpMeta + trailerLen := 8 + len(mmapProvingKeyMagic) + if len(data) < trailerLen { + return meta, 0, fmt.Errorf("file too small") + } + magicOff := len(data) - len(mmapProvingKeyMagic) + if !bytes.Equal(data[magicOff:], mmapProvingKeyMagic[:]) { + return meta, 0, fmt.Errorf("invalid mmap dump magic") + } + metaLenOff := magicOff - 8 + metaLen := binary.LittleEndian.Uint64(data[metaLenOff:magicOff]) + if metaLen == 0 || metaLen > uint64(metaLenOff) { + return meta, 0, fmt.Errorf("invalid metadata length %d", metaLen) + } + metaOff := metaLenOff - int(metaLen) + if err := json.Unmarshal(data[metaOff:metaLenOff], &meta); err != nil { + return meta, 0, fmt.Errorf("unmarshal metadata: %w", err) + } + return meta, metaOff, nil +} + +func validateMmapDumpMeta(meta mmapDumpMeta) error { + if meta.Format != mmapProvingKeyFormat { + return fmt.Errorf("unsupported format %q", meta.Format) + } + if meta.Version != mmapProvingKeyVersion { + return fmt.Errorf("unsupported mmap dump version %d", meta.Version) + } + if meta.Curve != "bn254" { + return fmt.Errorf("unsupported curve %q", meta.Curve) + } + if meta.GOOS != runtime.GOOS { + return fmt.Errorf("goos mismatch: dump=%s runtime=%s", meta.GOOS, runtime.GOOS) + } + if meta.GOARCH != runtime.GOARCH { + return fmt.Errorf("goarch mismatch: dump=%s runtime=%s", meta.GOARCH, runtime.GOARCH) + } + if meta.GoCompiler != "" && meta.GoCompiler != runtime.Compiler { + return fmt.Errorf("go compiler mismatch: dump=%s runtime=%s", meta.GoCompiler, runtime.Compiler) + } + if meta.GoVersion != "" && meta.GoVersion != runtime.Version() { + return fmt.Errorf("go version mismatch: dump=%s runtime=%s", meta.GoVersion, runtime.Version()) + } + if meta.Endian != nativeEndian() { + return fmt.Errorf("endianness mismatch: dump=%s runtime=%s", meta.Endian, nativeEndian()) + } + if meta.PointerSize != int(unsafe.Sizeof(uintptr(0))) { + return fmt.Errorf("pointer size mismatch: dump=%d runtime=%d", meta.PointerSize, unsafe.Sizeof(uintptr(0))) + } + if meta.Sizes != currentMmapDumpSizes() { + return fmt.Errorf("type size mismatch: dump=%+v runtime=%+v", meta.Sizes, currentMmapDumpSizes()) + } + gnarkVersion, gnarkCryptoVersion := dependencyVersions() + if meta.GnarkVersion != "" && gnarkVersion != "" && meta.GnarkVersion != gnarkVersion { + return fmt.Errorf("gnark version mismatch: dump=%s runtime=%s", meta.GnarkVersion, gnarkVersion) + } + if meta.GnarkCryptoVersion != "" && gnarkCryptoVersion != "" && meta.GnarkCryptoVersion != gnarkCryptoVersion { + return fmt.Errorf("gnark-crypto version mismatch: dump=%s runtime=%s", meta.GnarkCryptoVersion, gnarkCryptoVersion) + } + return nil +} + +type namedMmapDumpSection struct { + name string + section mmapDumpSection +} + +func validateMmapDumpSections(meta mmapDumpMeta, payloadLen int) error { + sections := []namedMmapDumpSection{ + {"domain", meta.Domain}, + {"alpha_g1", meta.AlphaG1}, + {"beta_g1", meta.BetaG1}, + {"delta_g1", meta.DeltaG1}, + {"beta_g2", meta.BetaG2}, + {"delta_g2", meta.DeltaG2}, + {"g1_a", meta.G1A}, + {"g1_b", meta.G1B}, + {"g1_z", meta.G1Z}, + {"g1_k", meta.G1K}, + {"g2_b", meta.G2B}, + {"infinity_a", meta.InfinityA}, + {"infinity_b", meta.InfinityB}, + } + for i := range meta.CommitmentKeys { + sections = append(sections, + namedMmapDumpSection{fmt.Sprintf("commitment_keys[%d].basis", i), meta.CommitmentKeys[i].Basis}, + namedMmapDumpSection{fmt.Sprintf("commitment_keys[%d].basis_exp_sigma", i), meta.CommitmentKeys[i].BasisExpSigma}, + ) + } + + ranges := make([]struct { + name string + start, end int64 + }, 0, len(sections)) + for _, named := range sections { + start, end, err := mmapDumpSectionRange(named.name, named.section, payloadLen) + if err != nil { + return err + } + if start == end { + continue + } + for _, prev := range ranges { + if start < prev.end && prev.start < end { + return fmt.Errorf("section %s overlaps section %s", named.name, prev.name) + } + } + ranges = append(ranges, struct { + name string + start, end int64 + }{named.name, start, end}) + } + return nil +} + +func mmapDumpSectionRange(name string, section mmapDumpSection, payloadLen int) (int64, int64, error) { + if section.Offset < 0 || section.Len < 0 || section.ElementSize <= 0 { + return 0, 0, fmt.Errorf("invalid section %s: %+v", name, section) + } + if section.Offset%mmapProvingKeyAlign != 0 { + return 0, 0, fmt.Errorf("section %s offset %d is not aligned to %d", name, section.Offset, mmapProvingKeyAlign) + } + if section.Offset > int64(payloadLen) { + return 0, 0, fmt.Errorf("section %s starts outside payload: offset=%d payload=%d", name, section.Offset, payloadLen) + } + if section.Len == 0 { + return section.Offset, section.Offset, nil + } + const maxInt64 = int64(^uint64(0) >> 1) + if section.Len > maxInt64/section.ElementSize { + return 0, 0, fmt.Errorf("section %s size overflow", name) + } + size := section.Len * section.ElementSize + if size > int64(payloadLen)-section.Offset { + return 0, 0, fmt.Errorf("section %s out of bounds: offset=%d len=%d element_size=%d payload=%d", name, section.Offset, section.Len, section.ElementSize, payloadLen) + } + return section.Offset, section.Offset + size, nil +} + +func validateMmapDumpProvingKey(pk *ProvingKey) error { + if pk.NbInfinityA > uint64(len(pk.InfinityA)) { + return fmt.Errorf("NbInfinityA=%d exceeds InfinityA length %d", pk.NbInfinityA, len(pk.InfinityA)) + } + if pk.NbInfinityB > uint64(len(pk.InfinityB)) { + return fmt.Errorf("NbInfinityB=%d exceeds InfinityB length %d", pk.NbInfinityB, len(pk.InfinityB)) + } + if uint64(len(pk.G1.A))+pk.NbInfinityA != uint64(len(pk.InfinityA)) { + return fmt.Errorf("inconsistent G1.A and InfinityA lengths: len(G1.A)=%d NbInfinityA=%d len(InfinityA)=%d", len(pk.G1.A), pk.NbInfinityA, len(pk.InfinityA)) + } + if uint64(len(pk.G1.B))+pk.NbInfinityB != uint64(len(pk.InfinityB)) { + return fmt.Errorf("inconsistent G1.B and InfinityB lengths: len(G1.B)=%d NbInfinityB=%d len(InfinityB)=%d", len(pk.G1.B), pk.NbInfinityB, len(pk.InfinityB)) + } + if len(pk.G2.B) != len(pk.G1.B) { + return fmt.Errorf("inconsistent B vector lengths: len(G1.B)=%d len(G2.B)=%d", len(pk.G1.B), len(pk.G2.B)) + } + if uint64(len(pk.G1.Z))+1 != pk.Domain.Cardinality { + return fmt.Errorf("inconsistent Z length: len(G1.Z)=%d domain cardinality=%d", len(pk.G1.Z), pk.Domain.Cardinality) + } + for i := range pk.CommitmentKeys { + if len(pk.CommitmentKeys[i].Basis) != len(pk.CommitmentKeys[i].BasisExpSigma) { + return fmt.Errorf("inconsistent commitment key %d lengths: len(Basis)=%d len(BasisExpSigma)=%d", i, len(pk.CommitmentKeys[i].Basis), len(pk.CommitmentKeys[i].BasisExpSigma)) + } + } + return nil +} + +func sectionValue[T any](data []byte, section mmapDumpSection) (T, error) { + var zero T + s, err := sectionSlice[T](data, section) + if err != nil { + return zero, err + } + if len(s) != 1 { + return zero, fmt.Errorf("expected one element, got %d", len(s)) + } + return s[0], nil +} + +func sectionSlice[T any](data []byte, section mmapDumpSection) ([]T, error) { + var zero T + size := int64(unsafe.Sizeof(zero)) + align := int64(unsafe.Alignof(zero)) + if section.ElementSize != size { + return nil, fmt.Errorf("element size mismatch: section=%d type=%d", section.ElementSize, size) + } + if section.Len == 0 { + return nil, nil + } + if section.Offset%align != 0 { + return nil, fmt.Errorf("section offset %d is not aligned to %d", section.Offset, align) + } + b, err := sectionBytes(data, section) + if err != nil { + return nil, err + } + if uintptr(unsafe.Pointer(&b[0]))%uintptr(align) != 0 { + return nil, fmt.Errorf("mapped section address is not aligned to %d", align) + } + return unsafe.Slice((*T)(unsafe.Pointer(&b[0])), int(section.Len)), nil +} + +func sectionBytes(data []byte, section mmapDumpSection) ([]byte, error) { + if section.Offset < 0 || section.Len < 0 || section.ElementSize <= 0 { + return nil, fmt.Errorf("invalid section %+v", section) + } + if section.Len == 0 { + return nil, nil + } + const maxInt64 = int64(^uint64(0) >> 1) + if section.Len > maxInt64/section.ElementSize { + return nil, fmt.Errorf("section size overflow") + } + size := section.Len * section.ElementSize + if section.Offset > int64(len(data)) || size > int64(len(data))-section.Offset { + return nil, fmt.Errorf("section out of bounds: offset=%d len=%d element_size=%d file=%d", section.Offset, section.Len, section.ElementSize, len(data)) + } + return data[section.Offset : section.Offset+size], nil +} + +func disableDomainPrecompute(domain []byte, threshold uint64) { + if len(domain) < 9 { + return + } + cardinality := binary.BigEndian.Uint64(domain[:8]) + if cardinality >= threshold { + // fft.Domain.WriteTo serializes the withPrecompute bool as the final byte. + domain[len(domain)-1] = 0 + } +} + +func nativeEndian() string { + var x uint16 = 1 + if *(*byte)(unsafe.Pointer(&x)) == 1 { + return "little" + } + return "big" +} + +func dependencyVersions() (string, string) { + info, ok := debug.ReadBuildInfo() + if !ok { + return "", "" + } + var gnarkVersion, gnarkCryptoVersion string + if info.Main.Path == "github.com/consensys/gnark" { + gnarkVersion = info.Main.Version + } + for _, dep := range info.Deps { + switch dep.Path { + case "github.com/consensys/gnark": + gnarkVersion = dep.Version + case "github.com/consensys/gnark-crypto": + gnarkCryptoVersion = dep.Version + } + } + return gnarkVersion, gnarkCryptoVersion +} diff --git a/backend/groth16/bn254/mmapdump_test.go b/backend/groth16/bn254/mmapdump_test.go new file mode 100644 index 0000000000..1437bd5fe5 --- /dev/null +++ b/backend/groth16/bn254/mmapdump_test.go @@ -0,0 +1,316 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package groth16 + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/consensys/gnark-crypto/ecc" + + "github.com/consensys/gnark-crypto/ecc/bn254/fr" + "github.com/consensys/gnark/backend/witness" + + cs "github.com/consensys/gnark/constraint/bn254" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/frontend/cs/r1cs" + "github.com/consensys/gnark/internal/backend/ioutils/mmap" +) + +func skipMmapDumpUnsupported(t testing.TB) { + t.Helper() + if !mmap.Supported() { + t.Skip("mmap is unsupported on this platform") + } +} + +func TestMmapDumpUnsupportedPlatform(t *testing.T) { + if mmap.Supported() { + t.Skip("mmap is supported on this platform") + } + + var pk ProvingKey + path := filepath.Join(t.TempDir(), "proving_key.mmap") + if err := pk.WriteMmapDump(path); err == nil { + t.Fatal("expected error") + } + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatalf("expected no dump file to be created, got %v", err) + } +} + +type mmapDumpCircuit struct { + X frontend.Variable + Y frontend.Variable `gnark:",public"` +} + +func (c *mmapDumpCircuit) Define(api frontend.API) error { + api.AssertIsEqual(api.Mul(c.X, c.X, c.X), c.Y) + return nil +} + +type mmapDumpCommitmentCircuit struct { + One frontend.Variable + Two frontend.Variable `gnark:",public"` +} + +func (c *mmapDumpCommitmentCircuit) Define(api frontend.API) error { + commitCompiler, ok := api.(frontend.Committer) + if !ok { + return fmt.Errorf("compiler does not commit") + } + commitment, err := commitCompiler.Commit(c.One, c.Two) + if err != nil { + return err + } + api.AssertIsDifferent(commitment, 0) + api.AssertIsEqual(c.One, 1) + api.AssertIsEqual(c.Two, 2) + return nil +} + +const mmapDumpBenchmarkCircuitSize = 2048 + +type mmapDumpBenchmarkCircuit struct { + X [mmapDumpBenchmarkCircuitSize]frontend.Variable + Y frontend.Variable `gnark:",public"` +} + +func (c *mmapDumpBenchmarkCircuit) Define(api frontend.API) error { + acc := frontend.Variable(1) + for i := range c.X { + acc = api.Mul(acc, api.Add(c.X[i], 1)) + } + api.AssertIsEqual(acc, c.Y) + return nil +} + +func TestMmapDumpProvingKeyRoundTrip(t *testing.T) { + skipMmapDumpUnsupported(t) + + ccs, err := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, &mmapDumpCircuit{}) + if err != nil { + t.Fatal(err) + } + r1cs := ccs.(*cs.R1CS) + + var pk ProvingKey + var vk VerifyingKey + if err := Setup(r1cs, &pk, &vk); err != nil { + t.Fatal(err) + } + + path := filepath.Join(t.TempDir(), "proving_key.mmap") + if err := pk.WriteMmapDump(path); err != nil { + t.Fatal(err) + } + + mappedPK, err := ReadMmapDump(path, WithMmapDumpNoDomainPrecompute(1)) + if err != nil { + t.Fatal(err) + } + defer mappedPK.Close() + + fullWitness, publicWitness := mmapDumpWitnesses(t) + proof, err := Prove(r1cs, &mappedPK.ProvingKey, fullWitness) + if err != nil { + t.Fatal(err) + } + if err := Verify(proof, &vk, publicWitness.Vector().(fr.Vector)); err != nil { + t.Fatal(err) + } + + if err := mappedPK.Close(); err != nil { + t.Fatal(err) + } + if mappedPK.G1.A != nil { + t.Fatal("expected mapped key to be cleared after close") + } +} + +func TestMmapDumpProvingKeyWithCommitment(t *testing.T) { + skipMmapDumpUnsupported(t) + + ccs, err := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, &mmapDumpCommitmentCircuit{}) + if err != nil { + t.Fatal(err) + } + r1cs := ccs.(*cs.R1CS) + + var pk ProvingKey + var vk VerifyingKey + if err := Setup(r1cs, &pk, &vk); err != nil { + t.Fatal(err) + } + if len(pk.CommitmentKeys) == 0 { + t.Fatal("expected commitment proving keys") + } + + path := filepath.Join(t.TempDir(), "proving_key.mmap") + if err := pk.WriteMmapDump(path); err != nil { + t.Fatal(err) + } + + mappedPK, err := ReadMmapDump(path) + if err != nil { + t.Fatal(err) + } + defer mappedPK.Close() + + assignment := mmapDumpCommitmentCircuit{One: 1, Two: 2} + fullWitness, err := frontend.NewWitness(&assignment, ecc.BN254.ScalarField()) + if err != nil { + t.Fatal(err) + } + publicWitness, err := fullWitness.Public() + if err != nil { + t.Fatal(err) + } + + proof, err := Prove(r1cs, &mappedPK.ProvingKey, fullWitness) + if err != nil { + t.Fatal(err) + } + if err := Verify(proof, &vk, publicWitness.Vector().(fr.Vector)); err != nil { + t.Fatal(err) + } +} + +func TestMmapDumpPreservesDomainByDefault(t *testing.T) { + skipMmapDumpUnsupported(t) + + ccs, err := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, &mmapDumpCircuit{}) + if err != nil { + t.Fatal(err) + } + r1cs := ccs.(*cs.R1CS) + + var pk ProvingKey + var vk VerifyingKey + if err := Setup(r1cs, &pk, &vk); err != nil { + t.Fatal(err) + } + + path := filepath.Join(t.TempDir(), "proving_key.mmap") + if err := pk.WriteMmapDump(path); err != nil { + t.Fatal(err) + } + + mappedPK, err := ReadMmapDump(path) + if err != nil { + t.Fatal(err) + } + defer mappedPK.Close() + + var originalDomain, mappedDomain bytes.Buffer + if _, err := pk.Domain.WriteTo(&originalDomain); err != nil { + t.Fatal(err) + } + if _, err := mappedPK.Domain.WriteTo(&mappedDomain); err != nil { + t.Fatal(err) + } + if !bytes.Equal(originalDomain.Bytes(), mappedDomain.Bytes()) { + t.Fatal("expected default mmap dump load to preserve serialized domain") + } +} + +func mmapDumpWitnesses(t *testing.T) (witness.Witness, witness.Witness) { + t.Helper() + + assignment := mmapDumpCircuit{X: 3, Y: 27} + fullWitness, err := frontend.NewWitness(&assignment, ecc.BN254.ScalarField()) + if err != nil { + t.Fatal(err) + } + publicWitness, err := fullWitness.Public() + if err != nil { + t.Fatal(err) + } + return fullWitness, publicWitness +} + +func BenchmarkMmapDumpProvingKeyLoad(b *testing.B) { + skipMmapDumpUnsupported(b) + + pk := mmapDumpBenchmarkProvingKey(b) + + var dump bytes.Buffer + if err := pk.WriteDump(&dump); err != nil { + b.Fatal(err) + } + + mmapPath := filepath.Join(b.TempDir(), "proving_key.mmap") + if err := pk.WriteMmapDump(mmapPath); err != nil { + b.Fatal(err) + } + + dumpBytes := dump.Bytes() + + b.Run("ReadDump", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + var loaded ProvingKey + if err := loaded.ReadDump(bytes.NewReader(dumpBytes)); err != nil { + b.Fatal(err) + } + if len(loaded.G1.A) == 0 { + b.Fatal("empty proving key") + } + } + }) + + b.Run("ReadMmapDump", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + loaded, err := ReadMmapDump(mmapPath) + if err != nil { + b.Fatal(err) + } + if len(loaded.G1.A) == 0 { + b.Fatal("empty proving key") + } + if err := loaded.Close(); err != nil { + b.Fatal(err) + } + } + }) + + b.Run("ReadMmapDumpNoDomainPrecompute", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + loaded, err := ReadMmapDump(mmapPath, WithMmapDumpNoDomainPrecompute(1)) + if err != nil { + b.Fatal(err) + } + if len(loaded.G1.A) == 0 { + b.Fatal("empty proving key") + } + if err := loaded.Close(); err != nil { + b.Fatal(err) + } + } + }) +} + +func mmapDumpBenchmarkProvingKey(b *testing.B) *ProvingKey { + b.Helper() + + ccs, err := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, &mmapDumpBenchmarkCircuit{}) + if err != nil { + b.Fatal(err) + } + r1cs := ccs.(*cs.R1CS) + + var pk ProvingKey + var vk VerifyingKey + if err := Setup(r1cs, &pk, &vk); err != nil { + b.Fatal(err) + } + return &pk +} diff --git a/backend/groth16/bw6-761/mmapdump.go b/backend/groth16/bw6-761/mmapdump.go new file mode 100644 index 0000000000..6c2e9421f7 --- /dev/null +++ b/backend/groth16/bw6-761/mmapdump.go @@ -0,0 +1,689 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package groth16 + +import ( + "bytes" + "encoding/binary" + "encoding/json" + "fmt" + "io" + "os" + "runtime" + "runtime/debug" + "unsafe" + + curve "github.com/consensys/gnark-crypto/ecc/bw6-761" + + "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/pedersen" + "github.com/consensys/gnark/internal/backend/ioutils/mmap" +) + +const ( + mmapProvingKeyFormat = "gnark.groth16.bw6-761.proving_key.mmap" + mmapProvingKeyVersion = 1 + mmapProvingKeyAlign = 64 +) + +var mmapProvingKeyMagic = [16]byte{'g', 'n', 'a', 'r', 'k', 'p', 'k', 'm', 'm', 'a', 'p', 'v', '1', '\r', '\n', 0} + +// MmapDumpOption configures ReadMmapDump. +type MmapDumpOption func(*mmapDumpConfig) + +type mmapDumpConfig struct { + disableDomainPrecompute bool + domainNoPrecomputeThreshold uint64 +} + +// WithMmapDumpNoDomainPrecompute disables FFT domain precomputation while +// loading a mapped dump once the serialized domain cardinality is greater than +// or equal to threshold. Passing 0 disables precomputation for every domain. +func WithMmapDumpNoDomainPrecompute(threshold uint64) MmapDumpOption { + return func(cfg *mmapDumpConfig) { + cfg.disableDomainPrecompute = true + cfg.domainNoPrecomputeThreshold = threshold + } +} + +// MmapProvingKey is a Groth16 BW6-761 proving key backed by a read-only memory +// mapping. Close must not be called while the proving key is in use. +type MmapProvingKey struct { + ProvingKey + + mapping io.Closer +} + +// Close releases the memory mapping backing pk. The key must not be used after +// Close returns. +func (pk *MmapProvingKey) Close() error { + if pk == nil || pk.mapping == nil { + return nil + } + err := pk.mapping.Close() + pk.mapping = nil + pk.ProvingKey = ProvingKey{} + return err +} + +type mmapDumpMeta struct { + Format string `json:"format"` + Version int `json:"version"` + Curve string `json:"curve"` + GOOS string `json:"goos"` + GOARCH string `json:"goarch"` + GoCompiler string `json:"go_compiler"` + GoVersion string `json:"go_version"` + Endian string `json:"endian"` + PointerSize int `json:"pointer_size"` + + GnarkVersion string `json:"gnark_version,omitempty"` + GnarkCryptoVersion string `json:"gnark_crypto_version,omitempty"` + + Sizes mmapDumpSizes `json:"sizes"` + + NbInfinityA uint64 `json:"nb_infinity_a"` + NbInfinityB uint64 `json:"nb_infinity_b"` + + Domain mmapDumpSection `json:"domain"` + + AlphaG1 mmapDumpSection `json:"alpha_g1"` + BetaG1 mmapDumpSection `json:"beta_g1"` + DeltaG1 mmapDumpSection `json:"delta_g1"` + BetaG2 mmapDumpSection `json:"beta_g2"` + DeltaG2 mmapDumpSection `json:"delta_g2"` + + G1A mmapDumpSection `json:"g1_a"` + G1B mmapDumpSection `json:"g1_b"` + G1Z mmapDumpSection `json:"g1_z"` + G1K mmapDumpSection `json:"g1_k"` + G2B mmapDumpSection `json:"g2_b"` + + InfinityA mmapDumpSection `json:"infinity_a"` + InfinityB mmapDumpSection `json:"infinity_b"` + + CommitmentKeys []mmapDumpCommitmentKey `json:"commitment_keys,omitempty"` +} + +type mmapDumpSizes struct { + G1Affine int `json:"g1_affine"` + G2Affine int `json:"g2_affine"` + Bool int `json:"bool"` +} + +type mmapDumpSection struct { + Offset int64 `json:"offset"` + Len int64 `json:"len"` + ElementSize int64 `json:"element_size"` +} + +type mmapDumpCommitmentKey struct { + Basis mmapDumpSection `json:"basis"` + BasisExpSigma mmapDumpSection `json:"basis_exp_sigma"` +} + +// WriteMmapDump writes pk to path using an aligned, file-backed dump format. +// +// The dump stores large proving-key slices as raw Go memory. It is intended for +// trusted local artifacts only and is not portable across incompatible gnark +// versions, gnark-crypto versions, architectures, endianness, or Go type +// layouts. +func (pk *ProvingKey) WriteMmapDump(path string) (err error) { + if !mmap.Supported() { + return fmt.Errorf("mmap is unsupported on this platform") + } + + f, err := os.Create(path) + if err != nil { + return err + } + defer func() { + if closeErr := f.Close(); err == nil { + err = closeErr + } + }() + + w := mmapDumpWriter{w: f} + meta := newMmapDumpMeta(pk) + + var domain bytes.Buffer + if _, err := pk.Domain.WriteTo(&domain); err != nil { + return fmt.Errorf("write domain: %w", err) + } + + if meta.Domain, err = w.writeBytes(domain.Bytes(), 1); err != nil { + return fmt.Errorf("write domain section: %w", err) + } + + if meta.AlphaG1, err = writeMmapDumpValue(&w, &pk.G1.Alpha); err != nil { + return fmt.Errorf("write G1.Alpha: %w", err) + } + if meta.BetaG1, err = writeMmapDumpValue(&w, &pk.G1.Beta); err != nil { + return fmt.Errorf("write G1.Beta: %w", err) + } + if meta.DeltaG1, err = writeMmapDumpValue(&w, &pk.G1.Delta); err != nil { + return fmt.Errorf("write G1.Delta: %w", err) + } + if meta.BetaG2, err = writeMmapDumpValue(&w, &pk.G2.Beta); err != nil { + return fmt.Errorf("write G2.Beta: %w", err) + } + if meta.DeltaG2, err = writeMmapDumpValue(&w, &pk.G2.Delta); err != nil { + return fmt.Errorf("write G2.Delta: %w", err) + } + + if meta.G1A, err = writeMmapDumpSlice(&w, pk.G1.A); err != nil { + return fmt.Errorf("write G1.A: %w", err) + } + if meta.G1B, err = writeMmapDumpSlice(&w, pk.G1.B); err != nil { + return fmt.Errorf("write G1.B: %w", err) + } + if meta.G1Z, err = writeMmapDumpSlice(&w, pk.G1.Z); err != nil { + return fmt.Errorf("write G1.Z: %w", err) + } + if meta.G1K, err = writeMmapDumpSlice(&w, pk.G1.K); err != nil { + return fmt.Errorf("write G1.K: %w", err) + } + if meta.G2B, err = writeMmapDumpSlice(&w, pk.G2.B); err != nil { + return fmt.Errorf("write G2.B: %w", err) + } + if meta.InfinityA, err = writeMmapDumpSlice(&w, pk.InfinityA); err != nil { + return fmt.Errorf("write InfinityA: %w", err) + } + if meta.InfinityB, err = writeMmapDumpSlice(&w, pk.InfinityB); err != nil { + return fmt.Errorf("write InfinityB: %w", err) + } + + if len(pk.CommitmentKeys) > 0 { + meta.CommitmentKeys = make([]mmapDumpCommitmentKey, len(pk.CommitmentKeys)) + for i := range pk.CommitmentKeys { + if meta.CommitmentKeys[i].Basis, err = writeMmapDumpSlice(&w, pk.CommitmentKeys[i].Basis); err != nil { + return fmt.Errorf("write commitment key %d basis: %w", i, err) + } + if meta.CommitmentKeys[i].BasisExpSigma, err = writeMmapDumpSlice(&w, pk.CommitmentKeys[i].BasisExpSigma); err != nil { + return fmt.Errorf("write commitment key %d basis exp sigma: %w", i, err) + } + } + } + + metaBytes, err := json.Marshal(meta) + if err != nil { + return fmt.Errorf("marshal metadata: %w", err) + } + if _, err := f.Write(metaBytes); err != nil { + return fmt.Errorf("write metadata: %w", err) + } + if err := binary.Write(f, binary.LittleEndian, uint64(len(metaBytes))); err != nil { + return fmt.Errorf("write metadata length: %w", err) + } + if _, err := f.Write(mmapProvingKeyMagic[:]); err != nil { + return fmt.Errorf("write magic: %w", err) + } + + return nil +} + +// ReadMmapDump maps path into memory and returns a proving key whose large +// slices are backed by the mapped file. The caller must keep the returned key +// open while it is used and call Close after proving is complete. +// +// Mmap dumps are unsafe raw-memory artifacts. ReadMmapDump validates the dump +// metadata against the current process, but it does not validate curve points or +// subgroup membership. +func ReadMmapDump(path string, opts ...MmapDumpOption) (*MmapProvingKey, error) { + var cfg mmapDumpConfig + for _, opt := range opts { + opt(&cfg) + } + + mapping, err := mmap.Open(path) + if err != nil { + return nil, err + } + pk := &MmapProvingKey{mapping: mapping} + if err := pk.ProvingKey.readMmapDump(mapping.Data, cfg); err != nil { + _ = mapping.Close() + return nil, err + } + return pk, nil +} + +func (pk *ProvingKey) readMmapDump(data []byte, cfg mmapDumpConfig) error { + meta, payloadLen, err := readMmapDumpMeta(data) + if err != nil { + return err + } + if err := validateMmapDumpMeta(meta); err != nil { + return err + } + if err := validateMmapDumpSections(meta, payloadLen); err != nil { + return err + } + payload := data[:payloadLen] + + pk.NbInfinityA = meta.NbInfinityA + pk.NbInfinityB = meta.NbInfinityB + + domainBytes, err := sectionBytes(payload, meta.Domain) + if err != nil { + return fmt.Errorf("domain section: %w", err) + } + domainBytes = append([]byte(nil), domainBytes...) + if cfg.disableDomainPrecompute && len(domainBytes) > 0 { + disableDomainPrecompute(domainBytes, cfg.domainNoPrecomputeThreshold) + } + if _, err := pk.Domain.ReadFrom(bytes.NewReader(domainBytes)); err != nil { + return fmt.Errorf("read domain: %w", err) + } + + if pk.G1.Alpha, err = sectionValue[curve.G1Affine](payload, meta.AlphaG1); err != nil { + return fmt.Errorf("read G1.Alpha: %w", err) + } + if pk.G1.Beta, err = sectionValue[curve.G1Affine](payload, meta.BetaG1); err != nil { + return fmt.Errorf("read G1.Beta: %w", err) + } + if pk.G1.Delta, err = sectionValue[curve.G1Affine](payload, meta.DeltaG1); err != nil { + return fmt.Errorf("read G1.Delta: %w", err) + } + if pk.G2.Beta, err = sectionValue[curve.G2Affine](payload, meta.BetaG2); err != nil { + return fmt.Errorf("read G2.Beta: %w", err) + } + if pk.G2.Delta, err = sectionValue[curve.G2Affine](payload, meta.DeltaG2); err != nil { + return fmt.Errorf("read G2.Delta: %w", err) + } + + if pk.G1.A, err = sectionSlice[curve.G1Affine](payload, meta.G1A); err != nil { + return fmt.Errorf("read G1.A: %w", err) + } + if pk.G1.B, err = sectionSlice[curve.G1Affine](payload, meta.G1B); err != nil { + return fmt.Errorf("read G1.B: %w", err) + } + if pk.G1.Z, err = sectionSlice[curve.G1Affine](payload, meta.G1Z); err != nil { + return fmt.Errorf("read G1.Z: %w", err) + } + if pk.G1.K, err = sectionSlice[curve.G1Affine](payload, meta.G1K); err != nil { + return fmt.Errorf("read G1.K: %w", err) + } + if pk.G2.B, err = sectionSlice[curve.G2Affine](payload, meta.G2B); err != nil { + return fmt.Errorf("read G2.B: %w", err) + } + if pk.InfinityA, err = sectionSlice[bool](payload, meta.InfinityA); err != nil { + return fmt.Errorf("read InfinityA: %w", err) + } + if pk.InfinityB, err = sectionSlice[bool](payload, meta.InfinityB); err != nil { + return fmt.Errorf("read InfinityB: %w", err) + } + + if len(meta.CommitmentKeys) > 0 { + pk.CommitmentKeys = make([]pedersen.ProvingKey, len(meta.CommitmentKeys)) + for i := range meta.CommitmentKeys { + if pk.CommitmentKeys[i].Basis, err = sectionSlice[curve.G1Affine](payload, meta.CommitmentKeys[i].Basis); err != nil { + return fmt.Errorf("read commitment key %d basis: %w", i, err) + } + if pk.CommitmentKeys[i].BasisExpSigma, err = sectionSlice[curve.G1Affine](payload, meta.CommitmentKeys[i].BasisExpSigma); err != nil { + return fmt.Errorf("read commitment key %d basis exp sigma: %w", i, err) + } + } + } else { + pk.CommitmentKeys = nil + } + + return validateMmapDumpProvingKey(pk) +} + +type mmapDumpWriter struct { + w io.Writer + pos int64 +} + +func writeMmapDumpValue[T any](w *mmapDumpWriter, v *T) (mmapDumpSection, error) { + var zero T + size := int64(unsafe.Sizeof(zero)) + b := unsafe.Slice((*byte)(unsafe.Pointer(v)), int(size)) + return w.writeBytes(b, size) +} + +func writeMmapDumpSlice[S ~[]E, E any](w *mmapDumpWriter, s S) (mmapDumpSection, error) { + var zero E + size := int64(unsafe.Sizeof(zero)) + if len(s) == 0 { + return w.writeBytes(nil, size) + } + b := unsafe.Slice((*byte)(unsafe.Pointer(&s[0])), len(s)*int(size)) + return w.writeBytes(b, size) +} + +func (w *mmapDumpWriter) writeBytes(b []byte, elementSize int64) (mmapDumpSection, error) { + if elementSize <= 0 { + return mmapDumpSection{}, fmt.Errorf("invalid element size %d", elementSize) + } + if err := w.padTo(mmapProvingKeyAlign); err != nil { + return mmapDumpSection{}, err + } + section := mmapDumpSection{ + Offset: w.pos, + ElementSize: elementSize, + } + if len(b)%int(elementSize) != 0 { + return mmapDumpSection{}, fmt.Errorf("section length %d is not a multiple of element size %d", len(b), elementSize) + } + section.Len = int64(len(b)) / elementSize + if len(b) == 0 { + return section, nil + } + n, err := w.w.Write(b) + w.pos += int64(n) + if err != nil { + return mmapDumpSection{}, err + } + if n != len(b) { + return mmapDumpSection{}, io.ErrShortWrite + } + return section, nil +} + +func (w *mmapDumpWriter) padTo(alignment int64) error { + rem := w.pos % alignment + if rem == 0 { + return nil + } + padding := make([]byte, alignment-rem) + n, err := w.w.Write(padding) + w.pos += int64(n) + if err != nil { + return err + } + if n != len(padding) { + return io.ErrShortWrite + } + return nil +} + +func newMmapDumpMeta(pk *ProvingKey) mmapDumpMeta { + gnarkVersion, gnarkCryptoVersion := dependencyVersions() + return mmapDumpMeta{ + Format: mmapProvingKeyFormat, + Version: mmapProvingKeyVersion, + Curve: "bw6-761", + GOOS: runtime.GOOS, + GOARCH: runtime.GOARCH, + GoCompiler: runtime.Compiler, + GoVersion: runtime.Version(), + Endian: nativeEndian(), + PointerSize: int(unsafe.Sizeof(uintptr(0))), + GnarkVersion: gnarkVersion, + GnarkCryptoVersion: gnarkCryptoVersion, + Sizes: currentMmapDumpSizes(), + NbInfinityA: pk.NbInfinityA, + NbInfinityB: pk.NbInfinityB, + CommitmentKeys: nil, + } +} + +func currentMmapDumpSizes() mmapDumpSizes { + return mmapDumpSizes{ + G1Affine: int(unsafe.Sizeof(curve.G1Affine{})), + G2Affine: int(unsafe.Sizeof(curve.G2Affine{})), + Bool: int(unsafe.Sizeof(false)), + } +} + +func readMmapDumpMeta(data []byte) (mmapDumpMeta, int, error) { + var meta mmapDumpMeta + trailerLen := 8 + len(mmapProvingKeyMagic) + if len(data) < trailerLen { + return meta, 0, fmt.Errorf("file too small") + } + magicOff := len(data) - len(mmapProvingKeyMagic) + if !bytes.Equal(data[magicOff:], mmapProvingKeyMagic[:]) { + return meta, 0, fmt.Errorf("invalid mmap dump magic") + } + metaLenOff := magicOff - 8 + metaLen := binary.LittleEndian.Uint64(data[metaLenOff:magicOff]) + if metaLen == 0 || metaLen > uint64(metaLenOff) { + return meta, 0, fmt.Errorf("invalid metadata length %d", metaLen) + } + metaOff := metaLenOff - int(metaLen) + if err := json.Unmarshal(data[metaOff:metaLenOff], &meta); err != nil { + return meta, 0, fmt.Errorf("unmarshal metadata: %w", err) + } + return meta, metaOff, nil +} + +func validateMmapDumpMeta(meta mmapDumpMeta) error { + if meta.Format != mmapProvingKeyFormat { + return fmt.Errorf("unsupported format %q", meta.Format) + } + if meta.Version != mmapProvingKeyVersion { + return fmt.Errorf("unsupported mmap dump version %d", meta.Version) + } + if meta.Curve != "bw6-761" { + return fmt.Errorf("unsupported curve %q", meta.Curve) + } + if meta.GOOS != runtime.GOOS { + return fmt.Errorf("goos mismatch: dump=%s runtime=%s", meta.GOOS, runtime.GOOS) + } + if meta.GOARCH != runtime.GOARCH { + return fmt.Errorf("goarch mismatch: dump=%s runtime=%s", meta.GOARCH, runtime.GOARCH) + } + if meta.GoCompiler != "" && meta.GoCompiler != runtime.Compiler { + return fmt.Errorf("go compiler mismatch: dump=%s runtime=%s", meta.GoCompiler, runtime.Compiler) + } + if meta.GoVersion != "" && meta.GoVersion != runtime.Version() { + return fmt.Errorf("go version mismatch: dump=%s runtime=%s", meta.GoVersion, runtime.Version()) + } + if meta.Endian != nativeEndian() { + return fmt.Errorf("endianness mismatch: dump=%s runtime=%s", meta.Endian, nativeEndian()) + } + if meta.PointerSize != int(unsafe.Sizeof(uintptr(0))) { + return fmt.Errorf("pointer size mismatch: dump=%d runtime=%d", meta.PointerSize, unsafe.Sizeof(uintptr(0))) + } + if meta.Sizes != currentMmapDumpSizes() { + return fmt.Errorf("type size mismatch: dump=%+v runtime=%+v", meta.Sizes, currentMmapDumpSizes()) + } + gnarkVersion, gnarkCryptoVersion := dependencyVersions() + if meta.GnarkVersion != "" && gnarkVersion != "" && meta.GnarkVersion != gnarkVersion { + return fmt.Errorf("gnark version mismatch: dump=%s runtime=%s", meta.GnarkVersion, gnarkVersion) + } + if meta.GnarkCryptoVersion != "" && gnarkCryptoVersion != "" && meta.GnarkCryptoVersion != gnarkCryptoVersion { + return fmt.Errorf("gnark-crypto version mismatch: dump=%s runtime=%s", meta.GnarkCryptoVersion, gnarkCryptoVersion) + } + return nil +} + +type namedMmapDumpSection struct { + name string + section mmapDumpSection +} + +func validateMmapDumpSections(meta mmapDumpMeta, payloadLen int) error { + sections := []namedMmapDumpSection{ + {"domain", meta.Domain}, + {"alpha_g1", meta.AlphaG1}, + {"beta_g1", meta.BetaG1}, + {"delta_g1", meta.DeltaG1}, + {"beta_g2", meta.BetaG2}, + {"delta_g2", meta.DeltaG2}, + {"g1_a", meta.G1A}, + {"g1_b", meta.G1B}, + {"g1_z", meta.G1Z}, + {"g1_k", meta.G1K}, + {"g2_b", meta.G2B}, + {"infinity_a", meta.InfinityA}, + {"infinity_b", meta.InfinityB}, + } + for i := range meta.CommitmentKeys { + sections = append(sections, + namedMmapDumpSection{fmt.Sprintf("commitment_keys[%d].basis", i), meta.CommitmentKeys[i].Basis}, + namedMmapDumpSection{fmt.Sprintf("commitment_keys[%d].basis_exp_sigma", i), meta.CommitmentKeys[i].BasisExpSigma}, + ) + } + + ranges := make([]struct { + name string + start, end int64 + }, 0, len(sections)) + for _, named := range sections { + start, end, err := mmapDumpSectionRange(named.name, named.section, payloadLen) + if err != nil { + return err + } + if start == end { + continue + } + for _, prev := range ranges { + if start < prev.end && prev.start < end { + return fmt.Errorf("section %s overlaps section %s", named.name, prev.name) + } + } + ranges = append(ranges, struct { + name string + start, end int64 + }{named.name, start, end}) + } + return nil +} + +func mmapDumpSectionRange(name string, section mmapDumpSection, payloadLen int) (int64, int64, error) { + if section.Offset < 0 || section.Len < 0 || section.ElementSize <= 0 { + return 0, 0, fmt.Errorf("invalid section %s: %+v", name, section) + } + if section.Offset%mmapProvingKeyAlign != 0 { + return 0, 0, fmt.Errorf("section %s offset %d is not aligned to %d", name, section.Offset, mmapProvingKeyAlign) + } + if section.Offset > int64(payloadLen) { + return 0, 0, fmt.Errorf("section %s starts outside payload: offset=%d payload=%d", name, section.Offset, payloadLen) + } + if section.Len == 0 { + return section.Offset, section.Offset, nil + } + const maxInt64 = int64(^uint64(0) >> 1) + if section.Len > maxInt64/section.ElementSize { + return 0, 0, fmt.Errorf("section %s size overflow", name) + } + size := section.Len * section.ElementSize + if size > int64(payloadLen)-section.Offset { + return 0, 0, fmt.Errorf("section %s out of bounds: offset=%d len=%d element_size=%d payload=%d", name, section.Offset, section.Len, section.ElementSize, payloadLen) + } + return section.Offset, section.Offset + size, nil +} + +func validateMmapDumpProvingKey(pk *ProvingKey) error { + if pk.NbInfinityA > uint64(len(pk.InfinityA)) { + return fmt.Errorf("NbInfinityA=%d exceeds InfinityA length %d", pk.NbInfinityA, len(pk.InfinityA)) + } + if pk.NbInfinityB > uint64(len(pk.InfinityB)) { + return fmt.Errorf("NbInfinityB=%d exceeds InfinityB length %d", pk.NbInfinityB, len(pk.InfinityB)) + } + if uint64(len(pk.G1.A))+pk.NbInfinityA != uint64(len(pk.InfinityA)) { + return fmt.Errorf("inconsistent G1.A and InfinityA lengths: len(G1.A)=%d NbInfinityA=%d len(InfinityA)=%d", len(pk.G1.A), pk.NbInfinityA, len(pk.InfinityA)) + } + if uint64(len(pk.G1.B))+pk.NbInfinityB != uint64(len(pk.InfinityB)) { + return fmt.Errorf("inconsistent G1.B and InfinityB lengths: len(G1.B)=%d NbInfinityB=%d len(InfinityB)=%d", len(pk.G1.B), pk.NbInfinityB, len(pk.InfinityB)) + } + if len(pk.G2.B) != len(pk.G1.B) { + return fmt.Errorf("inconsistent B vector lengths: len(G1.B)=%d len(G2.B)=%d", len(pk.G1.B), len(pk.G2.B)) + } + if uint64(len(pk.G1.Z))+1 != pk.Domain.Cardinality { + return fmt.Errorf("inconsistent Z length: len(G1.Z)=%d domain cardinality=%d", len(pk.G1.Z), pk.Domain.Cardinality) + } + for i := range pk.CommitmentKeys { + if len(pk.CommitmentKeys[i].Basis) != len(pk.CommitmentKeys[i].BasisExpSigma) { + return fmt.Errorf("inconsistent commitment key %d lengths: len(Basis)=%d len(BasisExpSigma)=%d", i, len(pk.CommitmentKeys[i].Basis), len(pk.CommitmentKeys[i].BasisExpSigma)) + } + } + return nil +} + +func sectionValue[T any](data []byte, section mmapDumpSection) (T, error) { + var zero T + s, err := sectionSlice[T](data, section) + if err != nil { + return zero, err + } + if len(s) != 1 { + return zero, fmt.Errorf("expected one element, got %d", len(s)) + } + return s[0], nil +} + +func sectionSlice[T any](data []byte, section mmapDumpSection) ([]T, error) { + var zero T + size := int64(unsafe.Sizeof(zero)) + align := int64(unsafe.Alignof(zero)) + if section.ElementSize != size { + return nil, fmt.Errorf("element size mismatch: section=%d type=%d", section.ElementSize, size) + } + if section.Len == 0 { + return nil, nil + } + if section.Offset%align != 0 { + return nil, fmt.Errorf("section offset %d is not aligned to %d", section.Offset, align) + } + b, err := sectionBytes(data, section) + if err != nil { + return nil, err + } + if uintptr(unsafe.Pointer(&b[0]))%uintptr(align) != 0 { + return nil, fmt.Errorf("mapped section address is not aligned to %d", align) + } + return unsafe.Slice((*T)(unsafe.Pointer(&b[0])), int(section.Len)), nil +} + +func sectionBytes(data []byte, section mmapDumpSection) ([]byte, error) { + if section.Offset < 0 || section.Len < 0 || section.ElementSize <= 0 { + return nil, fmt.Errorf("invalid section %+v", section) + } + if section.Len == 0 { + return nil, nil + } + const maxInt64 = int64(^uint64(0) >> 1) + if section.Len > maxInt64/section.ElementSize { + return nil, fmt.Errorf("section size overflow") + } + size := section.Len * section.ElementSize + if section.Offset > int64(len(data)) || size > int64(len(data))-section.Offset { + return nil, fmt.Errorf("section out of bounds: offset=%d len=%d element_size=%d file=%d", section.Offset, section.Len, section.ElementSize, len(data)) + } + return data[section.Offset : section.Offset+size], nil +} + +func disableDomainPrecompute(domain []byte, threshold uint64) { + if len(domain) < 9 { + return + } + cardinality := binary.BigEndian.Uint64(domain[:8]) + if cardinality >= threshold { + // fft.Domain.WriteTo serializes the withPrecompute bool as the final byte. + domain[len(domain)-1] = 0 + } +} + +func nativeEndian() string { + var x uint16 = 1 + if *(*byte)(unsafe.Pointer(&x)) == 1 { + return "little" + } + return "big" +} + +func dependencyVersions() (string, string) { + info, ok := debug.ReadBuildInfo() + if !ok { + return "", "" + } + var gnarkVersion, gnarkCryptoVersion string + if info.Main.Path == "github.com/consensys/gnark" { + gnarkVersion = info.Main.Version + } + for _, dep := range info.Deps { + switch dep.Path { + case "github.com/consensys/gnark": + gnarkVersion = dep.Version + case "github.com/consensys/gnark-crypto": + gnarkCryptoVersion = dep.Version + } + } + return gnarkVersion, gnarkCryptoVersion +} diff --git a/backend/groth16/bw6-761/mmapdump_test.go b/backend/groth16/bw6-761/mmapdump_test.go new file mode 100644 index 0000000000..2816f16b46 --- /dev/null +++ b/backend/groth16/bw6-761/mmapdump_test.go @@ -0,0 +1,316 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package groth16 + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/consensys/gnark-crypto/ecc" + + "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" + "github.com/consensys/gnark/backend/witness" + + cs "github.com/consensys/gnark/constraint/bw6-761" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/frontend/cs/r1cs" + "github.com/consensys/gnark/internal/backend/ioutils/mmap" +) + +func skipMmapDumpUnsupported(t testing.TB) { + t.Helper() + if !mmap.Supported() { + t.Skip("mmap is unsupported on this platform") + } +} + +func TestMmapDumpUnsupportedPlatform(t *testing.T) { + if mmap.Supported() { + t.Skip("mmap is supported on this platform") + } + + var pk ProvingKey + path := filepath.Join(t.TempDir(), "proving_key.mmap") + if err := pk.WriteMmapDump(path); err == nil { + t.Fatal("expected error") + } + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatalf("expected no dump file to be created, got %v", err) + } +} + +type mmapDumpCircuit struct { + X frontend.Variable + Y frontend.Variable `gnark:",public"` +} + +func (c *mmapDumpCircuit) Define(api frontend.API) error { + api.AssertIsEqual(api.Mul(c.X, c.X, c.X), c.Y) + return nil +} + +type mmapDumpCommitmentCircuit struct { + One frontend.Variable + Two frontend.Variable `gnark:",public"` +} + +func (c *mmapDumpCommitmentCircuit) Define(api frontend.API) error { + commitCompiler, ok := api.(frontend.Committer) + if !ok { + return fmt.Errorf("compiler does not commit") + } + commitment, err := commitCompiler.Commit(c.One, c.Two) + if err != nil { + return err + } + api.AssertIsDifferent(commitment, 0) + api.AssertIsEqual(c.One, 1) + api.AssertIsEqual(c.Two, 2) + return nil +} + +const mmapDumpBenchmarkCircuitSize = 2048 + +type mmapDumpBenchmarkCircuit struct { + X [mmapDumpBenchmarkCircuitSize]frontend.Variable + Y frontend.Variable `gnark:",public"` +} + +func (c *mmapDumpBenchmarkCircuit) Define(api frontend.API) error { + acc := frontend.Variable(1) + for i := range c.X { + acc = api.Mul(acc, api.Add(c.X[i], 1)) + } + api.AssertIsEqual(acc, c.Y) + return nil +} + +func TestMmapDumpProvingKeyRoundTrip(t *testing.T) { + skipMmapDumpUnsupported(t) + + ccs, err := frontend.Compile(ecc.BW6_761.ScalarField(), r1cs.NewBuilder, &mmapDumpCircuit{}) + if err != nil { + t.Fatal(err) + } + r1cs := ccs.(*cs.R1CS) + + var pk ProvingKey + var vk VerifyingKey + if err := Setup(r1cs, &pk, &vk); err != nil { + t.Fatal(err) + } + + path := filepath.Join(t.TempDir(), "proving_key.mmap") + if err := pk.WriteMmapDump(path); err != nil { + t.Fatal(err) + } + + mappedPK, err := ReadMmapDump(path, WithMmapDumpNoDomainPrecompute(1)) + if err != nil { + t.Fatal(err) + } + defer mappedPK.Close() + + fullWitness, publicWitness := mmapDumpWitnesses(t) + proof, err := Prove(r1cs, &mappedPK.ProvingKey, fullWitness) + if err != nil { + t.Fatal(err) + } + if err := Verify(proof, &vk, publicWitness.Vector().(fr.Vector)); err != nil { + t.Fatal(err) + } + + if err := mappedPK.Close(); err != nil { + t.Fatal(err) + } + if mappedPK.G1.A != nil { + t.Fatal("expected mapped key to be cleared after close") + } +} + +func TestMmapDumpProvingKeyWithCommitment(t *testing.T) { + skipMmapDumpUnsupported(t) + + ccs, err := frontend.Compile(ecc.BW6_761.ScalarField(), r1cs.NewBuilder, &mmapDumpCommitmentCircuit{}) + if err != nil { + t.Fatal(err) + } + r1cs := ccs.(*cs.R1CS) + + var pk ProvingKey + var vk VerifyingKey + if err := Setup(r1cs, &pk, &vk); err != nil { + t.Fatal(err) + } + if len(pk.CommitmentKeys) == 0 { + t.Fatal("expected commitment proving keys") + } + + path := filepath.Join(t.TempDir(), "proving_key.mmap") + if err := pk.WriteMmapDump(path); err != nil { + t.Fatal(err) + } + + mappedPK, err := ReadMmapDump(path) + if err != nil { + t.Fatal(err) + } + defer mappedPK.Close() + + assignment := mmapDumpCommitmentCircuit{One: 1, Two: 2} + fullWitness, err := frontend.NewWitness(&assignment, ecc.BW6_761.ScalarField()) + if err != nil { + t.Fatal(err) + } + publicWitness, err := fullWitness.Public() + if err != nil { + t.Fatal(err) + } + + proof, err := Prove(r1cs, &mappedPK.ProvingKey, fullWitness) + if err != nil { + t.Fatal(err) + } + if err := Verify(proof, &vk, publicWitness.Vector().(fr.Vector)); err != nil { + t.Fatal(err) + } +} + +func TestMmapDumpPreservesDomainByDefault(t *testing.T) { + skipMmapDumpUnsupported(t) + + ccs, err := frontend.Compile(ecc.BW6_761.ScalarField(), r1cs.NewBuilder, &mmapDumpCircuit{}) + if err != nil { + t.Fatal(err) + } + r1cs := ccs.(*cs.R1CS) + + var pk ProvingKey + var vk VerifyingKey + if err := Setup(r1cs, &pk, &vk); err != nil { + t.Fatal(err) + } + + path := filepath.Join(t.TempDir(), "proving_key.mmap") + if err := pk.WriteMmapDump(path); err != nil { + t.Fatal(err) + } + + mappedPK, err := ReadMmapDump(path) + if err != nil { + t.Fatal(err) + } + defer mappedPK.Close() + + var originalDomain, mappedDomain bytes.Buffer + if _, err := pk.Domain.WriteTo(&originalDomain); err != nil { + t.Fatal(err) + } + if _, err := mappedPK.Domain.WriteTo(&mappedDomain); err != nil { + t.Fatal(err) + } + if !bytes.Equal(originalDomain.Bytes(), mappedDomain.Bytes()) { + t.Fatal("expected default mmap dump load to preserve serialized domain") + } +} + +func mmapDumpWitnesses(t *testing.T) (witness.Witness, witness.Witness) { + t.Helper() + + assignment := mmapDumpCircuit{X: 3, Y: 27} + fullWitness, err := frontend.NewWitness(&assignment, ecc.BW6_761.ScalarField()) + if err != nil { + t.Fatal(err) + } + publicWitness, err := fullWitness.Public() + if err != nil { + t.Fatal(err) + } + return fullWitness, publicWitness +} + +func BenchmarkMmapDumpProvingKeyLoad(b *testing.B) { + skipMmapDumpUnsupported(b) + + pk := mmapDumpBenchmarkProvingKey(b) + + var dump bytes.Buffer + if err := pk.WriteDump(&dump); err != nil { + b.Fatal(err) + } + + mmapPath := filepath.Join(b.TempDir(), "proving_key.mmap") + if err := pk.WriteMmapDump(mmapPath); err != nil { + b.Fatal(err) + } + + dumpBytes := dump.Bytes() + + b.Run("ReadDump", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + var loaded ProvingKey + if err := loaded.ReadDump(bytes.NewReader(dumpBytes)); err != nil { + b.Fatal(err) + } + if len(loaded.G1.A) == 0 { + b.Fatal("empty proving key") + } + } + }) + + b.Run("ReadMmapDump", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + loaded, err := ReadMmapDump(mmapPath) + if err != nil { + b.Fatal(err) + } + if len(loaded.G1.A) == 0 { + b.Fatal("empty proving key") + } + if err := loaded.Close(); err != nil { + b.Fatal(err) + } + } + }) + + b.Run("ReadMmapDumpNoDomainPrecompute", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + loaded, err := ReadMmapDump(mmapPath, WithMmapDumpNoDomainPrecompute(1)) + if err != nil { + b.Fatal(err) + } + if len(loaded.G1.A) == 0 { + b.Fatal("empty proving key") + } + if err := loaded.Close(); err != nil { + b.Fatal(err) + } + } + }) +} + +func mmapDumpBenchmarkProvingKey(b *testing.B) *ProvingKey { + b.Helper() + + ccs, err := frontend.Compile(ecc.BW6_761.ScalarField(), r1cs.NewBuilder, &mmapDumpBenchmarkCircuit{}) + if err != nil { + b.Fatal(err) + } + r1cs := ccs.(*cs.R1CS) + + var pk ProvingKey + var vk VerifyingKey + if err := Setup(r1cs, &pk, &vk); err != nil { + b.Fatal(err) + } + return &pk +} diff --git a/backend/groth16/groth16.go b/backend/groth16/groth16.go index b3ca3584dc..14a3f852f6 100644 --- a/backend/groth16/groth16.go +++ b/backend/groth16/groth16.go @@ -9,6 +9,7 @@ package groth16 import ( + "fmt" "io" "github.com/consensys/gnark-crypto/ecc" @@ -77,6 +78,33 @@ type ProvingKey interface { IsDifferent(any) bool } +// MmapProvingKey is a Groth16 proving key backed by a read-only memory mapping. +// +// The caller must keep the key open while it is used and call Close after +// proving is complete. +type MmapProvingKey interface { + ProvingKey + io.Closer +} + +// MmapDumpOption configures ReadMmapDump. +type MmapDumpOption func(*mmapDumpConfig) + +type mmapDumpConfig struct { + disableDomainPrecompute bool + domainNoPrecomputeThreshold uint64 +} + +// WithMmapDumpNoDomainPrecompute disables FFT domain precomputation while +// loading a mapped dump once the serialized domain cardinality is greater than +// or equal to threshold. Passing 0 disables precomputation for every domain. +func WithMmapDumpNoDomainPrecompute(threshold uint64) MmapDumpOption { + return func(cfg *mmapDumpConfig) { + cfg.disableDomainPrecompute = true + cfg.domainNoPrecomputeThreshold = threshold + } +} + // VerifyingKey represents a Groth16 VerifyingKey // // it's underlying implementation is strongly typed with the curve (see gnark/internal/backend) @@ -143,6 +171,57 @@ func Verify(proof Proof, vk VerifyingKey, publicWitness witness.Witness, opts .. } } +// WriteMmapDump writes pk to path using an aligned, file-backed dump format. +// +// Mmap dumps store large proving-key slices as raw Go memory. They are intended +// for trusted local artifacts only and are not portable across incompatible +// gnark versions, gnark-crypto versions, operating systems, architectures, +// endianness, or Go type layouts. +func WriteMmapDump(pk ProvingKey, path string) error { + switch pk := pk.(type) { + case *groth16_bls12377.ProvingKey: + return pk.WriteMmapDump(path) + case *groth16_bls12377.MmapProvingKey: + return pk.ProvingKey.WriteMmapDump(path) + case *groth16_bls12381.ProvingKey: + return pk.WriteMmapDump(path) + case *groth16_bls12381.MmapProvingKey: + return pk.ProvingKey.WriteMmapDump(path) + case *groth16_bn254.ProvingKey: + return pk.WriteMmapDump(path) + case *groth16_bn254.MmapProvingKey: + return pk.ProvingKey.WriteMmapDump(path) + case *groth16_bw6761.ProvingKey: + return pk.WriteMmapDump(path) + case *groth16_bw6761.MmapProvingKey: + return pk.ProvingKey.WriteMmapDump(path) + default: + return fmt.Errorf("unsupported Groth16 proving key type %T", pk) + } +} + +// ReadMmapDump maps path into memory and returns a curve-typed proving key +// through the generic Groth16 interface. +// +// Mmap dumps are unsafe raw-memory artifacts. ReadMmapDump validates dump +// metadata against the current process, but it does not validate curve points or +// subgroup membership. +func ReadMmapDump(curveID ecc.ID, path string, opts ...MmapDumpOption) (MmapProvingKey, error) { + cfg := newMmapDumpConfig(opts...) + switch curveID { + case ecc.BLS12_377: + return groth16_bls12377.ReadMmapDump(path, cfg.bls12377Options()...) + case ecc.BLS12_381: + return groth16_bls12381.ReadMmapDump(path, cfg.bls12381Options()...) + case ecc.BN254: + return groth16_bn254.ReadMmapDump(path, cfg.bn254Options()...) + case ecc.BW6_761: + return groth16_bw6761.ReadMmapDump(path, cfg.bw6761Options()...) + default: + return nil, fmt.Errorf("unsupported Groth16 curve %s", curveID) + } +} + // Prove runs the groth16.Prove algorithm. // // if the force flag is set: @@ -153,22 +232,126 @@ func Verify(proof Proof, vk VerifyingKey, publicWitness witness.Witness, opts .. func Prove(r1cs constraint.ConstraintSystem, pk ProvingKey, fullWitness witness.Witness, opts ...backend.ProverOption) (Proof, error) { switch _r1cs := r1cs.(type) { case *cs_bls12377.R1CS: - return groth16_bls12377.Prove(_r1cs, pk.(*groth16_bls12377.ProvingKey), fullWitness, opts...) + _pk, ok := bls12377ProvingKey(pk) + if !ok { + return nil, fmt.Errorf("invalid BLS12-377 proving key type %T", pk) + } + return groth16_bls12377.Prove(_r1cs, _pk, fullWitness, opts...) case *cs_bls12381.R1CS: - return groth16_bls12381.Prove(_r1cs, pk.(*groth16_bls12381.ProvingKey), fullWitness, opts...) + _pk, ok := bls12381ProvingKey(pk) + if !ok { + return nil, fmt.Errorf("invalid BLS12-381 proving key type %T", pk) + } + return groth16_bls12381.Prove(_r1cs, _pk, fullWitness, opts...) case *cs_bn254.R1CS: - return groth16_bn254.Prove(_r1cs, pk.(*groth16_bn254.ProvingKey), fullWitness, opts...) + _pk, ok := bn254ProvingKey(pk) + if !ok { + return nil, fmt.Errorf("invalid BN254 proving key type %T", pk) + } + return groth16_bn254.Prove(_r1cs, _pk, fullWitness, opts...) case *cs_bw6761.R1CS: - return groth16_bw6761.Prove(_r1cs, pk.(*groth16_bw6761.ProvingKey), fullWitness, opts...) + _pk, ok := bw6761ProvingKey(pk) + if !ok { + return nil, fmt.Errorf("invalid BW6-761 proving key type %T", pk) + } + return groth16_bw6761.Prove(_r1cs, _pk, fullWitness, opts...) default: panic("unrecognized R1CS curve type") } } +func newMmapDumpConfig(opts ...MmapDumpOption) mmapDumpConfig { + var cfg mmapDumpConfig + for _, opt := range opts { + opt(&cfg) + } + return cfg +} + +func (cfg mmapDumpConfig) bls12377Options() []groth16_bls12377.MmapDumpOption { + if !cfg.disableDomainPrecompute { + return nil + } + return []groth16_bls12377.MmapDumpOption{ + groth16_bls12377.WithMmapDumpNoDomainPrecompute(cfg.domainNoPrecomputeThreshold), + } +} + +func (cfg mmapDumpConfig) bls12381Options() []groth16_bls12381.MmapDumpOption { + if !cfg.disableDomainPrecompute { + return nil + } + return []groth16_bls12381.MmapDumpOption{ + groth16_bls12381.WithMmapDumpNoDomainPrecompute(cfg.domainNoPrecomputeThreshold), + } +} + +func (cfg mmapDumpConfig) bn254Options() []groth16_bn254.MmapDumpOption { + if !cfg.disableDomainPrecompute { + return nil + } + return []groth16_bn254.MmapDumpOption{ + groth16_bn254.WithMmapDumpNoDomainPrecompute(cfg.domainNoPrecomputeThreshold), + } +} + +func (cfg mmapDumpConfig) bw6761Options() []groth16_bw6761.MmapDumpOption { + if !cfg.disableDomainPrecompute { + return nil + } + return []groth16_bw6761.MmapDumpOption{ + groth16_bw6761.WithMmapDumpNoDomainPrecompute(cfg.domainNoPrecomputeThreshold), + } +} + +func bls12377ProvingKey(pk ProvingKey) (*groth16_bls12377.ProvingKey, bool) { + switch pk := pk.(type) { + case *groth16_bls12377.ProvingKey: + return pk, true + case *groth16_bls12377.MmapProvingKey: + return &pk.ProvingKey, true + default: + return nil, false + } +} + +func bls12381ProvingKey(pk ProvingKey) (*groth16_bls12381.ProvingKey, bool) { + switch pk := pk.(type) { + case *groth16_bls12381.ProvingKey: + return pk, true + case *groth16_bls12381.MmapProvingKey: + return &pk.ProvingKey, true + default: + return nil, false + } +} + +func bn254ProvingKey(pk ProvingKey) (*groth16_bn254.ProvingKey, bool) { + switch pk := pk.(type) { + case *groth16_bn254.ProvingKey: + return pk, true + case *groth16_bn254.MmapProvingKey: + return &pk.ProvingKey, true + default: + return nil, false + } +} + +func bw6761ProvingKey(pk ProvingKey) (*groth16_bw6761.ProvingKey, bool) { + switch pk := pk.(type) { + case *groth16_bw6761.ProvingKey: + return pk, true + case *groth16_bw6761.MmapProvingKey: + return &pk.ProvingKey, true + default: + return nil, false + } +} + // Setup runs groth16.Setup with provided R1CS and outputs a key pair associated with the circuit. // // Note that careful consideration must be given to this step in a production environment. diff --git a/backend/groth16/mmapdump_test.go b/backend/groth16/mmapdump_test.go new file mode 100644 index 0000000000..72726cefeb --- /dev/null +++ b/backend/groth16/mmapdump_test.go @@ -0,0 +1,81 @@ +package groth16_test + +import ( + "path/filepath" + "testing" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark/backend/groth16" + "github.com/consensys/gnark/backend/witness" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/frontend/cs/r1cs" + "github.com/consensys/gnark/internal/backend/ioutils/mmap" +) + +type mmapDumpPublicAPICircuit struct { + X frontend.Variable + Y frontend.Variable `gnark:",public"` +} + +func (c *mmapDumpPublicAPICircuit) Define(api frontend.API) error { + api.AssertIsEqual(api.Mul(c.X, c.X, c.X), c.Y) + return nil +} + +func TestMmapDumpPublicAPIProve(t *testing.T) { + if !mmap.Supported() { + t.Skip("mmap is unsupported on this platform") + } + + for _, curve := range getCurves() { + t.Run(curve.String(), func(t *testing.T) { + ccs, err := frontend.Compile(curve.ScalarField(), r1cs.NewBuilder, &mmapDumpPublicAPICircuit{}) + if err != nil { + t.Fatal(err) + } + + pk, vk, err := groth16.Setup(ccs) + if err != nil { + t.Fatal(err) + } + + path := filepath.Join(t.TempDir(), "proving_key.mmap") + if err := groth16.WriteMmapDump(pk, path); err != nil { + t.Fatal(err) + } + + mappedPK, err := groth16.ReadMmapDump(curve, path, groth16.WithMmapDumpNoDomainPrecompute(1)) + if err != nil { + t.Fatal(err) + } + defer mappedPK.Close() + if mappedPK.CurveID() != curve { + t.Fatalf("unexpected curve: got %s want %s", mappedPK.CurveID(), curve) + } + + fullWitness, publicWitness := mmapDumpPublicAPIWitnesses(t, curve) + proof, err := groth16.Prove(ccs, mappedPK, fullWitness) + if err != nil { + t.Fatal(err) + } + if err := groth16.Verify(proof, vk, publicWitness); err != nil { + t.Fatal(err) + } + }) + } +} + +func mmapDumpPublicAPIWitnesses(t *testing.T, curve ecc.ID) (witness.Witness, witness.Witness) { + t.Helper() + + assignment := mmapDumpPublicAPICircuit{X: 3, Y: 27} + fullWitness, err := frontend.NewWitness(&assignment, curve.ScalarField()) + if err != nil { + t.Fatal(err) + } + publicWitness, err := fullWitness.Public() + if err != nil { + t.Fatal(err) + } + return fullWitness, publicWitness +} diff --git a/internal/backend/ioutils/mmap/mmap_unix.go b/internal/backend/ioutils/mmap/mmap_unix.go new file mode 100644 index 0000000000..54e77bf2d1 --- /dev/null +++ b/internal/backend/ioutils/mmap/mmap_unix.go @@ -0,0 +1,58 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +//go:build unix + +package mmap + +import ( + "fmt" + "os" + "syscall" +) + +// Mapping is a read-only memory mapping. +type Mapping struct { + Data []byte +} + +// Supported reports whether this build supports memory-mapped files. +func Supported() bool { + return true +} + +// Open maps path into memory using a private read-only mapping. +func Open(path string) (*Mapping, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + stat, err := f.Stat() + if err != nil { + return nil, err + } + if stat.Size() == 0 { + return nil, fmt.Errorf("mmap %s: empty file", path) + } + if stat.Size() > int64(int(^uint(0)>>1)) { + return nil, fmt.Errorf("mmap %s: file too large for platform", path) + } + + data, err := syscall.Mmap(int(f.Fd()), 0, int(stat.Size()), syscall.PROT_READ, syscall.MAP_PRIVATE) + if err != nil { + return nil, err + } + return &Mapping{Data: data}, nil +} + +// Close releases the mapping. +func (m *Mapping) Close() error { + if m == nil || m.Data == nil { + return nil + } + err := syscall.Munmap(m.Data) + m.Data = nil + return err +} diff --git a/internal/backend/ioutils/mmap/mmap_unsupported.go b/internal/backend/ioutils/mmap/mmap_unsupported.go new file mode 100644 index 0000000000..884e97987a --- /dev/null +++ b/internal/backend/ioutils/mmap/mmap_unsupported.go @@ -0,0 +1,31 @@ +// Copyright 2020-2026 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +//go:build !unix + +package mmap + +import "errors" + +// Mapping is a read-only memory mapping. +type Mapping struct { + Data []byte +} + +// Supported reports whether this build supports memory-mapped files. +func Supported() bool { + return false +} + +// Open returns an error on platforms without mmap support. +func Open(string) (*Mapping, error) { + return nil, errors.New("mmap is unsupported on this platform") +} + +// Close releases the mapping. +func (m *Mapping) Close() error { + if m != nil { + m.Data = nil + } + return nil +} diff --git a/internal/generator/backend/main.go b/internal/generator/backend/main.go index 6d78bf82ab..9fbf0e76a3 100644 --- a/internal/generator/backend/main.go +++ b/internal/generator/backend/main.go @@ -185,7 +185,9 @@ func main() { {File: filepath.Join(groth16Dir, "prove.go"), Templates: []string{"groth16/groth16.prove.go.tmpl", importCurve}}, {File: filepath.Join(groth16Dir, "setup.go"), Templates: []string{"groth16/groth16.setup.go.tmpl", importCurve}}, {File: filepath.Join(groth16Dir, "marshal.go"), Templates: []string{"groth16/groth16.marshal.go.tmpl", importCurve}}, + {File: filepath.Join(groth16Dir, "mmapdump.go"), Templates: []string{"groth16/groth16.mmapdump.go.tmpl", importCurve}}, {File: filepath.Join(groth16Dir, "marshal_test.go"), Templates: []string{"groth16/tests/groth16.marshal.go.tmpl", importCurve}}, + {File: filepath.Join(groth16Dir, "mmapdump_test.go"), Templates: []string{"groth16/tests/groth16.mmapdump.go.tmpl", importCurve}}, } if err := bgen.Generate(d, "groth16", "./template/zkpschemes/", entries...); err != nil { panic(err) // TODO handle diff --git a/internal/generator/backend/template/zkpschemes/groth16/groth16.mmapdump.go.tmpl b/internal/generator/backend/template/zkpschemes/groth16/groth16.mmapdump.go.tmpl new file mode 100644 index 0000000000..803455e25f --- /dev/null +++ b/internal/generator/backend/template/zkpschemes/groth16/groth16.mmapdump.go.tmpl @@ -0,0 +1,681 @@ +import ( + "bytes" + "encoding/binary" + "encoding/json" + "fmt" + "io" + "os" + "runtime" + "runtime/debug" + "unsafe" + + {{ template "import_curve" . }} + {{ template "import_pedersen" . }} + "github.com/consensys/gnark/internal/backend/ioutils/mmap" +) + +const ( + mmapProvingKeyFormat = "gnark.groth16.{{ toLower .Curve }}.proving_key.mmap" + mmapProvingKeyVersion = 1 + mmapProvingKeyAlign = 64 +) + +var mmapProvingKeyMagic = [16]byte{'g', 'n', 'a', 'r', 'k', 'p', 'k', 'm', 'm', 'a', 'p', 'v', '1', '\r', '\n', 0} + +// MmapDumpOption configures ReadMmapDump. +type MmapDumpOption func(*mmapDumpConfig) + +type mmapDumpConfig struct { + disableDomainPrecompute bool + domainNoPrecomputeThreshold uint64 +} + +// WithMmapDumpNoDomainPrecompute disables FFT domain precomputation while +// loading a mapped dump once the serialized domain cardinality is greater than +// or equal to threshold. Passing 0 disables precomputation for every domain. +func WithMmapDumpNoDomainPrecompute(threshold uint64) MmapDumpOption { + return func(cfg *mmapDumpConfig) { + cfg.disableDomainPrecompute = true + cfg.domainNoPrecomputeThreshold = threshold + } +} + +// MmapProvingKey is a Groth16 {{ .Curve }} proving key backed by a read-only memory +// mapping. Close must not be called while the proving key is in use. +type MmapProvingKey struct { + ProvingKey + + mapping io.Closer +} + +// Close releases the memory mapping backing pk. The key must not be used after +// Close returns. +func (pk *MmapProvingKey) Close() error { + if pk == nil || pk.mapping == nil { + return nil + } + err := pk.mapping.Close() + pk.mapping = nil + pk.ProvingKey = ProvingKey{} + return err +} + +type mmapDumpMeta struct { + Format string `json:"format"` + Version int `json:"version"` + Curve string `json:"curve"` + GOOS string `json:"goos"` + GOARCH string `json:"goarch"` + GoCompiler string `json:"go_compiler"` + GoVersion string `json:"go_version"` + Endian string `json:"endian"` + PointerSize int `json:"pointer_size"` + + GnarkVersion string `json:"gnark_version,omitempty"` + GnarkCryptoVersion string `json:"gnark_crypto_version,omitempty"` + + Sizes mmapDumpSizes `json:"sizes"` + + NbInfinityA uint64 `json:"nb_infinity_a"` + NbInfinityB uint64 `json:"nb_infinity_b"` + + Domain mmapDumpSection `json:"domain"` + + AlphaG1 mmapDumpSection `json:"alpha_g1"` + BetaG1 mmapDumpSection `json:"beta_g1"` + DeltaG1 mmapDumpSection `json:"delta_g1"` + BetaG2 mmapDumpSection `json:"beta_g2"` + DeltaG2 mmapDumpSection `json:"delta_g2"` + + G1A mmapDumpSection `json:"g1_a"` + G1B mmapDumpSection `json:"g1_b"` + G1Z mmapDumpSection `json:"g1_z"` + G1K mmapDumpSection `json:"g1_k"` + G2B mmapDumpSection `json:"g2_b"` + + InfinityA mmapDumpSection `json:"infinity_a"` + InfinityB mmapDumpSection `json:"infinity_b"` + + CommitmentKeys []mmapDumpCommitmentKey `json:"commitment_keys,omitempty"` +} + +type mmapDumpSizes struct { + G1Affine int `json:"g1_affine"` + G2Affine int `json:"g2_affine"` + Bool int `json:"bool"` +} + +type mmapDumpSection struct { + Offset int64 `json:"offset"` + Len int64 `json:"len"` + ElementSize int64 `json:"element_size"` +} + +type mmapDumpCommitmentKey struct { + Basis mmapDumpSection `json:"basis"` + BasisExpSigma mmapDumpSection `json:"basis_exp_sigma"` +} + +// WriteMmapDump writes pk to path using an aligned, file-backed dump format. +// +// The dump stores large proving-key slices as raw Go memory. It is intended for +// trusted local artifacts only and is not portable across incompatible gnark +// versions, gnark-crypto versions, architectures, endianness, or Go type +// layouts. +func (pk *ProvingKey) WriteMmapDump(path string) (err error) { + if !mmap.Supported() { + return fmt.Errorf("mmap is unsupported on this platform") + } + + f, err := os.Create(path) + if err != nil { + return err + } + defer func() { + if closeErr := f.Close(); err == nil { + err = closeErr + } + }() + + w := mmapDumpWriter{w: f} + meta := newMmapDumpMeta(pk) + + var domain bytes.Buffer + if _, err := pk.Domain.WriteTo(&domain); err != nil { + return fmt.Errorf("write domain: %w", err) + } + + if meta.Domain, err = w.writeBytes(domain.Bytes(), 1); err != nil { + return fmt.Errorf("write domain section: %w", err) + } + + if meta.AlphaG1, err = writeMmapDumpValue(&w, &pk.G1.Alpha); err != nil { + return fmt.Errorf("write G1.Alpha: %w", err) + } + if meta.BetaG1, err = writeMmapDumpValue(&w, &pk.G1.Beta); err != nil { + return fmt.Errorf("write G1.Beta: %w", err) + } + if meta.DeltaG1, err = writeMmapDumpValue(&w, &pk.G1.Delta); err != nil { + return fmt.Errorf("write G1.Delta: %w", err) + } + if meta.BetaG2, err = writeMmapDumpValue(&w, &pk.G2.Beta); err != nil { + return fmt.Errorf("write G2.Beta: %w", err) + } + if meta.DeltaG2, err = writeMmapDumpValue(&w, &pk.G2.Delta); err != nil { + return fmt.Errorf("write G2.Delta: %w", err) + } + + if meta.G1A, err = writeMmapDumpSlice(&w, pk.G1.A); err != nil { + return fmt.Errorf("write G1.A: %w", err) + } + if meta.G1B, err = writeMmapDumpSlice(&w, pk.G1.B); err != nil { + return fmt.Errorf("write G1.B: %w", err) + } + if meta.G1Z, err = writeMmapDumpSlice(&w, pk.G1.Z); err != nil { + return fmt.Errorf("write G1.Z: %w", err) + } + if meta.G1K, err = writeMmapDumpSlice(&w, pk.G1.K); err != nil { + return fmt.Errorf("write G1.K: %w", err) + } + if meta.G2B, err = writeMmapDumpSlice(&w, pk.G2.B); err != nil { + return fmt.Errorf("write G2.B: %w", err) + } + if meta.InfinityA, err = writeMmapDumpSlice(&w, pk.InfinityA); err != nil { + return fmt.Errorf("write InfinityA: %w", err) + } + if meta.InfinityB, err = writeMmapDumpSlice(&w, pk.InfinityB); err != nil { + return fmt.Errorf("write InfinityB: %w", err) + } + + if len(pk.CommitmentKeys) > 0 { + meta.CommitmentKeys = make([]mmapDumpCommitmentKey, len(pk.CommitmentKeys)) + for i := range pk.CommitmentKeys { + if meta.CommitmentKeys[i].Basis, err = writeMmapDumpSlice(&w, pk.CommitmentKeys[i].Basis); err != nil { + return fmt.Errorf("write commitment key %d basis: %w", i, err) + } + if meta.CommitmentKeys[i].BasisExpSigma, err = writeMmapDumpSlice(&w, pk.CommitmentKeys[i].BasisExpSigma); err != nil { + return fmt.Errorf("write commitment key %d basis exp sigma: %w", i, err) + } + } + } + + metaBytes, err := json.Marshal(meta) + if err != nil { + return fmt.Errorf("marshal metadata: %w", err) + } + if _, err := f.Write(metaBytes); err != nil { + return fmt.Errorf("write metadata: %w", err) + } + if err := binary.Write(f, binary.LittleEndian, uint64(len(metaBytes))); err != nil { + return fmt.Errorf("write metadata length: %w", err) + } + if _, err := f.Write(mmapProvingKeyMagic[:]); err != nil { + return fmt.Errorf("write magic: %w", err) + } + + return nil +} + +// ReadMmapDump maps path into memory and returns a proving key whose large +// slices are backed by the mapped file. The caller must keep the returned key +// open while it is used and call Close after proving is complete. +// +// Mmap dumps are unsafe raw-memory artifacts. ReadMmapDump validates the dump +// metadata against the current process, but it does not validate curve points or +// subgroup membership. +func ReadMmapDump(path string, opts ...MmapDumpOption) (*MmapProvingKey, error) { + var cfg mmapDumpConfig + for _, opt := range opts { + opt(&cfg) + } + + mapping, err := mmap.Open(path) + if err != nil { + return nil, err + } + pk := &MmapProvingKey{mapping: mapping} + if err := pk.ProvingKey.readMmapDump(mapping.Data, cfg); err != nil { + _ = mapping.Close() + return nil, err + } + return pk, nil +} + +func (pk *ProvingKey) readMmapDump(data []byte, cfg mmapDumpConfig) error { + meta, payloadLen, err := readMmapDumpMeta(data) + if err != nil { + return err + } + if err := validateMmapDumpMeta(meta); err != nil { + return err + } + if err := validateMmapDumpSections(meta, payloadLen); err != nil { + return err + } + payload := data[:payloadLen] + + pk.NbInfinityA = meta.NbInfinityA + pk.NbInfinityB = meta.NbInfinityB + + domainBytes, err := sectionBytes(payload, meta.Domain) + if err != nil { + return fmt.Errorf("domain section: %w", err) + } + domainBytes = append([]byte(nil), domainBytes...) + if cfg.disableDomainPrecompute && len(domainBytes) > 0 { + disableDomainPrecompute(domainBytes, cfg.domainNoPrecomputeThreshold) + } + if _, err := pk.Domain.ReadFrom(bytes.NewReader(domainBytes)); err != nil { + return fmt.Errorf("read domain: %w", err) + } + + if pk.G1.Alpha, err = sectionValue[curve.G1Affine](payload, meta.AlphaG1); err != nil { + return fmt.Errorf("read G1.Alpha: %w", err) + } + if pk.G1.Beta, err = sectionValue[curve.G1Affine](payload, meta.BetaG1); err != nil { + return fmt.Errorf("read G1.Beta: %w", err) + } + if pk.G1.Delta, err = sectionValue[curve.G1Affine](payload, meta.DeltaG1); err != nil { + return fmt.Errorf("read G1.Delta: %w", err) + } + if pk.G2.Beta, err = sectionValue[curve.G2Affine](payload, meta.BetaG2); err != nil { + return fmt.Errorf("read G2.Beta: %w", err) + } + if pk.G2.Delta, err = sectionValue[curve.G2Affine](payload, meta.DeltaG2); err != nil { + return fmt.Errorf("read G2.Delta: %w", err) + } + + if pk.G1.A, err = sectionSlice[curve.G1Affine](payload, meta.G1A); err != nil { + return fmt.Errorf("read G1.A: %w", err) + } + if pk.G1.B, err = sectionSlice[curve.G1Affine](payload, meta.G1B); err != nil { + return fmt.Errorf("read G1.B: %w", err) + } + if pk.G1.Z, err = sectionSlice[curve.G1Affine](payload, meta.G1Z); err != nil { + return fmt.Errorf("read G1.Z: %w", err) + } + if pk.G1.K, err = sectionSlice[curve.G1Affine](payload, meta.G1K); err != nil { + return fmt.Errorf("read G1.K: %w", err) + } + if pk.G2.B, err = sectionSlice[curve.G2Affine](payload, meta.G2B); err != nil { + return fmt.Errorf("read G2.B: %w", err) + } + if pk.InfinityA, err = sectionSlice[bool](payload, meta.InfinityA); err != nil { + return fmt.Errorf("read InfinityA: %w", err) + } + if pk.InfinityB, err = sectionSlice[bool](payload, meta.InfinityB); err != nil { + return fmt.Errorf("read InfinityB: %w", err) + } + + if len(meta.CommitmentKeys) > 0 { + pk.CommitmentKeys = make([]pedersen.ProvingKey, len(meta.CommitmentKeys)) + for i := range meta.CommitmentKeys { + if pk.CommitmentKeys[i].Basis, err = sectionSlice[curve.G1Affine](payload, meta.CommitmentKeys[i].Basis); err != nil { + return fmt.Errorf("read commitment key %d basis: %w", i, err) + } + if pk.CommitmentKeys[i].BasisExpSigma, err = sectionSlice[curve.G1Affine](payload, meta.CommitmentKeys[i].BasisExpSigma); err != nil { + return fmt.Errorf("read commitment key %d basis exp sigma: %w", i, err) + } + } + } else { + pk.CommitmentKeys = nil + } + + return validateMmapDumpProvingKey(pk) +} + +type mmapDumpWriter struct { + w io.Writer + pos int64 +} + +func writeMmapDumpValue[T any](w *mmapDumpWriter, v *T) (mmapDumpSection, error) { + var zero T + size := int64(unsafe.Sizeof(zero)) + b := unsafe.Slice((*byte)(unsafe.Pointer(v)), int(size)) + return w.writeBytes(b, size) +} + +func writeMmapDumpSlice[S ~[]E, E any](w *mmapDumpWriter, s S) (mmapDumpSection, error) { + var zero E + size := int64(unsafe.Sizeof(zero)) + if len(s) == 0 { + return w.writeBytes(nil, size) + } + b := unsafe.Slice((*byte)(unsafe.Pointer(&s[0])), len(s)*int(size)) + return w.writeBytes(b, size) +} + +func (w *mmapDumpWriter) writeBytes(b []byte, elementSize int64) (mmapDumpSection, error) { + if elementSize <= 0 { + return mmapDumpSection{}, fmt.Errorf("invalid element size %d", elementSize) + } + if err := w.padTo(mmapProvingKeyAlign); err != nil { + return mmapDumpSection{}, err + } + section := mmapDumpSection{ + Offset: w.pos, + ElementSize: elementSize, + } + if len(b)%int(elementSize) != 0 { + return mmapDumpSection{}, fmt.Errorf("section length %d is not a multiple of element size %d", len(b), elementSize) + } + section.Len = int64(len(b)) / elementSize + if len(b) == 0 { + return section, nil + } + n, err := w.w.Write(b) + w.pos += int64(n) + if err != nil { + return mmapDumpSection{}, err + } + if n != len(b) { + return mmapDumpSection{}, io.ErrShortWrite + } + return section, nil +} + +func (w *mmapDumpWriter) padTo(alignment int64) error { + rem := w.pos % alignment + if rem == 0 { + return nil + } + padding := make([]byte, alignment-rem) + n, err := w.w.Write(padding) + w.pos += int64(n) + if err != nil { + return err + } + if n != len(padding) { + return io.ErrShortWrite + } + return nil +} + +func newMmapDumpMeta(pk *ProvingKey) mmapDumpMeta { + gnarkVersion, gnarkCryptoVersion := dependencyVersions() + return mmapDumpMeta{ + Format: mmapProvingKeyFormat, + Version: mmapProvingKeyVersion, + Curve: "{{ toLower .Curve }}", + GOOS: runtime.GOOS, + GOARCH: runtime.GOARCH, + GoCompiler: runtime.Compiler, + GoVersion: runtime.Version(), + Endian: nativeEndian(), + PointerSize: int(unsafe.Sizeof(uintptr(0))), + GnarkVersion: gnarkVersion, + GnarkCryptoVersion: gnarkCryptoVersion, + Sizes: currentMmapDumpSizes(), + NbInfinityA: pk.NbInfinityA, + NbInfinityB: pk.NbInfinityB, + CommitmentKeys: nil, + } +} + +func currentMmapDumpSizes() mmapDumpSizes { + return mmapDumpSizes{ + G1Affine: int(unsafe.Sizeof(curve.G1Affine{})), + G2Affine: int(unsafe.Sizeof(curve.G2Affine{})), + Bool: int(unsafe.Sizeof(false)), + } +} + +func readMmapDumpMeta(data []byte) (mmapDumpMeta, int, error) { + var meta mmapDumpMeta + trailerLen := 8 + len(mmapProvingKeyMagic) + if len(data) < trailerLen { + return meta, 0, fmt.Errorf("file too small") + } + magicOff := len(data) - len(mmapProvingKeyMagic) + if !bytes.Equal(data[magicOff:], mmapProvingKeyMagic[:]) { + return meta, 0, fmt.Errorf("invalid mmap dump magic") + } + metaLenOff := magicOff - 8 + metaLen := binary.LittleEndian.Uint64(data[metaLenOff:magicOff]) + if metaLen == 0 || metaLen > uint64(metaLenOff) { + return meta, 0, fmt.Errorf("invalid metadata length %d", metaLen) + } + metaOff := metaLenOff - int(metaLen) + if err := json.Unmarshal(data[metaOff:metaLenOff], &meta); err != nil { + return meta, 0, fmt.Errorf("unmarshal metadata: %w", err) + } + return meta, metaOff, nil +} + +func validateMmapDumpMeta(meta mmapDumpMeta) error { + if meta.Format != mmapProvingKeyFormat { + return fmt.Errorf("unsupported format %q", meta.Format) + } + if meta.Version != mmapProvingKeyVersion { + return fmt.Errorf("unsupported mmap dump version %d", meta.Version) + } + if meta.Curve != "{{ toLower .Curve }}" { + return fmt.Errorf("unsupported curve %q", meta.Curve) + } + if meta.GOOS != runtime.GOOS { + return fmt.Errorf("goos mismatch: dump=%s runtime=%s", meta.GOOS, runtime.GOOS) + } + if meta.GOARCH != runtime.GOARCH { + return fmt.Errorf("goarch mismatch: dump=%s runtime=%s", meta.GOARCH, runtime.GOARCH) + } + if meta.GoCompiler != "" && meta.GoCompiler != runtime.Compiler { + return fmt.Errorf("go compiler mismatch: dump=%s runtime=%s", meta.GoCompiler, runtime.Compiler) + } + if meta.GoVersion != "" && meta.GoVersion != runtime.Version() { + return fmt.Errorf("go version mismatch: dump=%s runtime=%s", meta.GoVersion, runtime.Version()) + } + if meta.Endian != nativeEndian() { + return fmt.Errorf("endianness mismatch: dump=%s runtime=%s", meta.Endian, nativeEndian()) + } + if meta.PointerSize != int(unsafe.Sizeof(uintptr(0))) { + return fmt.Errorf("pointer size mismatch: dump=%d runtime=%d", meta.PointerSize, unsafe.Sizeof(uintptr(0))) + } + if meta.Sizes != currentMmapDumpSizes() { + return fmt.Errorf("type size mismatch: dump=%+v runtime=%+v", meta.Sizes, currentMmapDumpSizes()) + } + gnarkVersion, gnarkCryptoVersion := dependencyVersions() + if meta.GnarkVersion != "" && gnarkVersion != "" && meta.GnarkVersion != gnarkVersion { + return fmt.Errorf("gnark version mismatch: dump=%s runtime=%s", meta.GnarkVersion, gnarkVersion) + } + if meta.GnarkCryptoVersion != "" && gnarkCryptoVersion != "" && meta.GnarkCryptoVersion != gnarkCryptoVersion { + return fmt.Errorf("gnark-crypto version mismatch: dump=%s runtime=%s", meta.GnarkCryptoVersion, gnarkCryptoVersion) + } + return nil +} + +type namedMmapDumpSection struct { + name string + section mmapDumpSection +} + +func validateMmapDumpSections(meta mmapDumpMeta, payloadLen int) error { + sections := []namedMmapDumpSection{ + {"domain", meta.Domain}, + {"alpha_g1", meta.AlphaG1}, + {"beta_g1", meta.BetaG1}, + {"delta_g1", meta.DeltaG1}, + {"beta_g2", meta.BetaG2}, + {"delta_g2", meta.DeltaG2}, + {"g1_a", meta.G1A}, + {"g1_b", meta.G1B}, + {"g1_z", meta.G1Z}, + {"g1_k", meta.G1K}, + {"g2_b", meta.G2B}, + {"infinity_a", meta.InfinityA}, + {"infinity_b", meta.InfinityB}, + } + for i := range meta.CommitmentKeys { + sections = append(sections, + namedMmapDumpSection{fmt.Sprintf("commitment_keys[%d].basis", i), meta.CommitmentKeys[i].Basis}, + namedMmapDumpSection{fmt.Sprintf("commitment_keys[%d].basis_exp_sigma", i), meta.CommitmentKeys[i].BasisExpSigma}, + ) + } + + ranges := make([]struct { + name string + start, end int64 + }, 0, len(sections)) + for _, named := range sections { + start, end, err := mmapDumpSectionRange(named.name, named.section, payloadLen) + if err != nil { + return err + } + if start == end { + continue + } + for _, prev := range ranges { + if start < prev.end && prev.start < end { + return fmt.Errorf("section %s overlaps section %s", named.name, prev.name) + } + } + ranges = append(ranges, struct { + name string + start, end int64 + }{named.name, start, end}) + } + return nil +} + +func mmapDumpSectionRange(name string, section mmapDumpSection, payloadLen int) (int64, int64, error) { + if section.Offset < 0 || section.Len < 0 || section.ElementSize <= 0 { + return 0, 0, fmt.Errorf("invalid section %s: %+v", name, section) + } + if section.Offset%mmapProvingKeyAlign != 0 { + return 0, 0, fmt.Errorf("section %s offset %d is not aligned to %d", name, section.Offset, mmapProvingKeyAlign) + } + if section.Offset > int64(payloadLen) { + return 0, 0, fmt.Errorf("section %s starts outside payload: offset=%d payload=%d", name, section.Offset, payloadLen) + } + if section.Len == 0 { + return section.Offset, section.Offset, nil + } + const maxInt64 = int64(^uint64(0) >> 1) + if section.Len > maxInt64/section.ElementSize { + return 0, 0, fmt.Errorf("section %s size overflow", name) + } + size := section.Len * section.ElementSize + if size > int64(payloadLen)-section.Offset { + return 0, 0, fmt.Errorf("section %s out of bounds: offset=%d len=%d element_size=%d payload=%d", name, section.Offset, section.Len, section.ElementSize, payloadLen) + } + return section.Offset, section.Offset + size, nil +} + +func validateMmapDumpProvingKey(pk *ProvingKey) error { + if pk.NbInfinityA > uint64(len(pk.InfinityA)) { + return fmt.Errorf("NbInfinityA=%d exceeds InfinityA length %d", pk.NbInfinityA, len(pk.InfinityA)) + } + if pk.NbInfinityB > uint64(len(pk.InfinityB)) { + return fmt.Errorf("NbInfinityB=%d exceeds InfinityB length %d", pk.NbInfinityB, len(pk.InfinityB)) + } + if uint64(len(pk.G1.A))+pk.NbInfinityA != uint64(len(pk.InfinityA)) { + return fmt.Errorf("inconsistent G1.A and InfinityA lengths: len(G1.A)=%d NbInfinityA=%d len(InfinityA)=%d", len(pk.G1.A), pk.NbInfinityA, len(pk.InfinityA)) + } + if uint64(len(pk.G1.B))+pk.NbInfinityB != uint64(len(pk.InfinityB)) { + return fmt.Errorf("inconsistent G1.B and InfinityB lengths: len(G1.B)=%d NbInfinityB=%d len(InfinityB)=%d", len(pk.G1.B), pk.NbInfinityB, len(pk.InfinityB)) + } + if len(pk.G2.B) != len(pk.G1.B) { + return fmt.Errorf("inconsistent B vector lengths: len(G1.B)=%d len(G2.B)=%d", len(pk.G1.B), len(pk.G2.B)) + } + if uint64(len(pk.G1.Z))+1 != pk.Domain.Cardinality { + return fmt.Errorf("inconsistent Z length: len(G1.Z)=%d domain cardinality=%d", len(pk.G1.Z), pk.Domain.Cardinality) + } + for i := range pk.CommitmentKeys { + if len(pk.CommitmentKeys[i].Basis) != len(pk.CommitmentKeys[i].BasisExpSigma) { + return fmt.Errorf("inconsistent commitment key %d lengths: len(Basis)=%d len(BasisExpSigma)=%d", i, len(pk.CommitmentKeys[i].Basis), len(pk.CommitmentKeys[i].BasisExpSigma)) + } + } + return nil +} + +func sectionValue[T any](data []byte, section mmapDumpSection) (T, error) { + var zero T + s, err := sectionSlice[T](data, section) + if err != nil { + return zero, err + } + if len(s) != 1 { + return zero, fmt.Errorf("expected one element, got %d", len(s)) + } + return s[0], nil +} + +func sectionSlice[T any](data []byte, section mmapDumpSection) ([]T, error) { + var zero T + size := int64(unsafe.Sizeof(zero)) + align := int64(unsafe.Alignof(zero)) + if section.ElementSize != size { + return nil, fmt.Errorf("element size mismatch: section=%d type=%d", section.ElementSize, size) + } + if section.Len == 0 { + return nil, nil + } + if section.Offset%align != 0 { + return nil, fmt.Errorf("section offset %d is not aligned to %d", section.Offset, align) + } + b, err := sectionBytes(data, section) + if err != nil { + return nil, err + } + if uintptr(unsafe.Pointer(&b[0]))%uintptr(align) != 0 { + return nil, fmt.Errorf("mapped section address is not aligned to %d", align) + } + return unsafe.Slice((*T)(unsafe.Pointer(&b[0])), int(section.Len)), nil +} + +func sectionBytes(data []byte, section mmapDumpSection) ([]byte, error) { + if section.Offset < 0 || section.Len < 0 || section.ElementSize <= 0 { + return nil, fmt.Errorf("invalid section %+v", section) + } + if section.Len == 0 { + return nil, nil + } + const maxInt64 = int64(^uint64(0) >> 1) + if section.Len > maxInt64/section.ElementSize { + return nil, fmt.Errorf("section size overflow") + } + size := section.Len * section.ElementSize + if section.Offset > int64(len(data)) || size > int64(len(data))-section.Offset { + return nil, fmt.Errorf("section out of bounds: offset=%d len=%d element_size=%d file=%d", section.Offset, section.Len, section.ElementSize, len(data)) + } + return data[section.Offset : section.Offset+size], nil +} + +func disableDomainPrecompute(domain []byte, threshold uint64) { + if len(domain) < 9 { + return + } + cardinality := binary.BigEndian.Uint64(domain[:8]) + if cardinality >= threshold { + // fft.Domain.WriteTo serializes the withPrecompute bool as the final byte. + domain[len(domain)-1] = 0 + } +} + +func nativeEndian() string { + var x uint16 = 1 + if *(*byte)(unsafe.Pointer(&x)) == 1 { + return "little" + } + return "big" +} + +func dependencyVersions() (string, string) { + info, ok := debug.ReadBuildInfo() + if !ok { + return "", "" + } + var gnarkVersion, gnarkCryptoVersion string + if info.Main.Path == "github.com/consensys/gnark" { + gnarkVersion = info.Main.Version + } + for _, dep := range info.Deps { + switch dep.Path { + case "github.com/consensys/gnark": + gnarkVersion = dep.Version + case "github.com/consensys/gnark-crypto": + gnarkCryptoVersion = dep.Version + } + } + return gnarkVersion, gnarkCryptoVersion +} diff --git a/internal/generator/backend/template/zkpschemes/groth16/tests/groth16.mmapdump.go.tmpl b/internal/generator/backend/template/zkpschemes/groth16/tests/groth16.mmapdump.go.tmpl new file mode 100644 index 0000000000..c83e67d919 --- /dev/null +++ b/internal/generator/backend/template/zkpschemes/groth16/tests/groth16.mmapdump.go.tmpl @@ -0,0 +1,307 @@ +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/consensys/gnark-crypto/ecc" + {{ template "import_fr" . }} + "github.com/consensys/gnark/backend/witness" + {{ template "import_backend_cs" . }} + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/frontend/cs/r1cs" + "github.com/consensys/gnark/internal/backend/ioutils/mmap" +) + +func skipMmapDumpUnsupported(t testing.TB) { + t.Helper() + if !mmap.Supported() { + t.Skip("mmap is unsupported on this platform") + } +} + +func TestMmapDumpUnsupportedPlatform(t *testing.T) { + if mmap.Supported() { + t.Skip("mmap is supported on this platform") + } + + var pk ProvingKey + path := filepath.Join(t.TempDir(), "proving_key.mmap") + if err := pk.WriteMmapDump(path); err == nil { + t.Fatal("expected error") + } + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatalf("expected no dump file to be created, got %v", err) + } +} + +type mmapDumpCircuit struct { + X frontend.Variable + Y frontend.Variable `gnark:",public"` +} + +func (c *mmapDumpCircuit) Define(api frontend.API) error { + api.AssertIsEqual(api.Mul(c.X, c.X, c.X), c.Y) + return nil +} + +type mmapDumpCommitmentCircuit struct { + One frontend.Variable + Two frontend.Variable `gnark:",public"` +} + +func (c *mmapDumpCommitmentCircuit) Define(api frontend.API) error { + commitCompiler, ok := api.(frontend.Committer) + if !ok { + return fmt.Errorf("compiler does not commit") + } + commitment, err := commitCompiler.Commit(c.One, c.Two) + if err != nil { + return err + } + api.AssertIsDifferent(commitment, 0) + api.AssertIsEqual(c.One, 1) + api.AssertIsEqual(c.Two, 2) + return nil +} + +const mmapDumpBenchmarkCircuitSize = 2048 + +type mmapDumpBenchmarkCircuit struct { + X [mmapDumpBenchmarkCircuitSize]frontend.Variable + Y frontend.Variable `gnark:",public"` +} + +func (c *mmapDumpBenchmarkCircuit) Define(api frontend.API) error { + acc := frontend.Variable(1) + for i := range c.X { + acc = api.Mul(acc, api.Add(c.X[i], 1)) + } + api.AssertIsEqual(acc, c.Y) + return nil +} + +func TestMmapDumpProvingKeyRoundTrip(t *testing.T) { + skipMmapDumpUnsupported(t) + + ccs, err := frontend.Compile(ecc.{{ .CurveID }}.ScalarField(), r1cs.NewBuilder, &mmapDumpCircuit{}) + if err != nil { + t.Fatal(err) + } + r1cs := ccs.(*cs.R1CS) + + var pk ProvingKey + var vk VerifyingKey + if err := Setup(r1cs, &pk, &vk); err != nil { + t.Fatal(err) + } + + path := filepath.Join(t.TempDir(), "proving_key.mmap") + if err := pk.WriteMmapDump(path); err != nil { + t.Fatal(err) + } + + mappedPK, err := ReadMmapDump(path, WithMmapDumpNoDomainPrecompute(1)) + if err != nil { + t.Fatal(err) + } + defer mappedPK.Close() + + fullWitness, publicWitness := mmapDumpWitnesses(t) + proof, err := Prove(r1cs, &mappedPK.ProvingKey, fullWitness) + if err != nil { + t.Fatal(err) + } + if err := Verify(proof, &vk, publicWitness.Vector().(fr.Vector)); err != nil { + t.Fatal(err) + } + + if err := mappedPK.Close(); err != nil { + t.Fatal(err) + } + if mappedPK.G1.A != nil { + t.Fatal("expected mapped key to be cleared after close") + } +} + +func TestMmapDumpProvingKeyWithCommitment(t *testing.T) { + skipMmapDumpUnsupported(t) + + ccs, err := frontend.Compile(ecc.{{ .CurveID }}.ScalarField(), r1cs.NewBuilder, &mmapDumpCommitmentCircuit{}) + if err != nil { + t.Fatal(err) + } + r1cs := ccs.(*cs.R1CS) + + var pk ProvingKey + var vk VerifyingKey + if err := Setup(r1cs, &pk, &vk); err != nil { + t.Fatal(err) + } + if len(pk.CommitmentKeys) == 0 { + t.Fatal("expected commitment proving keys") + } + + path := filepath.Join(t.TempDir(), "proving_key.mmap") + if err := pk.WriteMmapDump(path); err != nil { + t.Fatal(err) + } + + mappedPK, err := ReadMmapDump(path) + if err != nil { + t.Fatal(err) + } + defer mappedPK.Close() + + assignment := mmapDumpCommitmentCircuit{One: 1, Two: 2} + fullWitness, err := frontend.NewWitness(&assignment, ecc.{{ .CurveID }}.ScalarField()) + if err != nil { + t.Fatal(err) + } + publicWitness, err := fullWitness.Public() + if err != nil { + t.Fatal(err) + } + + proof, err := Prove(r1cs, &mappedPK.ProvingKey, fullWitness) + if err != nil { + t.Fatal(err) + } + if err := Verify(proof, &vk, publicWitness.Vector().(fr.Vector)); err != nil { + t.Fatal(err) + } +} + +func TestMmapDumpPreservesDomainByDefault(t *testing.T) { + skipMmapDumpUnsupported(t) + + ccs, err := frontend.Compile(ecc.{{ .CurveID }}.ScalarField(), r1cs.NewBuilder, &mmapDumpCircuit{}) + if err != nil { + t.Fatal(err) + } + r1cs := ccs.(*cs.R1CS) + + var pk ProvingKey + var vk VerifyingKey + if err := Setup(r1cs, &pk, &vk); err != nil { + t.Fatal(err) + } + + path := filepath.Join(t.TempDir(), "proving_key.mmap") + if err := pk.WriteMmapDump(path); err != nil { + t.Fatal(err) + } + + mappedPK, err := ReadMmapDump(path) + if err != nil { + t.Fatal(err) + } + defer mappedPK.Close() + + var originalDomain, mappedDomain bytes.Buffer + if _, err := pk.Domain.WriteTo(&originalDomain); err != nil { + t.Fatal(err) + } + if _, err := mappedPK.Domain.WriteTo(&mappedDomain); err != nil { + t.Fatal(err) + } + if !bytes.Equal(originalDomain.Bytes(), mappedDomain.Bytes()) { + t.Fatal("expected default mmap dump load to preserve serialized domain") + } +} + +func mmapDumpWitnesses(t *testing.T) (witness.Witness, witness.Witness) { + t.Helper() + + assignment := mmapDumpCircuit{X: 3, Y: 27} + fullWitness, err := frontend.NewWitness(&assignment, ecc.{{ .CurveID }}.ScalarField()) + if err != nil { + t.Fatal(err) + } + publicWitness, err := fullWitness.Public() + if err != nil { + t.Fatal(err) + } + return fullWitness, publicWitness +} + +func BenchmarkMmapDumpProvingKeyLoad(b *testing.B) { + skipMmapDumpUnsupported(b) + + pk := mmapDumpBenchmarkProvingKey(b) + + var dump bytes.Buffer + if err := pk.WriteDump(&dump); err != nil { + b.Fatal(err) + } + + mmapPath := filepath.Join(b.TempDir(), "proving_key.mmap") + if err := pk.WriteMmapDump(mmapPath); err != nil { + b.Fatal(err) + } + + dumpBytes := dump.Bytes() + + b.Run("ReadDump", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + var loaded ProvingKey + if err := loaded.ReadDump(bytes.NewReader(dumpBytes)); err != nil { + b.Fatal(err) + } + if len(loaded.G1.A) == 0 { + b.Fatal("empty proving key") + } + } + }) + + b.Run("ReadMmapDump", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + loaded, err := ReadMmapDump(mmapPath) + if err != nil { + b.Fatal(err) + } + if len(loaded.G1.A) == 0 { + b.Fatal("empty proving key") + } + if err := loaded.Close(); err != nil { + b.Fatal(err) + } + } + }) + + b.Run("ReadMmapDumpNoDomainPrecompute", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + loaded, err := ReadMmapDump(mmapPath, WithMmapDumpNoDomainPrecompute(1)) + if err != nil { + b.Fatal(err) + } + if len(loaded.G1.A) == 0 { + b.Fatal("empty proving key") + } + if err := loaded.Close(); err != nil { + b.Fatal(err) + } + } + }) +} + +func mmapDumpBenchmarkProvingKey(b *testing.B) *ProvingKey { + b.Helper() + + ccs, err := frontend.Compile(ecc.{{ .CurveID }}.ScalarField(), r1cs.NewBuilder, &mmapDumpBenchmarkCircuit{}) + if err != nil { + b.Fatal(err) + } + r1cs := ccs.(*cs.R1CS) + + var pk ProvingKey + var vk VerifyingKey + if err := Setup(r1cs, &pk, &vk); err != nil { + b.Fatal(err) + } + return &pk +} From ee64ba82b7f189b55b30035fb06b256ad537ad50 Mon Sep 17 00:00:00 2001 From: Sergey Timoshin Date: Sun, 14 Jun 2026 18:28:06 +0100 Subject: [PATCH 2/2] feat: add file-backed constraint system loading --- backend/groth16/cs_file_test.go | 124 ++++++++++++++++++ backend/groth16/groth16.go | 37 ++++++ backend/plonk/cs_file_test.go | 71 ++++++++++ backend/plonk/plonk.go | 38 ++++++ constraint/babybear/coeff.go | 9 +- constraint/babybear/marshal.go | 111 ++++++++++++++-- constraint/babybear/r1cs_test.go | 30 ++++- constraint/bls12-377/coeff.go | 9 +- constraint/bls12-377/marshal.go | 111 ++++++++++++++-- constraint/bls12-377/r1cs_test.go | 30 ++++- constraint/bls12-381/coeff.go | 9 +- constraint/bls12-381/marshal.go | 111 ++++++++++++++-- constraint/bls12-381/r1cs_test.go | 30 ++++- constraint/bn254/coeff.go | 9 +- constraint/bn254/coeff_test.go | 16 +++ constraint/bn254/marshal.go | 111 ++++++++++++++-- constraint/bn254/r1cs_test.go | 30 ++++- constraint/bw6-761/coeff.go | 9 +- constraint/bw6-761/marshal.go | 111 ++++++++++++++-- constraint/bw6-761/r1cs_test.go | 30 ++++- constraint/grumpkin/coeff.go | 9 +- constraint/grumpkin/marshal.go | 111 ++++++++++++++-- constraint/koalabear/coeff.go | 9 +- constraint/koalabear/marshal.go | 111 ++++++++++++++-- constraint/koalabear/r1cs_test.go | 30 ++++- constraint/marshal.go | 55 ++++++-- constraint/marshal_test.go | 90 +++++++++++++ constraint/tinyfield/coeff.go | 9 +- constraint/tinyfield/marshal.go | 111 ++++++++++++++-- constraint/tinyfield/r1cs_test.go | 30 ++++- internal/backend/ioutils/intcomp.go | 28 ++-- internal/backend/ioutils/intcomp_test.go | 13 ++ .../template/representations/coeff.go.tmpl | 11 +- .../template/representations/marshal.go.tmpl | 113 +++++++++++++--- .../representations/tests/r1cs.go.tmpl | 32 ++++- 35 files changed, 1605 insertions(+), 193 deletions(-) create mode 100644 backend/groth16/cs_file_test.go create mode 100644 backend/plonk/cs_file_test.go create mode 100644 constraint/bn254/coeff_test.go create mode 100644 constraint/marshal_test.go diff --git a/backend/groth16/cs_file_test.go b/backend/groth16/cs_file_test.go new file mode 100644 index 0000000000..008732335c --- /dev/null +++ b/backend/groth16/cs_file_test.go @@ -0,0 +1,124 @@ +package groth16_test + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark/backend/groth16" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/frontend/cs/r1cs" +) + +type csFileCircuit struct { + X frontend.Variable + Y frontend.Variable `gnark:",public"` +} + +func (c *csFileCircuit) Define(api frontend.API) error { + api.AssertIsEqual(api.Mul(c.X, c.X, c.X), c.Y) + return nil +} + +func TestReadCSFromFile(t *testing.T) { + for _, curve := range getCurves() { + t.Run(curve.String(), func(t *testing.T) { + ccs, err := frontend.Compile(curve.ScalarField(), r1cs.NewBuilder, &csFileCircuit{}) + if err != nil { + t.Fatal(err) + } + + var encoded bytes.Buffer + if _, err := ccs.WriteTo(&encoded); err != nil { + t.Fatal(err) + } + + path := filepath.Join(t.TempDir(), "r1cs.bin") + if err := os.WriteFile(path, encoded.Bytes(), 0600); err != nil { + t.Fatal(err) + } + + decoded, err := groth16.ReadCSFromFile(curve, path) + if err != nil { + t.Fatal(err) + } + if decoded.GetNbConstraints() != ccs.GetNbConstraints() { + t.Fatalf("unexpected constraint count: got %d want %d", decoded.GetNbConstraints(), ccs.GetNbConstraints()) + } + + var reencoded bytes.Buffer + if _, err := decoded.WriteTo(&reencoded); err != nil { + t.Fatal(err) + } + if !bytes.Equal(encoded.Bytes(), reencoded.Bytes()) { + t.Fatal("constraint system changed after file round trip") + } + }) + } +} + +func TestReadCSFromFileErrorReturnsNil(t *testing.T) { + path := filepath.Join(t.TempDir(), "missing.r1cs") + decoded, err := groth16.ReadCSFromFile(ecc.BN254, path) + if err == nil { + t.Fatal("expected error") + } + if decoded != nil { + t.Fatal("expected nil constraint system on error") + } +} + +const csFileBenchmarkCircuitSize = 2048 + +type csFileBenchmarkCircuit struct { + X [csFileBenchmarkCircuitSize]frontend.Variable + Y frontend.Variable `gnark:",public"` +} + +func (c *csFileBenchmarkCircuit) Define(api frontend.API) error { + acc := frontend.Variable(1) + for i := range c.X { + acc = api.Mul(acc, api.Add(c.X[i], i+1)) + } + api.AssertIsEqual(acc, c.Y) + return nil +} + +func BenchmarkReadCSFromFile(b *testing.B) { + ccs, err := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, &csFileBenchmarkCircuit{}) + if err != nil { + b.Fatal(err) + } + + var encoded bytes.Buffer + if _, err := ccs.WriteTo(&encoded); err != nil { + b.Fatal(err) + } + encodedBytes := encoded.Bytes() + + path := filepath.Join(b.TempDir(), "r1cs.bin") + if err := os.WriteFile(path, encodedBytes, 0600); err != nil { + b.Fatal(err) + } + + b.Run("ReadFrom", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + decoded := groth16.NewCS(ecc.BN254) + if _, err := decoded.ReadFrom(bytes.NewReader(encodedBytes)); err != nil { + b.Fatal(err) + } + } + }) + + b.Run("ReadCSFromFile", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + if _, err := groth16.ReadCSFromFile(ecc.BN254, path); err != nil { + b.Fatal(err) + } + } + }) +} diff --git a/backend/groth16/groth16.go b/backend/groth16/groth16.go index 14a3f852f6..669ca4977c 100644 --- a/backend/groth16/groth16.go +++ b/backend/groth16/groth16.go @@ -506,3 +506,40 @@ func NewCS(curveID ecc.ID) constraint.ConstraintSystem { } return r1cs } + +// ReadCSFromFile instantiates a concrete curve-typed R1CS and decodes it from +// path. +// +// On platforms that support mmap, ReadCSFromFile avoids allocating a temporary +// copy of the serialized payload while reading. It falls back to regular +// ReadFrom semantics on unsupported platforms. +func ReadCSFromFile(curveID ecc.ID, path string) (constraint.ConstraintSystem, error) { + switch curveID { + case ecc.BN254: + r1cs := &cs_bn254.R1CS{} + if _, err := r1cs.ReadFromFile(path); err != nil { + return nil, err + } + return r1cs, nil + case ecc.BLS12_377: + r1cs := &cs_bls12377.R1CS{} + if _, err := r1cs.ReadFromFile(path); err != nil { + return nil, err + } + return r1cs, nil + case ecc.BLS12_381: + r1cs := &cs_bls12381.R1CS{} + if _, err := r1cs.ReadFromFile(path); err != nil { + return nil, err + } + return r1cs, nil + case ecc.BW6_761: + r1cs := &cs_bw6761.R1CS{} + if _, err := r1cs.ReadFromFile(path); err != nil { + return nil, err + } + return r1cs, nil + default: + return nil, fmt.Errorf("unsupported Groth16 curve %s", curveID) + } +} diff --git a/backend/plonk/cs_file_test.go b/backend/plonk/cs_file_test.go new file mode 100644 index 0000000000..ddf0ca93bc --- /dev/null +++ b/backend/plonk/cs_file_test.go @@ -0,0 +1,71 @@ +package plonk_test + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark/backend/plonk" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/frontend/cs/scs" +) + +type csFileCircuit struct { + X frontend.Variable + Y frontend.Variable `gnark:",public"` +} + +func (c *csFileCircuit) Define(api frontend.API) error { + api.AssertIsEqual(api.Mul(c.X, c.X, c.X), c.Y) + return nil +} + +func TestReadCSFromFile(t *testing.T) { + for _, curve := range getCurves() { + t.Run(curve.String(), func(t *testing.T) { + ccs, err := frontend.Compile(curve.ScalarField(), scs.NewBuilder, &csFileCircuit{}) + if err != nil { + t.Fatal(err) + } + + var encoded bytes.Buffer + if _, err := ccs.WriteTo(&encoded); err != nil { + t.Fatal(err) + } + + path := filepath.Join(t.TempDir(), "scs.bin") + if err := os.WriteFile(path, encoded.Bytes(), 0600); err != nil { + t.Fatal(err) + } + + decoded, err := plonk.ReadCSFromFile(curve, path) + if err != nil { + t.Fatal(err) + } + if decoded.GetNbConstraints() != ccs.GetNbConstraints() { + t.Fatalf("unexpected constraint count: got %d want %d", decoded.GetNbConstraints(), ccs.GetNbConstraints()) + } + + var reencoded bytes.Buffer + if _, err := decoded.WriteTo(&reencoded); err != nil { + t.Fatal(err) + } + if !bytes.Equal(encoded.Bytes(), reencoded.Bytes()) { + t.Fatal("constraint system changed after file round trip") + } + }) + } +} + +func TestReadCSFromFileErrorReturnsNil(t *testing.T) { + path := filepath.Join(t.TempDir(), "missing.scs") + decoded, err := plonk.ReadCSFromFile(ecc.BN254, path) + if err == nil { + t.Fatal("expected error") + } + if decoded != nil { + t.Fatal("expected nil constraint system on error") + } +} diff --git a/backend/plonk/plonk.go b/backend/plonk/plonk.go index 45d6957aef..d1b5a1fd60 100644 --- a/backend/plonk/plonk.go +++ b/backend/plonk/plonk.go @@ -9,6 +9,7 @@ package plonk import ( + "fmt" "io" "github.com/consensys/gnark-crypto/ecc" @@ -191,6 +192,43 @@ func NewCS(curveID ecc.ID) constraint.ConstraintSystem { return r1cs } +// ReadCSFromFile instantiates a concrete curve-typed SparseR1CS and decodes it +// from path. +// +// On platforms that support mmap, ReadCSFromFile avoids allocating a temporary +// copy of the serialized payload while reading. It falls back to regular +// ReadFrom semantics on unsupported platforms. +func ReadCSFromFile(curveID ecc.ID, path string) (constraint.ConstraintSystem, error) { + switch curveID { + case ecc.BN254: + r1cs := &cs_bn254.SparseR1CS{} + if _, err := r1cs.ReadFromFile(path); err != nil { + return nil, err + } + return r1cs, nil + case ecc.BLS12_377: + r1cs := &cs_bls12377.SparseR1CS{} + if _, err := r1cs.ReadFromFile(path); err != nil { + return nil, err + } + return r1cs, nil + case ecc.BLS12_381: + r1cs := &cs_bls12381.SparseR1CS{} + if _, err := r1cs.ReadFromFile(path); err != nil { + return nil, err + } + return r1cs, nil + case ecc.BW6_761: + r1cs := &cs_bw6761.SparseR1CS{} + if _, err := r1cs.ReadFromFile(path); err != nil { + return nil, err + } + return r1cs, nil + default: + return nil, fmt.Errorf("unsupported Plonk curve %s", curveID) + } +} + // NewProvingKey instantiates a curve-typed ProvingKey and returns an interface // This function exists for serialization purposes func NewProvingKey(curveID ecc.ID) ProvingKey { diff --git a/constraint/babybear/coeff.go b/constraint/babybear/coeff.go index 1c64fda577..604ee4966f 100644 --- a/constraint/babybear/coeff.go +++ b/constraint/babybear/coeff.go @@ -59,13 +59,14 @@ func (ct *CoeffTable) fromBytes(buf []byte) error { ctLen := binary.LittleEndian.Uint64(buf[:8]) buf = buf[8:] - if uint64(len(buf)) < ctLen*fr.Bytes { + if ctLen > uint64(len(buf)/fr.Bytes) { return errors.New("invalid buffer size") } - ct.Coefficients = make([]fr.Element, ctLen) - for i := uint64(0); i < ctLen; i++ { + ctLenInt := int(ctLen) + ct.Coefficients = make([]fr.Element, ctLenInt) + for i := 0; i < ctLenInt; i++ { var c fr.Element - k := int(i) * fr.Bytes + k := i * fr.Bytes for j := 0; j < fr.Limbs; j++ { c[j] = binary.LittleEndian.Uint32(buf[k+j*4 : k+(j+1)*4]) } diff --git a/constraint/babybear/marshal.go b/constraint/babybear/marshal.go index 68bbdb092f..f51a5c1841 100644 --- a/constraint/babybear/marshal.go +++ b/constraint/babybear/marshal.go @@ -9,10 +9,14 @@ import ( "encoding/binary" "fmt" "io" + "os" "github.com/blang/semver/v4" + "github.com/consensys/gnark/internal/backend/ioutils/mmap" ) +const serializedCSHeaderLen = 4 * 8 + // WriteTo encodes R1CS into provided io.Writer using cbor func (cs *system) WriteTo(w io.Writer) (int64, error) { b, err := cs.System.ToBytes() @@ -52,39 +56,118 @@ func (cs *system) WriteTo(w io.Writer) (int64, error) { // ReadFrom attempts to decode R1CS from io.Reader using cbor func (cs *system) ReadFrom(r io.Reader) (int64, error) { - var totalLen uint64 - if err := binary.Read(r, binary.LittleEndian, &totalLen); err != nil { + h, err := readSerializationHeader(r) + if err != nil { return 0, err } - - var major, minor, patch uint64 - if err := binary.Read(r, binary.LittleEndian, &major); err != nil { + if err := h.validate(); err != nil { return 0, err } - if err := binary.Read(r, binary.LittleEndian, &minor); err != nil { - return 0, err + if !serializedCSPayloadLenFits(h.totalLen) { + return 0, fmt.Errorf("serialized constraint system is too large: %d bytes", h.totalLen) } - if err := binary.Read(r, binary.LittleEndian, &patch); err != nil { + + data := make([]byte, h.totalLen) + if _, err := io.ReadFull(r, data); err != nil { return 0, err } - // TODO @gbotrel validate version, duplicate logic with core.go CheckSerializationHeader - if major != 0 || minor < 10 { - return 0, fmt.Errorf("unsupported gnark version %d.%d.%d", major, minor, patch) + return cs.readPayload(data, h.totalLen) +} + +// ReadFromFile decodes a constraint system from path. +// +// On platforms that support mmap, ReadFromFile decodes directly from mapped +// file pages and avoids allocating a temporary copy of the serialized payload. +// It falls back to ReadFrom on unsupported platforms. +func (cs *system) ReadFromFile(path string) (n int64, err error) { + if !mmap.Supported() { + f, err := os.Open(path) + if err != nil { + return 0, err + } + defer f.Close() + return cs.ReadFrom(f) } - data := make([]byte, totalLen) - if _, err := io.ReadFull(r, data); err != nil { + mapping, err := mmap.Open(path) + if err != nil { + return 0, err + } + defer func() { + if closeErr := mapping.Close(); err == nil { + err = closeErr + } + }() + + return cs.readFromBytes(mapping.Data) +} + +func (cs *system) readFromBytes(data []byte) (int64, error) { + h, err := parseSerializationHeader(data) + if err != nil { + return 0, err + } + if err := h.validate(); err != nil { return 0, err } + if h.totalLen > uint64(len(data)-serializedCSHeaderLen) { + return 0, io.ErrUnexpectedEOF + } + return cs.readPayload(data[serializedCSHeaderLen:serializedCSHeaderLen+int(h.totalLen)], h.totalLen) +} + +func (cs *system) readPayload(data []byte, totalLen uint64) (int64, error) { n, err := cs.System.FromBytes(data) if err != nil { return 0, err } + if n > len(data) { + return 0, io.ErrUnexpectedEOF + } data = data[n:] if err := cs.CoeffTable.fromBytes(data); err != nil { return 0, err } - return int64(totalLen) + 4*8, nil + return int64(totalLen) + serializedCSHeaderLen, nil +} + +type serializationHeader struct { + totalLen uint64 + major uint64 + minor uint64 + patch uint64 +} + +func readSerializationHeader(r io.Reader) (serializationHeader, error) { + var buf [serializedCSHeaderLen]byte + if _, err := io.ReadFull(r, buf[:]); err != nil { + return serializationHeader{}, err + } + return parseSerializationHeader(buf[:]) +} + +func parseSerializationHeader(data []byte) (serializationHeader, error) { + if len(data) < serializedCSHeaderLen { + return serializationHeader{}, io.ErrUnexpectedEOF + } + return serializationHeader{ + totalLen: binary.LittleEndian.Uint64(data[:8]), + major: binary.LittleEndian.Uint64(data[8:16]), + minor: binary.LittleEndian.Uint64(data[16:24]), + patch: binary.LittleEndian.Uint64(data[24:32]), + }, nil +} + +func (h serializationHeader) validate() error { + if h.major != 0 || h.minor < 10 { + return fmt.Errorf("unsupported gnark version %d.%d.%d", h.major, h.minor, h.patch) + } + return nil +} + +func serializedCSPayloadLenFits(payloadLen uint64) bool { + maxInt := uint64(int(^uint(0) >> 1)) + return payloadLen <= maxInt-serializedCSHeaderLen } diff --git a/constraint/babybear/r1cs_test.go b/constraint/babybear/r1cs_test.go index 17d987d0ce..c04e5cb9c3 100644 --- a/constraint/babybear/r1cs_test.go +++ b/constraint/babybear/r1cs_test.go @@ -7,6 +7,8 @@ package cs_test import ( "bytes" + "os" + "path/filepath" "reflect" "testing" @@ -64,8 +66,9 @@ func TestSerialization(t *testing.T) { if err != nil { t.Fatal(err) } + serialized := append([]byte(nil), buffer.Bytes()...) var reconstructed cs.R1CS - read, err = reconstructed.ReadFrom(&buffer) + read, err = reconstructed.ReadFrom(bytes.NewReader(serialized)) if err != nil { t.Fatal(err) } @@ -73,6 +76,19 @@ func TestSerialization(t *testing.T) { t.Fatal("didn't read same number of bytes we wrote") } + path := filepath.Join(t.TempDir(), "r1cs.bin") + if err := os.WriteFile(path, serialized, 0600); err != nil { + t.Fatal(err) + } + var reconstructedFromFile cs.R1CS + read, err = reconstructedFromFile.ReadFromFile(path) + if err != nil { + t.Fatal(err) + } + if written != read { + t.Fatal("didn't read same number of bytes from file") + } + // compare original and reconstructed // Some blueprints have unexported fields that are not serialized // (e.g., sync.Mutex, lazy-initialized caches). Use Equal method if available. @@ -94,6 +110,18 @@ func TestSerialization(t *testing.T) { "System.bitLen")); diff != "" { t.Fatalf("round trip mismatch (-want +got):\n%s", diff) } + if diff := cmp.Diff(r1cs1, &reconstructedFromFile, + blueprintComparer, + cmpopts.IgnoreFields(cs.R1CS{}, + "System.q", + "field", + "CoeffTable.mCoeffs", + "System.lbWireLevel", + "System.genericHint", + "System.SymbolTable", + "System.bitLen")); diff != "" { + t.Fatalf("file round trip mismatch (-want +got):\n%s", diff) + } } // ensure determinism in compilation / serialization / reconstruction diff --git a/constraint/bls12-377/coeff.go b/constraint/bls12-377/coeff.go index fed515058f..b33d2e86dc 100644 --- a/constraint/bls12-377/coeff.go +++ b/constraint/bls12-377/coeff.go @@ -59,13 +59,14 @@ func (ct *CoeffTable) fromBytes(buf []byte) error { ctLen := binary.LittleEndian.Uint64(buf[:8]) buf = buf[8:] - if uint64(len(buf)) < ctLen*fr.Bytes { + if ctLen > uint64(len(buf)/fr.Bytes) { return errors.New("invalid buffer size") } - ct.Coefficients = make([]fr.Element, ctLen) - for i := uint64(0); i < ctLen; i++ { + ctLenInt := int(ctLen) + ct.Coefficients = make([]fr.Element, ctLenInt) + for i := 0; i < ctLenInt; i++ { var c fr.Element - k := int(i) * fr.Bytes + k := i * fr.Bytes for j := 0; j < fr.Limbs; j++ { c[j] = binary.LittleEndian.Uint64(buf[k+j*8 : k+(j+1)*8]) } diff --git a/constraint/bls12-377/marshal.go b/constraint/bls12-377/marshal.go index 68bbdb092f..f51a5c1841 100644 --- a/constraint/bls12-377/marshal.go +++ b/constraint/bls12-377/marshal.go @@ -9,10 +9,14 @@ import ( "encoding/binary" "fmt" "io" + "os" "github.com/blang/semver/v4" + "github.com/consensys/gnark/internal/backend/ioutils/mmap" ) +const serializedCSHeaderLen = 4 * 8 + // WriteTo encodes R1CS into provided io.Writer using cbor func (cs *system) WriteTo(w io.Writer) (int64, error) { b, err := cs.System.ToBytes() @@ -52,39 +56,118 @@ func (cs *system) WriteTo(w io.Writer) (int64, error) { // ReadFrom attempts to decode R1CS from io.Reader using cbor func (cs *system) ReadFrom(r io.Reader) (int64, error) { - var totalLen uint64 - if err := binary.Read(r, binary.LittleEndian, &totalLen); err != nil { + h, err := readSerializationHeader(r) + if err != nil { return 0, err } - - var major, minor, patch uint64 - if err := binary.Read(r, binary.LittleEndian, &major); err != nil { + if err := h.validate(); err != nil { return 0, err } - if err := binary.Read(r, binary.LittleEndian, &minor); err != nil { - return 0, err + if !serializedCSPayloadLenFits(h.totalLen) { + return 0, fmt.Errorf("serialized constraint system is too large: %d bytes", h.totalLen) } - if err := binary.Read(r, binary.LittleEndian, &patch); err != nil { + + data := make([]byte, h.totalLen) + if _, err := io.ReadFull(r, data); err != nil { return 0, err } - // TODO @gbotrel validate version, duplicate logic with core.go CheckSerializationHeader - if major != 0 || minor < 10 { - return 0, fmt.Errorf("unsupported gnark version %d.%d.%d", major, minor, patch) + return cs.readPayload(data, h.totalLen) +} + +// ReadFromFile decodes a constraint system from path. +// +// On platforms that support mmap, ReadFromFile decodes directly from mapped +// file pages and avoids allocating a temporary copy of the serialized payload. +// It falls back to ReadFrom on unsupported platforms. +func (cs *system) ReadFromFile(path string) (n int64, err error) { + if !mmap.Supported() { + f, err := os.Open(path) + if err != nil { + return 0, err + } + defer f.Close() + return cs.ReadFrom(f) } - data := make([]byte, totalLen) - if _, err := io.ReadFull(r, data); err != nil { + mapping, err := mmap.Open(path) + if err != nil { + return 0, err + } + defer func() { + if closeErr := mapping.Close(); err == nil { + err = closeErr + } + }() + + return cs.readFromBytes(mapping.Data) +} + +func (cs *system) readFromBytes(data []byte) (int64, error) { + h, err := parseSerializationHeader(data) + if err != nil { + return 0, err + } + if err := h.validate(); err != nil { return 0, err } + if h.totalLen > uint64(len(data)-serializedCSHeaderLen) { + return 0, io.ErrUnexpectedEOF + } + return cs.readPayload(data[serializedCSHeaderLen:serializedCSHeaderLen+int(h.totalLen)], h.totalLen) +} + +func (cs *system) readPayload(data []byte, totalLen uint64) (int64, error) { n, err := cs.System.FromBytes(data) if err != nil { return 0, err } + if n > len(data) { + return 0, io.ErrUnexpectedEOF + } data = data[n:] if err := cs.CoeffTable.fromBytes(data); err != nil { return 0, err } - return int64(totalLen) + 4*8, nil + return int64(totalLen) + serializedCSHeaderLen, nil +} + +type serializationHeader struct { + totalLen uint64 + major uint64 + minor uint64 + patch uint64 +} + +func readSerializationHeader(r io.Reader) (serializationHeader, error) { + var buf [serializedCSHeaderLen]byte + if _, err := io.ReadFull(r, buf[:]); err != nil { + return serializationHeader{}, err + } + return parseSerializationHeader(buf[:]) +} + +func parseSerializationHeader(data []byte) (serializationHeader, error) { + if len(data) < serializedCSHeaderLen { + return serializationHeader{}, io.ErrUnexpectedEOF + } + return serializationHeader{ + totalLen: binary.LittleEndian.Uint64(data[:8]), + major: binary.LittleEndian.Uint64(data[8:16]), + minor: binary.LittleEndian.Uint64(data[16:24]), + patch: binary.LittleEndian.Uint64(data[24:32]), + }, nil +} + +func (h serializationHeader) validate() error { + if h.major != 0 || h.minor < 10 { + return fmt.Errorf("unsupported gnark version %d.%d.%d", h.major, h.minor, h.patch) + } + return nil +} + +func serializedCSPayloadLenFits(payloadLen uint64) bool { + maxInt := uint64(int(^uint(0) >> 1)) + return payloadLen <= maxInt-serializedCSHeaderLen } diff --git a/constraint/bls12-377/r1cs_test.go b/constraint/bls12-377/r1cs_test.go index 9d95eb5920..3f5e4b23f9 100644 --- a/constraint/bls12-377/r1cs_test.go +++ b/constraint/bls12-377/r1cs_test.go @@ -7,6 +7,8 @@ package cs_test import ( "bytes" + "os" + "path/filepath" "reflect" "testing" @@ -56,8 +58,9 @@ func TestSerialization(t *testing.T) { if err != nil { t.Fatal(err) } + serialized := append([]byte(nil), buffer.Bytes()...) var reconstructed cs.R1CS - read, err = reconstructed.ReadFrom(&buffer) + read, err = reconstructed.ReadFrom(bytes.NewReader(serialized)) if err != nil { t.Fatal(err) } @@ -65,6 +68,19 @@ func TestSerialization(t *testing.T) { t.Fatal("didn't read same number of bytes we wrote") } + path := filepath.Join(t.TempDir(), "r1cs.bin") + if err := os.WriteFile(path, serialized, 0600); err != nil { + t.Fatal(err) + } + var reconstructedFromFile cs.R1CS + read, err = reconstructedFromFile.ReadFromFile(path) + if err != nil { + t.Fatal(err) + } + if written != read { + t.Fatal("didn't read same number of bytes from file") + } + // compare original and reconstructed // Some blueprints have unexported fields that are not serialized // (e.g., sync.Mutex, lazy-initialized caches). Use Equal method if available. @@ -86,6 +102,18 @@ func TestSerialization(t *testing.T) { "System.bitLen")); diff != "" { t.Fatalf("round trip mismatch (-want +got):\n%s", diff) } + if diff := cmp.Diff(r1cs1, &reconstructedFromFile, + blueprintComparer, + cmpopts.IgnoreFields(cs.R1CS{}, + "System.q", + "field", + "CoeffTable.mCoeffs", + "System.lbWireLevel", + "System.genericHint", + "System.SymbolTable", + "System.bitLen")); diff != "" { + t.Fatalf("file round trip mismatch (-want +got):\n%s", diff) + } } // ensure determinism in compilation / serialization / reconstruction diff --git a/constraint/bls12-381/coeff.go b/constraint/bls12-381/coeff.go index bcab2dc414..775b2c469f 100644 --- a/constraint/bls12-381/coeff.go +++ b/constraint/bls12-381/coeff.go @@ -59,13 +59,14 @@ func (ct *CoeffTable) fromBytes(buf []byte) error { ctLen := binary.LittleEndian.Uint64(buf[:8]) buf = buf[8:] - if uint64(len(buf)) < ctLen*fr.Bytes { + if ctLen > uint64(len(buf)/fr.Bytes) { return errors.New("invalid buffer size") } - ct.Coefficients = make([]fr.Element, ctLen) - for i := uint64(0); i < ctLen; i++ { + ctLenInt := int(ctLen) + ct.Coefficients = make([]fr.Element, ctLenInt) + for i := 0; i < ctLenInt; i++ { var c fr.Element - k := int(i) * fr.Bytes + k := i * fr.Bytes for j := 0; j < fr.Limbs; j++ { c[j] = binary.LittleEndian.Uint64(buf[k+j*8 : k+(j+1)*8]) } diff --git a/constraint/bls12-381/marshal.go b/constraint/bls12-381/marshal.go index 68bbdb092f..f51a5c1841 100644 --- a/constraint/bls12-381/marshal.go +++ b/constraint/bls12-381/marshal.go @@ -9,10 +9,14 @@ import ( "encoding/binary" "fmt" "io" + "os" "github.com/blang/semver/v4" + "github.com/consensys/gnark/internal/backend/ioutils/mmap" ) +const serializedCSHeaderLen = 4 * 8 + // WriteTo encodes R1CS into provided io.Writer using cbor func (cs *system) WriteTo(w io.Writer) (int64, error) { b, err := cs.System.ToBytes() @@ -52,39 +56,118 @@ func (cs *system) WriteTo(w io.Writer) (int64, error) { // ReadFrom attempts to decode R1CS from io.Reader using cbor func (cs *system) ReadFrom(r io.Reader) (int64, error) { - var totalLen uint64 - if err := binary.Read(r, binary.LittleEndian, &totalLen); err != nil { + h, err := readSerializationHeader(r) + if err != nil { return 0, err } - - var major, minor, patch uint64 - if err := binary.Read(r, binary.LittleEndian, &major); err != nil { + if err := h.validate(); err != nil { return 0, err } - if err := binary.Read(r, binary.LittleEndian, &minor); err != nil { - return 0, err + if !serializedCSPayloadLenFits(h.totalLen) { + return 0, fmt.Errorf("serialized constraint system is too large: %d bytes", h.totalLen) } - if err := binary.Read(r, binary.LittleEndian, &patch); err != nil { + + data := make([]byte, h.totalLen) + if _, err := io.ReadFull(r, data); err != nil { return 0, err } - // TODO @gbotrel validate version, duplicate logic with core.go CheckSerializationHeader - if major != 0 || minor < 10 { - return 0, fmt.Errorf("unsupported gnark version %d.%d.%d", major, minor, patch) + return cs.readPayload(data, h.totalLen) +} + +// ReadFromFile decodes a constraint system from path. +// +// On platforms that support mmap, ReadFromFile decodes directly from mapped +// file pages and avoids allocating a temporary copy of the serialized payload. +// It falls back to ReadFrom on unsupported platforms. +func (cs *system) ReadFromFile(path string) (n int64, err error) { + if !mmap.Supported() { + f, err := os.Open(path) + if err != nil { + return 0, err + } + defer f.Close() + return cs.ReadFrom(f) } - data := make([]byte, totalLen) - if _, err := io.ReadFull(r, data); err != nil { + mapping, err := mmap.Open(path) + if err != nil { + return 0, err + } + defer func() { + if closeErr := mapping.Close(); err == nil { + err = closeErr + } + }() + + return cs.readFromBytes(mapping.Data) +} + +func (cs *system) readFromBytes(data []byte) (int64, error) { + h, err := parseSerializationHeader(data) + if err != nil { + return 0, err + } + if err := h.validate(); err != nil { return 0, err } + if h.totalLen > uint64(len(data)-serializedCSHeaderLen) { + return 0, io.ErrUnexpectedEOF + } + return cs.readPayload(data[serializedCSHeaderLen:serializedCSHeaderLen+int(h.totalLen)], h.totalLen) +} + +func (cs *system) readPayload(data []byte, totalLen uint64) (int64, error) { n, err := cs.System.FromBytes(data) if err != nil { return 0, err } + if n > len(data) { + return 0, io.ErrUnexpectedEOF + } data = data[n:] if err := cs.CoeffTable.fromBytes(data); err != nil { return 0, err } - return int64(totalLen) + 4*8, nil + return int64(totalLen) + serializedCSHeaderLen, nil +} + +type serializationHeader struct { + totalLen uint64 + major uint64 + minor uint64 + patch uint64 +} + +func readSerializationHeader(r io.Reader) (serializationHeader, error) { + var buf [serializedCSHeaderLen]byte + if _, err := io.ReadFull(r, buf[:]); err != nil { + return serializationHeader{}, err + } + return parseSerializationHeader(buf[:]) +} + +func parseSerializationHeader(data []byte) (serializationHeader, error) { + if len(data) < serializedCSHeaderLen { + return serializationHeader{}, io.ErrUnexpectedEOF + } + return serializationHeader{ + totalLen: binary.LittleEndian.Uint64(data[:8]), + major: binary.LittleEndian.Uint64(data[8:16]), + minor: binary.LittleEndian.Uint64(data[16:24]), + patch: binary.LittleEndian.Uint64(data[24:32]), + }, nil +} + +func (h serializationHeader) validate() error { + if h.major != 0 || h.minor < 10 { + return fmt.Errorf("unsupported gnark version %d.%d.%d", h.major, h.minor, h.patch) + } + return nil +} + +func serializedCSPayloadLenFits(payloadLen uint64) bool { + maxInt := uint64(int(^uint(0) >> 1)) + return payloadLen <= maxInt-serializedCSHeaderLen } diff --git a/constraint/bls12-381/r1cs_test.go b/constraint/bls12-381/r1cs_test.go index d8bbf1b57e..5b9eb59de6 100644 --- a/constraint/bls12-381/r1cs_test.go +++ b/constraint/bls12-381/r1cs_test.go @@ -7,6 +7,8 @@ package cs_test import ( "bytes" + "os" + "path/filepath" "reflect" "testing" @@ -56,8 +58,9 @@ func TestSerialization(t *testing.T) { if err != nil { t.Fatal(err) } + serialized := append([]byte(nil), buffer.Bytes()...) var reconstructed cs.R1CS - read, err = reconstructed.ReadFrom(&buffer) + read, err = reconstructed.ReadFrom(bytes.NewReader(serialized)) if err != nil { t.Fatal(err) } @@ -65,6 +68,19 @@ func TestSerialization(t *testing.T) { t.Fatal("didn't read same number of bytes we wrote") } + path := filepath.Join(t.TempDir(), "r1cs.bin") + if err := os.WriteFile(path, serialized, 0600); err != nil { + t.Fatal(err) + } + var reconstructedFromFile cs.R1CS + read, err = reconstructedFromFile.ReadFromFile(path) + if err != nil { + t.Fatal(err) + } + if written != read { + t.Fatal("didn't read same number of bytes from file") + } + // compare original and reconstructed // Some blueprints have unexported fields that are not serialized // (e.g., sync.Mutex, lazy-initialized caches). Use Equal method if available. @@ -86,6 +102,18 @@ func TestSerialization(t *testing.T) { "System.bitLen")); diff != "" { t.Fatalf("round trip mismatch (-want +got):\n%s", diff) } + if diff := cmp.Diff(r1cs1, &reconstructedFromFile, + blueprintComparer, + cmpopts.IgnoreFields(cs.R1CS{}, + "System.q", + "field", + "CoeffTable.mCoeffs", + "System.lbWireLevel", + "System.genericHint", + "System.SymbolTable", + "System.bitLen")); diff != "" { + t.Fatalf("file round trip mismatch (-want +got):\n%s", diff) + } } // ensure determinism in compilation / serialization / reconstruction diff --git a/constraint/bn254/coeff.go b/constraint/bn254/coeff.go index eaaaafd57d..1b6a57eb44 100644 --- a/constraint/bn254/coeff.go +++ b/constraint/bn254/coeff.go @@ -59,13 +59,14 @@ func (ct *CoeffTable) fromBytes(buf []byte) error { ctLen := binary.LittleEndian.Uint64(buf[:8]) buf = buf[8:] - if uint64(len(buf)) < ctLen*fr.Bytes { + if ctLen > uint64(len(buf)/fr.Bytes) { return errors.New("invalid buffer size") } - ct.Coefficients = make([]fr.Element, ctLen) - for i := uint64(0); i < ctLen; i++ { + ctLenInt := int(ctLen) + ct.Coefficients = make([]fr.Element, ctLenInt) + for i := 0; i < ctLenInt; i++ { var c fr.Element - k := int(i) * fr.Bytes + k := i * fr.Bytes for j := 0; j < fr.Limbs; j++ { c[j] = binary.LittleEndian.Uint64(buf[k+j*8 : k+(j+1)*8]) } diff --git a/constraint/bn254/coeff_test.go b/constraint/bn254/coeff_test.go new file mode 100644 index 0000000000..ffe5f578e1 --- /dev/null +++ b/constraint/bn254/coeff_test.go @@ -0,0 +1,16 @@ +package cs + +import ( + "encoding/binary" + "testing" +) + +func TestCoeffTableFromBytesRejectsMalformedLength(t *testing.T) { + buf := make([]byte, 8) + binary.LittleEndian.PutUint64(buf, ^uint64(0)) + + var ct CoeffTable + if err := ct.fromBytes(buf); err == nil { + t.Fatal("expected error") + } +} diff --git a/constraint/bn254/marshal.go b/constraint/bn254/marshal.go index 68bbdb092f..f51a5c1841 100644 --- a/constraint/bn254/marshal.go +++ b/constraint/bn254/marshal.go @@ -9,10 +9,14 @@ import ( "encoding/binary" "fmt" "io" + "os" "github.com/blang/semver/v4" + "github.com/consensys/gnark/internal/backend/ioutils/mmap" ) +const serializedCSHeaderLen = 4 * 8 + // WriteTo encodes R1CS into provided io.Writer using cbor func (cs *system) WriteTo(w io.Writer) (int64, error) { b, err := cs.System.ToBytes() @@ -52,39 +56,118 @@ func (cs *system) WriteTo(w io.Writer) (int64, error) { // ReadFrom attempts to decode R1CS from io.Reader using cbor func (cs *system) ReadFrom(r io.Reader) (int64, error) { - var totalLen uint64 - if err := binary.Read(r, binary.LittleEndian, &totalLen); err != nil { + h, err := readSerializationHeader(r) + if err != nil { return 0, err } - - var major, minor, patch uint64 - if err := binary.Read(r, binary.LittleEndian, &major); err != nil { + if err := h.validate(); err != nil { return 0, err } - if err := binary.Read(r, binary.LittleEndian, &minor); err != nil { - return 0, err + if !serializedCSPayloadLenFits(h.totalLen) { + return 0, fmt.Errorf("serialized constraint system is too large: %d bytes", h.totalLen) } - if err := binary.Read(r, binary.LittleEndian, &patch); err != nil { + + data := make([]byte, h.totalLen) + if _, err := io.ReadFull(r, data); err != nil { return 0, err } - // TODO @gbotrel validate version, duplicate logic with core.go CheckSerializationHeader - if major != 0 || minor < 10 { - return 0, fmt.Errorf("unsupported gnark version %d.%d.%d", major, minor, patch) + return cs.readPayload(data, h.totalLen) +} + +// ReadFromFile decodes a constraint system from path. +// +// On platforms that support mmap, ReadFromFile decodes directly from mapped +// file pages and avoids allocating a temporary copy of the serialized payload. +// It falls back to ReadFrom on unsupported platforms. +func (cs *system) ReadFromFile(path string) (n int64, err error) { + if !mmap.Supported() { + f, err := os.Open(path) + if err != nil { + return 0, err + } + defer f.Close() + return cs.ReadFrom(f) } - data := make([]byte, totalLen) - if _, err := io.ReadFull(r, data); err != nil { + mapping, err := mmap.Open(path) + if err != nil { + return 0, err + } + defer func() { + if closeErr := mapping.Close(); err == nil { + err = closeErr + } + }() + + return cs.readFromBytes(mapping.Data) +} + +func (cs *system) readFromBytes(data []byte) (int64, error) { + h, err := parseSerializationHeader(data) + if err != nil { + return 0, err + } + if err := h.validate(); err != nil { return 0, err } + if h.totalLen > uint64(len(data)-serializedCSHeaderLen) { + return 0, io.ErrUnexpectedEOF + } + return cs.readPayload(data[serializedCSHeaderLen:serializedCSHeaderLen+int(h.totalLen)], h.totalLen) +} + +func (cs *system) readPayload(data []byte, totalLen uint64) (int64, error) { n, err := cs.System.FromBytes(data) if err != nil { return 0, err } + if n > len(data) { + return 0, io.ErrUnexpectedEOF + } data = data[n:] if err := cs.CoeffTable.fromBytes(data); err != nil { return 0, err } - return int64(totalLen) + 4*8, nil + return int64(totalLen) + serializedCSHeaderLen, nil +} + +type serializationHeader struct { + totalLen uint64 + major uint64 + minor uint64 + patch uint64 +} + +func readSerializationHeader(r io.Reader) (serializationHeader, error) { + var buf [serializedCSHeaderLen]byte + if _, err := io.ReadFull(r, buf[:]); err != nil { + return serializationHeader{}, err + } + return parseSerializationHeader(buf[:]) +} + +func parseSerializationHeader(data []byte) (serializationHeader, error) { + if len(data) < serializedCSHeaderLen { + return serializationHeader{}, io.ErrUnexpectedEOF + } + return serializationHeader{ + totalLen: binary.LittleEndian.Uint64(data[:8]), + major: binary.LittleEndian.Uint64(data[8:16]), + minor: binary.LittleEndian.Uint64(data[16:24]), + patch: binary.LittleEndian.Uint64(data[24:32]), + }, nil +} + +func (h serializationHeader) validate() error { + if h.major != 0 || h.minor < 10 { + return fmt.Errorf("unsupported gnark version %d.%d.%d", h.major, h.minor, h.patch) + } + return nil +} + +func serializedCSPayloadLenFits(payloadLen uint64) bool { + maxInt := uint64(int(^uint(0) >> 1)) + return payloadLen <= maxInt-serializedCSHeaderLen } diff --git a/constraint/bn254/r1cs_test.go b/constraint/bn254/r1cs_test.go index 748b8cc24f..6680fbb181 100644 --- a/constraint/bn254/r1cs_test.go +++ b/constraint/bn254/r1cs_test.go @@ -7,6 +7,8 @@ package cs_test import ( "bytes" + "os" + "path/filepath" "reflect" "testing" @@ -56,8 +58,9 @@ func TestSerialization(t *testing.T) { if err != nil { t.Fatal(err) } + serialized := append([]byte(nil), buffer.Bytes()...) var reconstructed cs.R1CS - read, err = reconstructed.ReadFrom(&buffer) + read, err = reconstructed.ReadFrom(bytes.NewReader(serialized)) if err != nil { t.Fatal(err) } @@ -65,6 +68,19 @@ func TestSerialization(t *testing.T) { t.Fatal("didn't read same number of bytes we wrote") } + path := filepath.Join(t.TempDir(), "r1cs.bin") + if err := os.WriteFile(path, serialized, 0600); err != nil { + t.Fatal(err) + } + var reconstructedFromFile cs.R1CS + read, err = reconstructedFromFile.ReadFromFile(path) + if err != nil { + t.Fatal(err) + } + if written != read { + t.Fatal("didn't read same number of bytes from file") + } + // compare original and reconstructed // Some blueprints have unexported fields that are not serialized // (e.g., sync.Mutex, lazy-initialized caches). Use Equal method if available. @@ -86,6 +102,18 @@ func TestSerialization(t *testing.T) { "System.bitLen")); diff != "" { t.Fatalf("round trip mismatch (-want +got):\n%s", diff) } + if diff := cmp.Diff(r1cs1, &reconstructedFromFile, + blueprintComparer, + cmpopts.IgnoreFields(cs.R1CS{}, + "System.q", + "field", + "CoeffTable.mCoeffs", + "System.lbWireLevel", + "System.genericHint", + "System.SymbolTable", + "System.bitLen")); diff != "" { + t.Fatalf("file round trip mismatch (-want +got):\n%s", diff) + } } // ensure determinism in compilation / serialization / reconstruction diff --git a/constraint/bw6-761/coeff.go b/constraint/bw6-761/coeff.go index f964ebd533..7b772dc3df 100644 --- a/constraint/bw6-761/coeff.go +++ b/constraint/bw6-761/coeff.go @@ -59,13 +59,14 @@ func (ct *CoeffTable) fromBytes(buf []byte) error { ctLen := binary.LittleEndian.Uint64(buf[:8]) buf = buf[8:] - if uint64(len(buf)) < ctLen*fr.Bytes { + if ctLen > uint64(len(buf)/fr.Bytes) { return errors.New("invalid buffer size") } - ct.Coefficients = make([]fr.Element, ctLen) - for i := uint64(0); i < ctLen; i++ { + ctLenInt := int(ctLen) + ct.Coefficients = make([]fr.Element, ctLenInt) + for i := 0; i < ctLenInt; i++ { var c fr.Element - k := int(i) * fr.Bytes + k := i * fr.Bytes for j := 0; j < fr.Limbs; j++ { c[j] = binary.LittleEndian.Uint64(buf[k+j*8 : k+(j+1)*8]) } diff --git a/constraint/bw6-761/marshal.go b/constraint/bw6-761/marshal.go index 68bbdb092f..f51a5c1841 100644 --- a/constraint/bw6-761/marshal.go +++ b/constraint/bw6-761/marshal.go @@ -9,10 +9,14 @@ import ( "encoding/binary" "fmt" "io" + "os" "github.com/blang/semver/v4" + "github.com/consensys/gnark/internal/backend/ioutils/mmap" ) +const serializedCSHeaderLen = 4 * 8 + // WriteTo encodes R1CS into provided io.Writer using cbor func (cs *system) WriteTo(w io.Writer) (int64, error) { b, err := cs.System.ToBytes() @@ -52,39 +56,118 @@ func (cs *system) WriteTo(w io.Writer) (int64, error) { // ReadFrom attempts to decode R1CS from io.Reader using cbor func (cs *system) ReadFrom(r io.Reader) (int64, error) { - var totalLen uint64 - if err := binary.Read(r, binary.LittleEndian, &totalLen); err != nil { + h, err := readSerializationHeader(r) + if err != nil { return 0, err } - - var major, minor, patch uint64 - if err := binary.Read(r, binary.LittleEndian, &major); err != nil { + if err := h.validate(); err != nil { return 0, err } - if err := binary.Read(r, binary.LittleEndian, &minor); err != nil { - return 0, err + if !serializedCSPayloadLenFits(h.totalLen) { + return 0, fmt.Errorf("serialized constraint system is too large: %d bytes", h.totalLen) } - if err := binary.Read(r, binary.LittleEndian, &patch); err != nil { + + data := make([]byte, h.totalLen) + if _, err := io.ReadFull(r, data); err != nil { return 0, err } - // TODO @gbotrel validate version, duplicate logic with core.go CheckSerializationHeader - if major != 0 || minor < 10 { - return 0, fmt.Errorf("unsupported gnark version %d.%d.%d", major, minor, patch) + return cs.readPayload(data, h.totalLen) +} + +// ReadFromFile decodes a constraint system from path. +// +// On platforms that support mmap, ReadFromFile decodes directly from mapped +// file pages and avoids allocating a temporary copy of the serialized payload. +// It falls back to ReadFrom on unsupported platforms. +func (cs *system) ReadFromFile(path string) (n int64, err error) { + if !mmap.Supported() { + f, err := os.Open(path) + if err != nil { + return 0, err + } + defer f.Close() + return cs.ReadFrom(f) } - data := make([]byte, totalLen) - if _, err := io.ReadFull(r, data); err != nil { + mapping, err := mmap.Open(path) + if err != nil { + return 0, err + } + defer func() { + if closeErr := mapping.Close(); err == nil { + err = closeErr + } + }() + + return cs.readFromBytes(mapping.Data) +} + +func (cs *system) readFromBytes(data []byte) (int64, error) { + h, err := parseSerializationHeader(data) + if err != nil { + return 0, err + } + if err := h.validate(); err != nil { return 0, err } + if h.totalLen > uint64(len(data)-serializedCSHeaderLen) { + return 0, io.ErrUnexpectedEOF + } + return cs.readPayload(data[serializedCSHeaderLen:serializedCSHeaderLen+int(h.totalLen)], h.totalLen) +} + +func (cs *system) readPayload(data []byte, totalLen uint64) (int64, error) { n, err := cs.System.FromBytes(data) if err != nil { return 0, err } + if n > len(data) { + return 0, io.ErrUnexpectedEOF + } data = data[n:] if err := cs.CoeffTable.fromBytes(data); err != nil { return 0, err } - return int64(totalLen) + 4*8, nil + return int64(totalLen) + serializedCSHeaderLen, nil +} + +type serializationHeader struct { + totalLen uint64 + major uint64 + minor uint64 + patch uint64 +} + +func readSerializationHeader(r io.Reader) (serializationHeader, error) { + var buf [serializedCSHeaderLen]byte + if _, err := io.ReadFull(r, buf[:]); err != nil { + return serializationHeader{}, err + } + return parseSerializationHeader(buf[:]) +} + +func parseSerializationHeader(data []byte) (serializationHeader, error) { + if len(data) < serializedCSHeaderLen { + return serializationHeader{}, io.ErrUnexpectedEOF + } + return serializationHeader{ + totalLen: binary.LittleEndian.Uint64(data[:8]), + major: binary.LittleEndian.Uint64(data[8:16]), + minor: binary.LittleEndian.Uint64(data[16:24]), + patch: binary.LittleEndian.Uint64(data[24:32]), + }, nil +} + +func (h serializationHeader) validate() error { + if h.major != 0 || h.minor < 10 { + return fmt.Errorf("unsupported gnark version %d.%d.%d", h.major, h.minor, h.patch) + } + return nil +} + +func serializedCSPayloadLenFits(payloadLen uint64) bool { + maxInt := uint64(int(^uint(0) >> 1)) + return payloadLen <= maxInt-serializedCSHeaderLen } diff --git a/constraint/bw6-761/r1cs_test.go b/constraint/bw6-761/r1cs_test.go index ec86db39fb..b8d6751c6d 100644 --- a/constraint/bw6-761/r1cs_test.go +++ b/constraint/bw6-761/r1cs_test.go @@ -7,6 +7,8 @@ package cs_test import ( "bytes" + "os" + "path/filepath" "reflect" "testing" @@ -59,8 +61,9 @@ func TestSerialization(t *testing.T) { if err != nil { t.Fatal(err) } + serialized := append([]byte(nil), buffer.Bytes()...) var reconstructed cs.R1CS - read, err = reconstructed.ReadFrom(&buffer) + read, err = reconstructed.ReadFrom(bytes.NewReader(serialized)) if err != nil { t.Fatal(err) } @@ -68,6 +71,19 @@ func TestSerialization(t *testing.T) { t.Fatal("didn't read same number of bytes we wrote") } + path := filepath.Join(t.TempDir(), "r1cs.bin") + if err := os.WriteFile(path, serialized, 0600); err != nil { + t.Fatal(err) + } + var reconstructedFromFile cs.R1CS + read, err = reconstructedFromFile.ReadFromFile(path) + if err != nil { + t.Fatal(err) + } + if written != read { + t.Fatal("didn't read same number of bytes from file") + } + // compare original and reconstructed // Some blueprints have unexported fields that are not serialized // (e.g., sync.Mutex, lazy-initialized caches). Use Equal method if available. @@ -89,6 +105,18 @@ func TestSerialization(t *testing.T) { "System.bitLen")); diff != "" { t.Fatalf("round trip mismatch (-want +got):\n%s", diff) } + if diff := cmp.Diff(r1cs1, &reconstructedFromFile, + blueprintComparer, + cmpopts.IgnoreFields(cs.R1CS{}, + "System.q", + "field", + "CoeffTable.mCoeffs", + "System.lbWireLevel", + "System.genericHint", + "System.SymbolTable", + "System.bitLen")); diff != "" { + t.Fatalf("file round trip mismatch (-want +got):\n%s", diff) + } } // ensure determinism in compilation / serialization / reconstruction diff --git a/constraint/grumpkin/coeff.go b/constraint/grumpkin/coeff.go index 5f96aa8b0e..afd5e2f987 100644 --- a/constraint/grumpkin/coeff.go +++ b/constraint/grumpkin/coeff.go @@ -59,13 +59,14 @@ func (ct *CoeffTable) fromBytes(buf []byte) error { ctLen := binary.LittleEndian.Uint64(buf[:8]) buf = buf[8:] - if uint64(len(buf)) < ctLen*fr.Bytes { + if ctLen > uint64(len(buf)/fr.Bytes) { return errors.New("invalid buffer size") } - ct.Coefficients = make([]fr.Element, ctLen) - for i := uint64(0); i < ctLen; i++ { + ctLenInt := int(ctLen) + ct.Coefficients = make([]fr.Element, ctLenInt) + for i := 0; i < ctLenInt; i++ { var c fr.Element - k := int(i) * fr.Bytes + k := i * fr.Bytes for j := 0; j < fr.Limbs; j++ { c[j] = binary.LittleEndian.Uint64(buf[k+j*8 : k+(j+1)*8]) } diff --git a/constraint/grumpkin/marshal.go b/constraint/grumpkin/marshal.go index 68bbdb092f..f51a5c1841 100644 --- a/constraint/grumpkin/marshal.go +++ b/constraint/grumpkin/marshal.go @@ -9,10 +9,14 @@ import ( "encoding/binary" "fmt" "io" + "os" "github.com/blang/semver/v4" + "github.com/consensys/gnark/internal/backend/ioutils/mmap" ) +const serializedCSHeaderLen = 4 * 8 + // WriteTo encodes R1CS into provided io.Writer using cbor func (cs *system) WriteTo(w io.Writer) (int64, error) { b, err := cs.System.ToBytes() @@ -52,39 +56,118 @@ func (cs *system) WriteTo(w io.Writer) (int64, error) { // ReadFrom attempts to decode R1CS from io.Reader using cbor func (cs *system) ReadFrom(r io.Reader) (int64, error) { - var totalLen uint64 - if err := binary.Read(r, binary.LittleEndian, &totalLen); err != nil { + h, err := readSerializationHeader(r) + if err != nil { return 0, err } - - var major, minor, patch uint64 - if err := binary.Read(r, binary.LittleEndian, &major); err != nil { + if err := h.validate(); err != nil { return 0, err } - if err := binary.Read(r, binary.LittleEndian, &minor); err != nil { - return 0, err + if !serializedCSPayloadLenFits(h.totalLen) { + return 0, fmt.Errorf("serialized constraint system is too large: %d bytes", h.totalLen) } - if err := binary.Read(r, binary.LittleEndian, &patch); err != nil { + + data := make([]byte, h.totalLen) + if _, err := io.ReadFull(r, data); err != nil { return 0, err } - // TODO @gbotrel validate version, duplicate logic with core.go CheckSerializationHeader - if major != 0 || minor < 10 { - return 0, fmt.Errorf("unsupported gnark version %d.%d.%d", major, minor, patch) + return cs.readPayload(data, h.totalLen) +} + +// ReadFromFile decodes a constraint system from path. +// +// On platforms that support mmap, ReadFromFile decodes directly from mapped +// file pages and avoids allocating a temporary copy of the serialized payload. +// It falls back to ReadFrom on unsupported platforms. +func (cs *system) ReadFromFile(path string) (n int64, err error) { + if !mmap.Supported() { + f, err := os.Open(path) + if err != nil { + return 0, err + } + defer f.Close() + return cs.ReadFrom(f) } - data := make([]byte, totalLen) - if _, err := io.ReadFull(r, data); err != nil { + mapping, err := mmap.Open(path) + if err != nil { + return 0, err + } + defer func() { + if closeErr := mapping.Close(); err == nil { + err = closeErr + } + }() + + return cs.readFromBytes(mapping.Data) +} + +func (cs *system) readFromBytes(data []byte) (int64, error) { + h, err := parseSerializationHeader(data) + if err != nil { + return 0, err + } + if err := h.validate(); err != nil { return 0, err } + if h.totalLen > uint64(len(data)-serializedCSHeaderLen) { + return 0, io.ErrUnexpectedEOF + } + return cs.readPayload(data[serializedCSHeaderLen:serializedCSHeaderLen+int(h.totalLen)], h.totalLen) +} + +func (cs *system) readPayload(data []byte, totalLen uint64) (int64, error) { n, err := cs.System.FromBytes(data) if err != nil { return 0, err } + if n > len(data) { + return 0, io.ErrUnexpectedEOF + } data = data[n:] if err := cs.CoeffTable.fromBytes(data); err != nil { return 0, err } - return int64(totalLen) + 4*8, nil + return int64(totalLen) + serializedCSHeaderLen, nil +} + +type serializationHeader struct { + totalLen uint64 + major uint64 + minor uint64 + patch uint64 +} + +func readSerializationHeader(r io.Reader) (serializationHeader, error) { + var buf [serializedCSHeaderLen]byte + if _, err := io.ReadFull(r, buf[:]); err != nil { + return serializationHeader{}, err + } + return parseSerializationHeader(buf[:]) +} + +func parseSerializationHeader(data []byte) (serializationHeader, error) { + if len(data) < serializedCSHeaderLen { + return serializationHeader{}, io.ErrUnexpectedEOF + } + return serializationHeader{ + totalLen: binary.LittleEndian.Uint64(data[:8]), + major: binary.LittleEndian.Uint64(data[8:16]), + minor: binary.LittleEndian.Uint64(data[16:24]), + patch: binary.LittleEndian.Uint64(data[24:32]), + }, nil +} + +func (h serializationHeader) validate() error { + if h.major != 0 || h.minor < 10 { + return fmt.Errorf("unsupported gnark version %d.%d.%d", h.major, h.minor, h.patch) + } + return nil +} + +func serializedCSPayloadLenFits(payloadLen uint64) bool { + maxInt := uint64(int(^uint(0) >> 1)) + return payloadLen <= maxInt-serializedCSHeaderLen } diff --git a/constraint/koalabear/coeff.go b/constraint/koalabear/coeff.go index 650fa21c42..c7b9c87213 100644 --- a/constraint/koalabear/coeff.go +++ b/constraint/koalabear/coeff.go @@ -59,13 +59,14 @@ func (ct *CoeffTable) fromBytes(buf []byte) error { ctLen := binary.LittleEndian.Uint64(buf[:8]) buf = buf[8:] - if uint64(len(buf)) < ctLen*fr.Bytes { + if ctLen > uint64(len(buf)/fr.Bytes) { return errors.New("invalid buffer size") } - ct.Coefficients = make([]fr.Element, ctLen) - for i := uint64(0); i < ctLen; i++ { + ctLenInt := int(ctLen) + ct.Coefficients = make([]fr.Element, ctLenInt) + for i := 0; i < ctLenInt; i++ { var c fr.Element - k := int(i) * fr.Bytes + k := i * fr.Bytes for j := 0; j < fr.Limbs; j++ { c[j] = binary.LittleEndian.Uint32(buf[k+j*4 : k+(j+1)*4]) } diff --git a/constraint/koalabear/marshal.go b/constraint/koalabear/marshal.go index 68bbdb092f..f51a5c1841 100644 --- a/constraint/koalabear/marshal.go +++ b/constraint/koalabear/marshal.go @@ -9,10 +9,14 @@ import ( "encoding/binary" "fmt" "io" + "os" "github.com/blang/semver/v4" + "github.com/consensys/gnark/internal/backend/ioutils/mmap" ) +const serializedCSHeaderLen = 4 * 8 + // WriteTo encodes R1CS into provided io.Writer using cbor func (cs *system) WriteTo(w io.Writer) (int64, error) { b, err := cs.System.ToBytes() @@ -52,39 +56,118 @@ func (cs *system) WriteTo(w io.Writer) (int64, error) { // ReadFrom attempts to decode R1CS from io.Reader using cbor func (cs *system) ReadFrom(r io.Reader) (int64, error) { - var totalLen uint64 - if err := binary.Read(r, binary.LittleEndian, &totalLen); err != nil { + h, err := readSerializationHeader(r) + if err != nil { return 0, err } - - var major, minor, patch uint64 - if err := binary.Read(r, binary.LittleEndian, &major); err != nil { + if err := h.validate(); err != nil { return 0, err } - if err := binary.Read(r, binary.LittleEndian, &minor); err != nil { - return 0, err + if !serializedCSPayloadLenFits(h.totalLen) { + return 0, fmt.Errorf("serialized constraint system is too large: %d bytes", h.totalLen) } - if err := binary.Read(r, binary.LittleEndian, &patch); err != nil { + + data := make([]byte, h.totalLen) + if _, err := io.ReadFull(r, data); err != nil { return 0, err } - // TODO @gbotrel validate version, duplicate logic with core.go CheckSerializationHeader - if major != 0 || minor < 10 { - return 0, fmt.Errorf("unsupported gnark version %d.%d.%d", major, minor, patch) + return cs.readPayload(data, h.totalLen) +} + +// ReadFromFile decodes a constraint system from path. +// +// On platforms that support mmap, ReadFromFile decodes directly from mapped +// file pages and avoids allocating a temporary copy of the serialized payload. +// It falls back to ReadFrom on unsupported platforms. +func (cs *system) ReadFromFile(path string) (n int64, err error) { + if !mmap.Supported() { + f, err := os.Open(path) + if err != nil { + return 0, err + } + defer f.Close() + return cs.ReadFrom(f) } - data := make([]byte, totalLen) - if _, err := io.ReadFull(r, data); err != nil { + mapping, err := mmap.Open(path) + if err != nil { + return 0, err + } + defer func() { + if closeErr := mapping.Close(); err == nil { + err = closeErr + } + }() + + return cs.readFromBytes(mapping.Data) +} + +func (cs *system) readFromBytes(data []byte) (int64, error) { + h, err := parseSerializationHeader(data) + if err != nil { + return 0, err + } + if err := h.validate(); err != nil { return 0, err } + if h.totalLen > uint64(len(data)-serializedCSHeaderLen) { + return 0, io.ErrUnexpectedEOF + } + return cs.readPayload(data[serializedCSHeaderLen:serializedCSHeaderLen+int(h.totalLen)], h.totalLen) +} + +func (cs *system) readPayload(data []byte, totalLen uint64) (int64, error) { n, err := cs.System.FromBytes(data) if err != nil { return 0, err } + if n > len(data) { + return 0, io.ErrUnexpectedEOF + } data = data[n:] if err := cs.CoeffTable.fromBytes(data); err != nil { return 0, err } - return int64(totalLen) + 4*8, nil + return int64(totalLen) + serializedCSHeaderLen, nil +} + +type serializationHeader struct { + totalLen uint64 + major uint64 + minor uint64 + patch uint64 +} + +func readSerializationHeader(r io.Reader) (serializationHeader, error) { + var buf [serializedCSHeaderLen]byte + if _, err := io.ReadFull(r, buf[:]); err != nil { + return serializationHeader{}, err + } + return parseSerializationHeader(buf[:]) +} + +func parseSerializationHeader(data []byte) (serializationHeader, error) { + if len(data) < serializedCSHeaderLen { + return serializationHeader{}, io.ErrUnexpectedEOF + } + return serializationHeader{ + totalLen: binary.LittleEndian.Uint64(data[:8]), + major: binary.LittleEndian.Uint64(data[8:16]), + minor: binary.LittleEndian.Uint64(data[16:24]), + patch: binary.LittleEndian.Uint64(data[24:32]), + }, nil +} + +func (h serializationHeader) validate() error { + if h.major != 0 || h.minor < 10 { + return fmt.Errorf("unsupported gnark version %d.%d.%d", h.major, h.minor, h.patch) + } + return nil +} + +func serializedCSPayloadLenFits(payloadLen uint64) bool { + maxInt := uint64(int(^uint(0) >> 1)) + return payloadLen <= maxInt-serializedCSHeaderLen } diff --git a/constraint/koalabear/r1cs_test.go b/constraint/koalabear/r1cs_test.go index b66184fa29..32918f2348 100644 --- a/constraint/koalabear/r1cs_test.go +++ b/constraint/koalabear/r1cs_test.go @@ -7,6 +7,8 @@ package cs_test import ( "bytes" + "os" + "path/filepath" "reflect" "testing" @@ -64,8 +66,9 @@ func TestSerialization(t *testing.T) { if err != nil { t.Fatal(err) } + serialized := append([]byte(nil), buffer.Bytes()...) var reconstructed cs.R1CS - read, err = reconstructed.ReadFrom(&buffer) + read, err = reconstructed.ReadFrom(bytes.NewReader(serialized)) if err != nil { t.Fatal(err) } @@ -73,6 +76,19 @@ func TestSerialization(t *testing.T) { t.Fatal("didn't read same number of bytes we wrote") } + path := filepath.Join(t.TempDir(), "r1cs.bin") + if err := os.WriteFile(path, serialized, 0600); err != nil { + t.Fatal(err) + } + var reconstructedFromFile cs.R1CS + read, err = reconstructedFromFile.ReadFromFile(path) + if err != nil { + t.Fatal(err) + } + if written != read { + t.Fatal("didn't read same number of bytes from file") + } + // compare original and reconstructed // Some blueprints have unexported fields that are not serialized // (e.g., sync.Mutex, lazy-initialized caches). Use Equal method if available. @@ -94,6 +110,18 @@ func TestSerialization(t *testing.T) { "System.bitLen")); diff != "" { t.Fatalf("round trip mismatch (-want +got):\n%s", diff) } + if diff := cmp.Diff(r1cs1, &reconstructedFromFile, + blueprintComparer, + cmpopts.IgnoreFields(cs.R1CS{}, + "System.q", + "field", + "CoeffTable.mCoeffs", + "System.lbWireLevel", + "System.genericHint", + "System.SymbolTable", + "System.bitLen")); diff != "" { + t.Fatalf("file round trip mismatch (-want +got):\n%s", diff) + } } // ensure determinism in compilation / serialization / reconstruction diff --git a/constraint/marshal.go b/constraint/marshal.go index 71bcd4540e..492b75a5aa 100644 --- a/constraint/marshal.go +++ b/constraint/marshal.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "errors" "fmt" + "io" "reflect" "slices" @@ -76,22 +77,23 @@ func (system *System) FromBytes(data []byte) (int, error) { h := new(header) h.fromBytes(data) - if len(data) < headerLen+int(h.levelsLen)+int(h.instructionsLen)+int(h.calldataLen)+int(h.bodyLen) { - return 0, errors.New("invalid data length") + levelsEnd, instructionsEnd, calldataEnd, bodyEnd, err := h.sectionBoundaries(len(data)) + if err != nil { + return 0, err } // read the sections in parallel var g errgroup.Group g.Go(func() error { - return system.levelsFromBytes(data[headerLen : headerLen+h.levelsLen]) + return system.levelsFromBytes(data[headerLen:levelsEnd]) }) g.Go(func() error { - return system.instructionsFromBytes(data[headerLen+h.levelsLen : headerLen+h.levelsLen+h.instructionsLen]) + return system.instructionsFromBytes(data[levelsEnd:instructionsEnd]) }) g.Go(func() error { - return system.calldataFromBytes(data[headerLen+h.levelsLen+h.instructionsLen : headerLen+h.levelsLen+h.instructionsLen+h.calldataLen]) + return system.calldataFromBytes(data[instructionsEnd:calldataEnd]) }) // CBOR decoding of the constraint system (except what we do directly in binary) @@ -104,13 +106,15 @@ func (system *System) FromBytes(data []byte) (int, error) { if err != nil { return 0, err } - decoder := dm.NewDecoder(bytes.NewReader(data[headerLen+h.levelsLen+h.instructionsLen+h.calldataLen : headerLen+h.levelsLen+h.instructionsLen+h.calldataLen+h.bodyLen])) + decoder := dm.NewDecoder(bytes.NewReader(data[calldataEnd:bodyEnd])) if err := decoder.Decode(&system); err != nil { + _ = g.Wait() return 0, err } if err := system.CheckSerializationHeader(); err != nil { + _ = g.Wait() return 0, err } @@ -125,7 +129,7 @@ func (system *System) FromBytes(data []byte) (int, error) { return 0, err } - return headerLen + int(h.levelsLen) + int(h.instructionsLen) + int(h.calldataLen) + int(h.bodyLen), nil + return bodyEnd, nil } func (system *System) toBytes() ([]byte, error) { @@ -175,6 +179,29 @@ func (h *header) fromBytes(buf []byte) { h.bodyLen = binary.LittleEndian.Uint64(buf[24:32]) } +func (h header) sectionBoundaries(dataLen int) (levelsEnd, instructionsEnd, calldataEnd, bodyEnd int, err error) { + if levelsEnd, err = sectionEnd(headerLen, h.levelsLen, dataLen); err != nil { + return 0, 0, 0, 0, err + } + if instructionsEnd, err = sectionEnd(levelsEnd, h.instructionsLen, dataLen); err != nil { + return 0, 0, 0, 0, err + } + if calldataEnd, err = sectionEnd(instructionsEnd, h.calldataLen, dataLen); err != nil { + return 0, 0, 0, 0, err + } + if bodyEnd, err = sectionEnd(calldataEnd, h.bodyLen, dataLen); err != nil { + return 0, 0, 0, 0, err + } + return levelsEnd, instructionsEnd, calldataEnd, bodyEnd, nil +} + +func sectionEnd(start int, sectionLen uint64, dataLen int) (int, error) { + if start < 0 || start > dataLen || sectionLen > uint64(dataLen-start) { + return 0, errors.New("invalid data length") + } + return start + int(sectionLen), nil +} + func (system *System) calldataToBytes() ([]byte, error) { // calldata doesn't compress as well as the other sections; // it still gives a better size to use intcomp.CompressUint32 here, @@ -253,10 +280,16 @@ func (system *System) levelsToBytes() ([]byte, error) { } func (system *System) levelsFromBytes(in []byte) error { + if len(in) < 8 { + return io.ErrUnexpectedEOF + } levelsLen := binary.LittleEndian.Uint64(in[:8]) in = in[8:] + if levelsLen > uint64(len(in)/8) { + return errors.New("invalid data length") + } var ( buf32 []uint32 @@ -321,9 +354,15 @@ func (system *System) instructionsFromBytes(in []byte) error { } func (system *System) calldataFromBytes(buf []byte) error { + if len(buf) < 8 { + return io.ErrUnexpectedEOF + } calldataLen := binary.LittleEndian.Uint64(buf[:8]) - system.CallData = make([]uint32, calldataLen) buf = buf[8:] + if calldataLen > uint64(len(buf)) { + return errors.New("invalid calldata") + } + system.CallData = make([]uint32, int(calldataLen)) for i := uint64(0); i < calldataLen; i++ { v, n := binary.Uvarint(buf[:min(len(buf), binary.MaxVarintLen64)]) if n <= 0 { diff --git a/constraint/marshal_test.go b/constraint/marshal_test.go new file mode 100644 index 0000000000..cf1485f0c4 --- /dev/null +++ b/constraint/marshal_test.go @@ -0,0 +1,90 @@ +package constraint + +import ( + "encoding/binary" + "testing" +) + +func TestSystemFromBytesRejectsMalformedSectionLengths(t *testing.T) { + testCases := []struct { + name string + lengths [4]uint64 + payloads [][]byte + }{ + { + name: "all sections empty", + lengths: [4]uint64{}, + }, + { + name: "oversized levels section", + lengths: [4]uint64{^uint64(0), 0, 0, 0}, + }, + { + name: "truncated levels section header", + lengths: [4]uint64{1, 0, 0, 0}, + payloads: [][]byte{ + {0}, + }, + }, + { + name: "oversized levels count", + lengths: [4]uint64{8, 0, 0, 0}, + payloads: [][]byte{ + uint64Bytes(^uint64(0)), + }, + }, + { + name: "oversized instructions compressed count", + lengths: [4]uint64{8, 8, 0, 0}, + payloads: [][]byte{ + uint64Bytes(0), + uint64Bytes(^uint64(0)), + }, + }, + { + name: "truncated calldata section header", + lengths: [4]uint64{8, 32, 1, 0}, + payloads: [][]byte{ + make([]byte, 8), + make([]byte, 32), + {0}, + }, + }, + { + name: "oversized calldata count", + lengths: [4]uint64{8, 32, 8, 0}, + payloads: [][]byte{ + uint64Bytes(0), + make([]byte, 32), + uint64Bytes(^uint64(0)), + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var system System + _, err := system.FromBytes(serializedSystemPayload(tc.lengths, tc.payloads...)) + if err == nil { + t.Fatal("expected error") + } + }) + } +} + +func serializedSystemPayload(lengths [4]uint64, payloads ...[]byte) []byte { + data := make([]byte, headerLen) + for i, length := range lengths { + binary.LittleEndian.PutUint64(data[i*8:(i+1)*8], length) + } + for _, payload := range payloads { + data = append(data, payload...) + } + return data +} + +func uint64Bytes(v uint64) []byte { + data := make([]byte, 8) + binary.LittleEndian.PutUint64(data, v) + return data +} diff --git a/constraint/tinyfield/coeff.go b/constraint/tinyfield/coeff.go index 408519925a..3936ce5511 100644 --- a/constraint/tinyfield/coeff.go +++ b/constraint/tinyfield/coeff.go @@ -59,13 +59,14 @@ func (ct *CoeffTable) fromBytes(buf []byte) error { ctLen := binary.LittleEndian.Uint64(buf[:8]) buf = buf[8:] - if uint64(len(buf)) < ctLen*fr.Bytes { + if ctLen > uint64(len(buf)/fr.Bytes) { return errors.New("invalid buffer size") } - ct.Coefficients = make([]fr.Element, ctLen) - for i := uint64(0); i < ctLen; i++ { + ctLenInt := int(ctLen) + ct.Coefficients = make([]fr.Element, ctLenInt) + for i := 0; i < ctLenInt; i++ { var c fr.Element - k := int(i) * fr.Bytes + k := i * fr.Bytes for j := 0; j < fr.Limbs; j++ { c[j] = binary.LittleEndian.Uint32(buf[k+j*4 : k+(j+1)*4]) } diff --git a/constraint/tinyfield/marshal.go b/constraint/tinyfield/marshal.go index 68bbdb092f..f51a5c1841 100644 --- a/constraint/tinyfield/marshal.go +++ b/constraint/tinyfield/marshal.go @@ -9,10 +9,14 @@ import ( "encoding/binary" "fmt" "io" + "os" "github.com/blang/semver/v4" + "github.com/consensys/gnark/internal/backend/ioutils/mmap" ) +const serializedCSHeaderLen = 4 * 8 + // WriteTo encodes R1CS into provided io.Writer using cbor func (cs *system) WriteTo(w io.Writer) (int64, error) { b, err := cs.System.ToBytes() @@ -52,39 +56,118 @@ func (cs *system) WriteTo(w io.Writer) (int64, error) { // ReadFrom attempts to decode R1CS from io.Reader using cbor func (cs *system) ReadFrom(r io.Reader) (int64, error) { - var totalLen uint64 - if err := binary.Read(r, binary.LittleEndian, &totalLen); err != nil { + h, err := readSerializationHeader(r) + if err != nil { return 0, err } - - var major, minor, patch uint64 - if err := binary.Read(r, binary.LittleEndian, &major); err != nil { + if err := h.validate(); err != nil { return 0, err } - if err := binary.Read(r, binary.LittleEndian, &minor); err != nil { - return 0, err + if !serializedCSPayloadLenFits(h.totalLen) { + return 0, fmt.Errorf("serialized constraint system is too large: %d bytes", h.totalLen) } - if err := binary.Read(r, binary.LittleEndian, &patch); err != nil { + + data := make([]byte, h.totalLen) + if _, err := io.ReadFull(r, data); err != nil { return 0, err } - // TODO @gbotrel validate version, duplicate logic with core.go CheckSerializationHeader - if major != 0 || minor < 10 { - return 0, fmt.Errorf("unsupported gnark version %d.%d.%d", major, minor, patch) + return cs.readPayload(data, h.totalLen) +} + +// ReadFromFile decodes a constraint system from path. +// +// On platforms that support mmap, ReadFromFile decodes directly from mapped +// file pages and avoids allocating a temporary copy of the serialized payload. +// It falls back to ReadFrom on unsupported platforms. +func (cs *system) ReadFromFile(path string) (n int64, err error) { + if !mmap.Supported() { + f, err := os.Open(path) + if err != nil { + return 0, err + } + defer f.Close() + return cs.ReadFrom(f) } - data := make([]byte, totalLen) - if _, err := io.ReadFull(r, data); err != nil { + mapping, err := mmap.Open(path) + if err != nil { + return 0, err + } + defer func() { + if closeErr := mapping.Close(); err == nil { + err = closeErr + } + }() + + return cs.readFromBytes(mapping.Data) +} + +func (cs *system) readFromBytes(data []byte) (int64, error) { + h, err := parseSerializationHeader(data) + if err != nil { + return 0, err + } + if err := h.validate(); err != nil { return 0, err } + if h.totalLen > uint64(len(data)-serializedCSHeaderLen) { + return 0, io.ErrUnexpectedEOF + } + return cs.readPayload(data[serializedCSHeaderLen:serializedCSHeaderLen+int(h.totalLen)], h.totalLen) +} + +func (cs *system) readPayload(data []byte, totalLen uint64) (int64, error) { n, err := cs.System.FromBytes(data) if err != nil { return 0, err } + if n > len(data) { + return 0, io.ErrUnexpectedEOF + } data = data[n:] if err := cs.CoeffTable.fromBytes(data); err != nil { return 0, err } - return int64(totalLen) + 4*8, nil + return int64(totalLen) + serializedCSHeaderLen, nil +} + +type serializationHeader struct { + totalLen uint64 + major uint64 + minor uint64 + patch uint64 +} + +func readSerializationHeader(r io.Reader) (serializationHeader, error) { + var buf [serializedCSHeaderLen]byte + if _, err := io.ReadFull(r, buf[:]); err != nil { + return serializationHeader{}, err + } + return parseSerializationHeader(buf[:]) +} + +func parseSerializationHeader(data []byte) (serializationHeader, error) { + if len(data) < serializedCSHeaderLen { + return serializationHeader{}, io.ErrUnexpectedEOF + } + return serializationHeader{ + totalLen: binary.LittleEndian.Uint64(data[:8]), + major: binary.LittleEndian.Uint64(data[8:16]), + minor: binary.LittleEndian.Uint64(data[16:24]), + patch: binary.LittleEndian.Uint64(data[24:32]), + }, nil +} + +func (h serializationHeader) validate() error { + if h.major != 0 || h.minor < 10 { + return fmt.Errorf("unsupported gnark version %d.%d.%d", h.major, h.minor, h.patch) + } + return nil +} + +func serializedCSPayloadLenFits(payloadLen uint64) bool { + maxInt := uint64(int(^uint(0) >> 1)) + return payloadLen <= maxInt-serializedCSHeaderLen } diff --git a/constraint/tinyfield/r1cs_test.go b/constraint/tinyfield/r1cs_test.go index 074454190c..13637b4b98 100644 --- a/constraint/tinyfield/r1cs_test.go +++ b/constraint/tinyfield/r1cs_test.go @@ -7,6 +7,8 @@ package cs_test import ( "bytes" + "os" + "path/filepath" "reflect" "testing" @@ -67,8 +69,9 @@ func TestSerialization(t *testing.T) { if err != nil { t.Fatal(err) } + serialized := append([]byte(nil), buffer.Bytes()...) var reconstructed cs.R1CS - read, err = reconstructed.ReadFrom(&buffer) + read, err = reconstructed.ReadFrom(bytes.NewReader(serialized)) if err != nil { t.Fatal(err) } @@ -76,6 +79,19 @@ func TestSerialization(t *testing.T) { t.Fatal("didn't read same number of bytes we wrote") } + path := filepath.Join(t.TempDir(), "r1cs.bin") + if err := os.WriteFile(path, serialized, 0600); err != nil { + t.Fatal(err) + } + var reconstructedFromFile cs.R1CS + read, err = reconstructedFromFile.ReadFromFile(path) + if err != nil { + t.Fatal(err) + } + if written != read { + t.Fatal("didn't read same number of bytes from file") + } + // compare original and reconstructed // Some blueprints have unexported fields that are not serialized // (e.g., sync.Mutex, lazy-initialized caches). Use Equal method if available. @@ -97,6 +113,18 @@ func TestSerialization(t *testing.T) { "System.bitLen")); diff != "" { t.Fatalf("round trip mismatch (-want +got):\n%s", diff) } + if diff := cmp.Diff(r1cs1, &reconstructedFromFile, + blueprintComparer, + cmpopts.IgnoreFields(cs.R1CS{}, + "System.q", + "field", + "CoeffTable.mCoeffs", + "System.lbWireLevel", + "System.genericHint", + "System.SymbolTable", + "System.bitLen")); diff != "" { + t.Fatalf("file round trip mismatch (-want +got):\n%s", diff) + } } // ensure determinism in compilation / serialization / reconstruction diff --git a/internal/backend/ioutils/intcomp.go b/internal/backend/ioutils/intcomp.go index def78616b1..51479e116e 100644 --- a/internal/backend/ioutils/intcomp.go +++ b/internal/backend/ioutils/intcomp.go @@ -38,21 +38,23 @@ func ReadAndDecompressUints32(in []byte, buf32 []uint32) (outbuf32 []uint32, rea return buf32, 0, nil, io.ErrUnexpectedEOF } length := binary.LittleEndian.Uint64(in[:8]) - if uint64(len(in)) < 8+4*length { + if length > uint64((len(in)-8)/4) { return buf32, 0, nil, io.ErrUnexpectedEOF } - in = in[8 : 8+4*length] - if cap(buf32) < int(length) { - buf32 = make([]uint32, length) + lengthInt := int(length) + read = 8 + 4*lengthInt + in = in[8:read] + if cap(buf32) < lengthInt { + buf32 = make([]uint32, lengthInt) } else { - buf32 = buf32[:length] + buf32 = buf32[:lengthInt] } - for i := 0; i < int(length); i++ { + for i := 0; i < lengthInt; i++ { buf32[i] = binary.LittleEndian.Uint32(in[4*i : 4*(i+1)]) } - return buf32, 8 + 4*int(length), intcomp.UncompressUint32(buf32, nil), nil + return buf32, read, intcomp.UncompressUint32(buf32, nil), nil } // ReadAndDecompressUints64 reads a compressed slice of uint64 from r and decompresses it. @@ -62,13 +64,15 @@ func ReadAndDecompressUints64(in []byte) (int, []uint64, error) { return 0, nil, io.ErrUnexpectedEOF } length := binary.LittleEndian.Uint64(in[:8]) - if uint64(len(in)) < 8+8*length { + if length > uint64((len(in)-8)/8) { return 0, nil, io.ErrUnexpectedEOF } - in = in[8 : 8+8*length] - buffer := make([]uint64, length) - for i := 0; i < int(length); i++ { + lengthInt := int(length) + read := 8 + 8*lengthInt + in = in[8:read] + buffer := make([]uint64, lengthInt) + for i := 0; i < lengthInt; i++ { buffer[i] = binary.LittleEndian.Uint64(in[8*i : 8*(i+1)]) } - return 8 + 8*int(length), intcomp.UncompressUint64(buffer, nil), nil + return read, intcomp.UncompressUint64(buffer, nil), nil } diff --git a/internal/backend/ioutils/intcomp_test.go b/internal/backend/ioutils/intcomp_test.go index 4abfdc3f44..611695750b 100644 --- a/internal/backend/ioutils/intcomp_test.go +++ b/internal/backend/ioutils/intcomp_test.go @@ -2,9 +2,22 @@ package ioutils import ( "bytes" + "encoding/binary" "testing" ) +func TestReadAndDecompressRejectsMalformedLength(t *testing.T) { + input := make([]byte, 8) + binary.LittleEndian.PutUint64(input, ^uint64(0)) + + if _, _, _, err := ReadAndDecompressUints32(input, nil); err == nil { + t.Fatal("expected ReadAndDecompressUints32 error") + } + if _, _, err := ReadAndDecompressUints64(input); err == nil { + t.Fatal("expected ReadAndDecompressUints64 error") + } +} + func FuzzIntcomp32(f *testing.F) { f.Fuzz(func(t *testing.T, in []byte) { // convert in into a []uint32 ref slice; we just parse by multiple of 4 bytes diff --git a/internal/generator/backend/template/representations/coeff.go.tmpl b/internal/generator/backend/template/representations/coeff.go.tmpl index 75990b703f..26fdd5ef2e 100644 --- a/internal/generator/backend/template/representations/coeff.go.tmpl +++ b/internal/generator/backend/template/representations/coeff.go.tmpl @@ -54,13 +54,14 @@ func (ct *CoeffTable) fromBytes(buf []byte) error { ctLen := binary.LittleEndian.Uint64(buf[:8]) buf = buf[8:] - if uint64(len(buf)) < ctLen*fr.Bytes { + if ctLen > uint64(len(buf)/fr.Bytes) { return errors.New("invalid buffer size") } - ct.Coefficients = make([]fr.Element, ctLen) - for i := uint64(0); i < ctLen; i++ { + ctLenInt := int(ctLen) + ct.Coefficients = make([]fr.Element, ctLenInt) + for i := 0; i < ctLenInt; i++ { var c fr.Element - k := int(i) * fr.Bytes + k := i * fr.Bytes for j := 0; j < fr.Limbs; j++ { {{- if eq .ElementType "U64" -}} c[j] = binary.LittleEndian.Uint64(buf[k + j * 8 : k + (j+1)*8]) @@ -219,4 +220,4 @@ func (engine *field) Uint64(a constraint.{{ .ElementType }}) (uint64, bool) { return 0, false } return e.Uint64(), true -} \ No newline at end of file +} diff --git a/internal/generator/backend/template/representations/marshal.go.tmpl b/internal/generator/backend/template/representations/marshal.go.tmpl index 95456fc9cd..48118f6793 100644 --- a/internal/generator/backend/template/representations/marshal.go.tmpl +++ b/internal/generator/backend/template/representations/marshal.go.tmpl @@ -1,11 +1,15 @@ import ( - "io" "encoding/binary" "fmt" + "io" + "os" "github.com/blang/semver/v4" + "github.com/consensys/gnark/internal/backend/ioutils/mmap" ) +const serializedCSHeaderLen = 4 * 8 + // WriteTo encodes R1CS into provided io.Writer using cbor func (cs *system) WriteTo(w io.Writer) (int64, error) { b, err := cs.System.ToBytes() @@ -45,39 +49,118 @@ func (cs *system) WriteTo(w io.Writer) (int64, error) { // ReadFrom attempts to decode R1CS from io.Reader using cbor func (cs *system) ReadFrom(r io.Reader) (int64, error) { - var totalLen uint64 - if err := binary.Read(r, binary.LittleEndian, &totalLen); err != nil { + h, err := readSerializationHeader(r) + if err != nil { return 0, err } - - var major, minor, patch uint64 - if err := binary.Read(r, binary.LittleEndian, &major); err != nil { + if err := h.validate(); err != nil { return 0, err } - if err := binary.Read(r, binary.LittleEndian, &minor); err != nil { - return 0, err + if !serializedCSPayloadLenFits(h.totalLen) { + return 0, fmt.Errorf("serialized constraint system is too large: %d bytes", h.totalLen) } - if err := binary.Read(r, binary.LittleEndian, &patch); err != nil { + + data := make([]byte, h.totalLen) + if _, err := io.ReadFull(r, data); err != nil { return 0, err } - // TODO @gbotrel validate version, duplicate logic with core.go CheckSerializationHeader - if major != 0 || minor < 10 { - return 0, fmt.Errorf("unsupported gnark version %d.%d.%d", major, minor, patch) + return cs.readPayload(data, h.totalLen) +} + +// ReadFromFile decodes a constraint system from path. +// +// On platforms that support mmap, ReadFromFile decodes directly from mapped +// file pages and avoids allocating a temporary copy of the serialized payload. +// It falls back to ReadFrom on unsupported platforms. +func (cs *system) ReadFromFile(path string) (n int64, err error) { + if !mmap.Supported() { + f, err := os.Open(path) + if err != nil { + return 0, err + } + defer f.Close() + return cs.ReadFrom(f) } - data := make([]byte, totalLen) - if _, err := io.ReadFull(r, data); err != nil { + mapping, err := mmap.Open(path) + if err != nil { return 0, err } + defer func() { + if closeErr := mapping.Close(); err == nil { + err = closeErr + } + }() + + return cs.readFromBytes(mapping.Data) +} + +func (cs *system) readFromBytes(data []byte) (int64, error) { + h, err := parseSerializationHeader(data) + if err != nil { + return 0, err + } + if err := h.validate(); err != nil { + return 0, err + } + if h.totalLen > uint64(len(data)-serializedCSHeaderLen) { + return 0, io.ErrUnexpectedEOF + } + return cs.readPayload(data[serializedCSHeaderLen:serializedCSHeaderLen+int(h.totalLen)], h.totalLen) +} + +func (cs *system) readPayload(data []byte, totalLen uint64) (int64, error) { n, err := cs.System.FromBytes(data) if err != nil { return 0, err } + if n > len(data) { + return 0, io.ErrUnexpectedEOF + } data = data[n:] if err := cs.CoeffTable.fromBytes(data); err != nil { return 0, err } - return int64(totalLen) + 4*8, nil + return int64(totalLen) + serializedCSHeaderLen, nil +} + +type serializationHeader struct { + totalLen uint64 + major uint64 + minor uint64 + patch uint64 +} + +func readSerializationHeader(r io.Reader) (serializationHeader, error) { + var buf [serializedCSHeaderLen]byte + if _, err := io.ReadFull(r, buf[:]); err != nil { + return serializationHeader{}, err + } + return parseSerializationHeader(buf[:]) +} + +func parseSerializationHeader(data []byte) (serializationHeader, error) { + if len(data) < serializedCSHeaderLen { + return serializationHeader{}, io.ErrUnexpectedEOF + } + return serializationHeader{ + totalLen: binary.LittleEndian.Uint64(data[:8]), + major: binary.LittleEndian.Uint64(data[8:16]), + minor: binary.LittleEndian.Uint64(data[16:24]), + patch: binary.LittleEndian.Uint64(data[24:32]), + }, nil +} + +func (h serializationHeader) validate() error { + if h.major != 0 || h.minor < 10 { + return fmt.Errorf("unsupported gnark version %d.%d.%d", h.major, h.minor, h.patch) + } + return nil +} + +func serializedCSPayloadLenFits(payloadLen uint64) bool { + maxInt := uint64(int(^uint(0) >> 1)) + return payloadLen <= maxInt-serializedCSHeaderLen } diff --git a/internal/generator/backend/template/representations/tests/r1cs.go.tmpl b/internal/generator/backend/template/representations/tests/r1cs.go.tmpl index 48fcab43a1..c475a5e4a7 100644 --- a/internal/generator/backend/template/representations/tests/r1cs.go.tmpl +++ b/internal/generator/backend/template/representations/tests/r1cs.go.tmpl @@ -1,6 +1,8 @@ import ( "bytes" + "os" + "path/filepath" "reflect" "testing" @@ -72,8 +74,9 @@ func TestSerialization(t *testing.T) { if err != nil { t.Fatal(err) } + serialized := append([]byte(nil), buffer.Bytes()...) var reconstructed cs.R1CS - read, err = reconstructed.ReadFrom(&buffer) + read, err = reconstructed.ReadFrom(bytes.NewReader(serialized)) if err != nil { t.Fatal(err) } @@ -81,6 +84,19 @@ func TestSerialization(t *testing.T) { t.Fatal("didn't read same number of bytes we wrote") } + path := filepath.Join(t.TempDir(), "r1cs.bin") + if err := os.WriteFile(path, serialized, 0600); err != nil { + t.Fatal(err) + } + var reconstructedFromFile cs.R1CS + read, err = reconstructedFromFile.ReadFromFile(path) + if err != nil { + t.Fatal(err) + } + if written != read { + t.Fatal("didn't read same number of bytes from file") + } + // compare original and reconstructed // Some blueprints have unexported fields that are not serialized // (e.g., sync.Mutex, lazy-initialized caches). Use Equal method if available. @@ -100,6 +116,18 @@ func TestSerialization(t *testing.T) { "System.bitLen")); diff != "" { t.Fatalf("round trip mismatch (-want +got):\n%s", diff) } + if diff := cmp.Diff(r1cs1, &reconstructedFromFile, + blueprintComparer, + cmpopts.IgnoreFields(cs.R1CS{}, + "System.q", + "field", + "CoeffTable.mCoeffs", + "System.lbWireLevel", + "System.genericHint", + "System.SymbolTable", + "System.bitLen")); diff != "" { + t.Fatalf("file round trip mismatch (-want +got):\n%s", diff) + } } // ensure determinism in compilation / serialization / reconstruction @@ -200,4 +228,4 @@ func BenchmarkSolve(b *testing.B) { } }) -} \ No newline at end of file +}