starkfri: refuse a proof that picked its own difficulty

The proof argument carries the p3q header, and this precompile never
looked at it -- only the "P3Q1" tag and the length framing. Everything
about how hard the verification would be, log_blowup and num_queries,
arrived off calldata chosen by whoever submitted the proof, and at the
weakest settings the wire admits that is about one bit.

admit() is now the one question both entry points ask: the envelope
tag, a frame long enough to state parameters at all, and
log_blowup * num_queries >= MinSoundnessBits. The verifier enforces
the same floor, but a consensus rule belongs where the chain can be
read, not only inside a linked staticlib -- and refusing here means
the node crosses no FFI boundary for a proof it was never going to
accept. The gas charge is unchanged; it is a function of calldata
length alone, so the saving is the node's work, not the caller's fee.
Under the starkfri_p3q tag a test reads p3q_min_soundness_bits() out
of the linked library, so the two copies cannot drift.

The fixtures move with it. "P3Q1" followed by arbitrary bytes was
standing in for a well-formed proof, but those bytes ARE the
parameters, so whether such a fixture cleared the floor was an
accident -- and under crypto/rand, not the same accident twice.
sfProof builds a frame that states what it means to state.
This commit is contained in:
2026-08-28 14:04:37 -07:00
parent 2b0e35ad84
commit 73f3159a94
9 changed files with 337 additions and 37 deletions
+4 -4
View File
@@ -35,11 +35,11 @@ func BenchmarkStarkFRI_Verify_AcceptAll(b *testing.B) {
RegisterVerifier(func(byte, []byte, []byte) (bool, error) { return true, nil })
defer RegisterVerifier(nil)
body := make([]byte, 2048-len(MagicHeader))
body := make([]byte, 2048-MinProofLength)
for i := range body {
body[i] = byte(i)
}
proof := append([]byte(MagicHeader), body...)
proof := sfProof(body)
pub := make([]byte, 128)
input := buildBenchInput(VersionV1, proof, pub)
gas := StarkFRIVerifyPrecompile.RequiredGas(input) * 2
@@ -84,11 +84,11 @@ func BenchmarkStarkFRI_Verify_Standalone(b *testing.B) {
RegisterVerifier(func(byte, []byte, []byte) (bool, error) { return true, nil })
defer RegisterVerifier(nil)
body := make([]byte, 2048-len(MagicHeader))
body := make([]byte, 2048-MinProofLength)
for i := range body {
body[i] = byte(i)
}
proof := append([]byte(MagicHeader), body...)
proof := sfProof(body)
pub := make([]byte, 128)
b.ReportAllocs()
b.ResetTimer()
+13 -10
View File
@@ -55,13 +55,13 @@
// calls `starkfri.RegisterVerifier` to wire the Go-side callback that
// actually invokes the FFI verifier. Without a registered verifier
// the precompile returns ErrVerifierNotRegistered. Structural
// validation (length checks + MagicHeader) runs in the precompile
// itself and is constant-time-friendly: no secret-dependent branches,
// public inputs and the proof are non-secret by construction.
// validation (length checks, MagicHeader, and the soundness floor in
// soundness.go) runs in the precompile itself and is
// constant-time-friendly: no secret-dependent branches, public inputs
// and the proof are non-secret by construction.
package starkfri
import (
"bytes"
"errors"
"sync/atomic"
@@ -120,6 +120,7 @@ var (
ErrInvalidVersion = errors.New("starkfri: invalid version byte")
ErrVerifierNotRegistered = errors.New("starkfri: verifier not registered")
ErrInvalidProof = errors.New("starkfri: proof verification failed")
ErrInsufficientSoundness = errors.New("starkfri: proof parameters below the soundness floor")
)
// VerifierFn is the Go-side callback that bridges to the out-of-band
@@ -192,8 +193,8 @@ func loadVerifier() VerifierFn {
// callback is wired (binding pending, not "unimplemented"); (false, err)
// on FFI / decode failures.
func Verify(proof, pubInputs []byte) (bool, error) {
if !bytes.HasPrefix(proof, magic) {
return false, ErrInvalidProof
if err := admit(proof); err != nil {
return false, err
}
fn := loadVerifier()
if fn == nil {
@@ -279,10 +280,12 @@ func (p *starkFRIVerifyPrecompile) Run(
// documented behaviour, matching the sibling P3Q precompile, which is
// why in.End() is not called here.
// Structural check: proof must begin with the MagicHeader. STARK-FRI
// proofs are non-secret, so byte-equality is fine.
if !bytes.HasPrefix(proof, magic) {
return nil, remaining, ErrInvalidProof
// Envelope tag, then the soundness floor — both decided on the
// proof's own bytes, before a verifier is consulted and before any
// cryptographic work. STARK-FRI proofs are non-secret, so
// byte-equality is fine.
if err := admit(proof); err != nil {
return nil, remaining, err
}
fn := loadVerifier()
+8 -8
View File
@@ -73,7 +73,7 @@ func TestStarkFRI_E2E_VerifiesNRealishProofs(t *testing.T) {
_, err = rand.Read(pub)
require.NoError(t, err)
proof := append([]byte(MagicHeader), body...)
proof := sfProof(body)
input := buildInput(VersionV1, proof, pub)
gas := StarkFRIVerifyPrecompile.RequiredGas(input) * 2
@@ -142,7 +142,7 @@ func TestStarkFRI_E2E_VerifierFailureSurfacesAsInvalidProof(t *testing.T) {
},
}
proof := append([]byte(MagicHeader), make([]byte, 64)...)
proof := sfProof(make([]byte, 64))
pub := []byte{0x01, 0x02}
input := buildInput(VersionV1, proof, pub)
@@ -212,7 +212,7 @@ func TestStarkFRI_E2E_OOGShortCircuit(t *testing.T) {
})
defer RegisterVerifier(nil)
proof := append([]byte(MagicHeader), make([]byte, 64)...)
proof := sfProof(make([]byte, 64))
pub := []byte{0x01}
input := buildInput(VersionV1, proof, pub)
gas := StarkFRIVerifyPrecompile.RequiredGas(input) - 1 // one wei short
@@ -231,7 +231,7 @@ func TestStarkFRI_E2E_OOGShortCircuit(t *testing.T) {
// the same (version, proof, pub) triple.
func TestStarkFRI_E2E_RoundTripParser(t *testing.T) {
wantVersion := VersionV1
wantProof := append([]byte(MagicHeader), []byte("P3Q-STARK-payload-bytes-N-rounds")...)
wantProof := sfProof([]byte("P3Q-STARK-payload-bytes-N-rounds"))
wantPub := []byte{0xab, 0xcd, 0xef, 0x12, 0x34}
var saw struct {
@@ -291,7 +291,7 @@ func TestStarkFRI_Fuzz_TruncatedInput(t *testing.T) {
})
defer RegisterVerifier(nil)
full := buildInput(VersionV1, append([]byte(MagicHeader), make([]byte, 64)...), []byte{0xaa, 0xbb})
full := buildInput(VersionV1, sfProof(make([]byte, 64)), []byte{0xaa, 0xbb})
for k := 0; k < len(full); k++ {
input := full[:k]
gas := StarkFRIVerifyPrecompile.RequiredGas(input) * 2
@@ -320,7 +320,7 @@ func TestStarkFRI_Fuzz_PaddedInput(t *testing.T) {
})
defer RegisterVerifier(nil)
canonical := buildInput(VersionV1, append([]byte(MagicHeader), make([]byte, 16)...), []byte{0xaa})
canonical := buildInput(VersionV1, sfProof(make([]byte, 16)), []byte{0xaa})
padded := append(bytes.Clone(canonical), make([]byte, 64)...)
gas := StarkFRIVerifyPrecompile.RequiredGas(padded) * 2
_, _, err := StarkFRIVerifyPrecompile.Run(
@@ -349,7 +349,7 @@ func TestStarkFRI_Fuzz_BitFlipped(t *testing.T) {
defer RegisterVerifier(nil)
canonical := buildInput(VersionV1,
append([]byte(MagicHeader), make([]byte, 128)...),
sfProof(make([]byte, 128)),
make([]byte, 32))
for i := 0; i < iterations; i++ {
@@ -429,7 +429,7 @@ func TestStarkFRI_Fuzz_WrongVersion(t *testing.T) {
if byte(v) == VersionV1 {
continue
}
proof := append([]byte(MagicHeader), make([]byte, 64)...)
proof := sfProof(make([]byte, 64))
pub := []byte{0xaa}
input := buildInput(byte(v), proof, pub)
gas := StarkFRIVerifyPrecompile.RequiredGas(input) * 2
+4 -4
View File
@@ -85,7 +85,7 @@ func TestStarkFRI_RoundTrip_WithRegisteredVerifier(t *testing.T) {
})
defer RegisterVerifier(nil)
proof := append([]byte(MagicHeader), []byte("STARK-FRI-cSHAKE256-Goldilocks-payload")...)
proof := sfProof([]byte("STARK-FRI-cSHAKE256-Goldilocks-payload"))
pub := []byte{0x01, 0x02, 0x03, 0x04, 0x05}
input := buildInput(VersionV1, proof, pub)
@@ -105,7 +105,7 @@ func TestStarkFRI_RoundTrip_WithRegisteredVerifier(t *testing.T) {
func TestStarkFRI_NoVerifierRegistered(t *testing.T) {
RegisterVerifier(nil)
proof := append([]byte(MagicHeader), []byte("payload")...)
proof := sfProof([]byte("payload"))
pub := []byte{0xaa}
input := buildInput(VersionV1, proof, pub)
@@ -120,7 +120,7 @@ func TestStarkFRI_RejectsBadVersion(t *testing.T) {
RegisterVerifier(func(byte, []byte, []byte) (bool, error) { return true, nil })
defer RegisterVerifier(nil)
proof := append([]byte(MagicHeader), []byte("payload")...)
proof := sfProof([]byte("payload"))
pub := []byte{0xaa}
input := buildInput(0x00, proof, pub)
@@ -135,7 +135,7 @@ func TestStarkFRI_VerifierReturnsFalse(t *testing.T) {
RegisterVerifier(func(byte, []byte, []byte) (bool, error) { return false, nil })
defer RegisterVerifier(nil)
proof := append([]byte(MagicHeader), []byte("payload")...)
proof := sfProof([]byte("payload"))
pub := []byte{0xaa}
input := buildInput(VersionV1, proof, pub)
+11 -11
View File
@@ -70,9 +70,9 @@ func TestSF_SafeRefuseIsTheDefault(t *testing.T) {
sfClear(t)
proofs := [][]byte{
[]byte(MagicHeader),
append([]byte(MagicHeader), bytes.Repeat([]byte{0x00}, 64)...),
append([]byte(MagicHeader), bytes.Repeat([]byte{0xff}, 4096)...),
sfProof(nil),
sfProof(bytes.Repeat([]byte{0x00}, 64)),
sfProof(bytes.Repeat([]byte{0xff}, 4096)),
}
pubs := [][]byte{{}, {0xaa}, bytes.Repeat([]byte{0x5a}, 256)}
@@ -124,7 +124,7 @@ func TestSF_VerifyChecksStructureBeforeConsultingTheVerifier(t *testing.T) {
"the verifier was consulted for a proof that failed structural validation")
// Control: a well-formed header does reach it.
ok, err := Verify([]byte(MagicHeader), []byte{0x01})
ok, err := Verify(sfProof(nil), []byte{0x01})
require.NoError(t, err)
require.True(t, ok)
require.True(t, called)
@@ -134,7 +134,7 @@ func TestSF_VerifyChecksStructureBeforeConsultingTheVerifier(t *testing.T) {
// distinctly, never collapsing a failure into a success.
func TestSF_VerifySurfacesTheVerdict(t *testing.T) {
sfClear(t)
proof := append([]byte(MagicHeader), 0x01, 0x02)
proof := sfProof([]byte{0x01, 0x02})
RegisterVerifier(func(byte, []byte, []byte) (bool, error) { return true, nil })
ok, err := Verify(proof, nil)
@@ -165,7 +165,7 @@ func TestSF_VerifySurfacesTheVerdict(t *testing.T) {
func TestSF_VerifyPassesTheWireThrough(t *testing.T) {
sfClear(t)
proof := append([]byte(MagicHeader), []byte("body-bytes")...)
proof := sfProof([]byte("body-bytes"))
pub := []byte("public-inputs")
var gotVer byte
@@ -209,7 +209,7 @@ func TestSF_RegistrationSeams(t *testing.T) {
// again — a verifier cannot be made permanent by accident.
RegisterVerifier(nil)
require.Nil(t, loadVerifier())
_, err := Verify([]byte(MagicHeader), nil)
_, err := Verify(sfProof(nil), nil)
require.ErrorIs(t, err, ErrVerifierNotRegistered)
// RegisterVerifier FORCES, overriding a default already in place.
@@ -217,7 +217,7 @@ func TestSF_RegistrationSeams(t *testing.T) {
return false, ErrVerifierNotRegistered
}))
RegisterVerifier(func(byte, []byte, []byte) (bool, error) { return true, nil })
ok, err := Verify([]byte(MagicHeader), nil)
ok, err := Verify(sfProof(nil), nil)
require.NoError(t, err)
require.True(t, ok, "RegisterVerifier must override an installed default")
}
@@ -229,7 +229,7 @@ func TestSF_WireBoundsAreRefusedNotPanicked(t *testing.T) {
sfClear(t)
RegisterVerifier(func(byte, []byte, []byte) (bool, error) { return true, nil })
proof := append([]byte(MagicHeader), []byte("body")...)
proof := sfProof([]byte("body"))
pub := []byte{0xaa}
good := sfWire(VersionV1, proof, pub)
@@ -287,7 +287,7 @@ func TestSF_RefusesLengthDeclaredPastTheCalldata(t *testing.T) {
return true, nil
})
proof := append([]byte(MagicHeader), []byte("body")...)
proof := sfProof([]byte("body"))
pub := []byte{0xaa}
good := sfWire(VersionV1, proof, pub)
@@ -339,7 +339,7 @@ func TestSF_GasIsChargedAndProportional(t *testing.T) {
prev = got
}
proof := append([]byte(MagicHeader), bytes.Repeat([]byte{1}, 128)...)
proof := sfProof(bytes.Repeat([]byte{1}, 128))
in := sfWire(VersionV1, proof, []byte{0xaa})
need := StarkFRIVerifyPrecompile.RequiredGas(in)
+71
View File
@@ -0,0 +1,71 @@
// Copyright (C) 2025-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package starkfri
import "bytes"
// The soundness floor: what the chain demands of a proof's parameters
// before it will spend a verification on it.
//
// A STARK/FRI proof carries its own security parameters. The verifier
// repeats an independent query check `num_queries` times against a
// codeword of rate 2^-log_blowup, so the two together decide how hard
// the proof is to forge — and both arrive off calldata, chosen by
// whoever submitted the proof. Without a floor the submitter picks the
// difficulty of the test applied to their own proof, which at the
// weakest settings the wire admits is about one bit: two attempts.
//
// This is a consensus rule. It is enforced here as well as inside the
// verifier so it is legible in the chain's own code rather than only
// in a linked staticlib, and so the node does no cryptographic work,
// and crosses no FFI boundary, for a proof it was never going to
// accept. The gas charge is unchanged either way — it is a function of
// calldata length alone (see RequiredGas) — so the saving is the
// node's, not the caller's.
const (
// MinSoundnessBits is log_blowup * num_queries, the bits of
// query-phase soundness a proof must claim.
//
// Under the capacity bound one query buys log_blowup bits, which
// makes the predicate an exact integer product — no floating
// point and no table, which is what a rule every node must agree
// on needs. Under the provable Johnson bound (Ben-Sasson, Carmon,
// Ishai, Kopparty, Saraf, "Proximity Gaps for Reed-Solomon
// Codes", FOCS 2020) the same parameters deliver roughly half as
// much. 128 is reachable at every blowup but rate-1/2, which
// cannot reach it under any bound within the wire's 64-query
// ceiling.
//
// The verifier is the source of this number; under the
// starkfri_p3q build tag a test asserts the two agree by reading
// p3q_min_soundness_bits() out of the linked library.
MinSoundnessBits = 128
// Offsets into the proof argument. The precompile envelope is the
// 4-byte MagicHeader; the p3q-native proof follows, beginning with
// a ProofSystemId byte and then a 10-byte header whose third and
// fifth bytes are the two parameters (see p3q-verifier/src/wire.rs).
offsetBlowup = len(MagicHeader) + 1 + 2
offsetQueries = len(MagicHeader) + 1 + 4
// MinProofLength is the shortest proof that can carry parameters
// at all: envelope + profile byte + the full 10-byte header.
MinProofLength = len(MagicHeader) + 1 + 10
)
// admit reports whether proof is a frame this chain will verify: it
// carries the envelope tag, it is long enough to state its parameters,
// and those parameters meet MinSoundnessBits.
//
// One predicate, asked identically by both entry points, so the two
// cannot answer differently.
func admit(proof []byte) error {
if !bytes.HasPrefix(proof, magic) || len(proof) < MinProofLength {
return ErrInvalidProof
}
if uint32(proof[offsetBlowup])*uint32(proof[offsetQueries]) < MinSoundnessBits {
return ErrInsufficientSoundness
}
return nil
}
+173
View File
@@ -0,0 +1,173 @@
// Copyright (C) 2025-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package starkfri
import (
"bytes"
"testing"
"github.com/luxfi/geth/common"
"github.com/stretchr/testify/require"
)
// sfProofAt returns a proof frame stating the given parameters:
// the "P3Q1" envelope tag, the P3Q-SHA3 profile byte, the 10-byte p3q
// header, then body.
func sfProofAt(logBlowup, numQueries byte, body []byte) []byte {
out := append([]byte(MagicHeader), 0x10) // ProofSystemId::Sha3
out = append(out,
0x01, // wire version
0x03, // log_degree_bound
logBlowup, //
0x01, // log_arity (binary folding)
numQueries, //
0x02, // log_final_poly
0x00, 0x00, // flags, reserved
0x00, 0x00, // public_input_len
)
return append(out, body...)
}
// sfProof returns the cheapest frame the floor admits, carrying body.
//
// Fixtures meaning "a proof that clears structural validation" must be
// built with this. A bare MagicHeader followed by arbitrary bytes
// states parameters nobody chose, so whether it clears the floor is an
// accident of those bytes — and under crypto/rand it is not even the
// same accident twice.
func sfProof(body []byte) []byte { return sfProofAt(2, 64, body) }
// TestSF_FloorBoundary walks the whole parameter grid the p3q wire
// admits and pins the verdict at every point: admitted exactly when
// log_blowup * num_queries reaches MinSoundnessBits. One query below
// the line refuses, one query on it passes.
//
// A per-field minimum cannot express this. (log_blowup=1,
// num_queries=64) and (log_blowup=5, num_queries=1) both sit inside
// every range the wire allows and neither is admissible; the boundary
// is a product, so no pair of independent bounds draws it.
func TestSF_FloorBoundary(t *testing.T) {
sfClear(t)
var reached int
RegisterVerifier(func(byte, []byte, []byte) (bool, error) {
reached++
return true, nil
})
for logBlowup := 1; logBlowup <= 5; logBlowup++ {
for numQueries := 1; numQueries <= 64; numQueries++ {
proof := sfProofAt(byte(logBlowup), byte(numQueries), nil)
want := logBlowup*numQueries >= MinSoundnessBits
before := reached
ok, err := Verify(proof, nil)
require.Equalf(t, want, ok,
"log_blowup=%d num_queries=%d (%d bits)",
logBlowup, numQueries, logBlowup*numQueries)
if want {
require.NoError(t, err)
require.Equal(t, before+1, reached, "an admitted proof must reach the verifier")
} else {
require.ErrorIs(t, err, ErrInsufficientSoundness)
require.Equal(t, before, reached,
"a proof below the floor must not reach the verifier")
}
}
}
// Rate 1/2 cannot reach the floor at any query count the wire
// allows, so the whole blowup is unreachable.
for numQueries := 1; numQueries <= 64; numQueries++ {
ok, _ := Verify(sfProofAt(1, byte(numQueries), nil), nil)
require.False(t, ok, "log_blowup=1 num_queries=%d", numQueries)
}
}
// TestSF_FloorHoldsAtBothEntryPoints: Run and Verify ask the same
// question, so a proof refused by one cannot be admitted by the other.
func TestSF_FloorHoldsAtBothEntryPoints(t *testing.T) {
sfClear(t)
called := false
RegisterVerifier(func(byte, []byte, []byte) (bool, error) {
called = true
return true, nil
})
weak := sfProofAt(2, 63, nil) // 126 bits — one query short
at := sfProofAt(2, 64, nil) // 128 bits — exactly the floor
_, err := Verify(weak, nil)
require.ErrorIs(t, err, ErrInsufficientSoundness)
require.ErrorIs(t, sfRun(sfWire(VersionV1, weak, nil)), ErrInsufficientSoundness)
require.False(t, called, "a sub-floor proof reached the verifier")
_, err = Verify(at, nil)
require.NoError(t, err)
require.NoError(t, sfRun(sfWire(VersionV1, at, nil)))
require.True(t, called)
}
// TestSF_FrameTooShortToStateParametersIsRefused: a proof carrying the
// envelope tag but not the header states no parameters at all, so
// there is nothing to hold to the floor. Refusing is the only
// fail-closed answer — reading past the end would decide the policy on
// whatever memory followed.
func TestSF_FrameTooShortToStateParametersIsRefused(t *testing.T) {
sfClear(t)
called := false
RegisterVerifier(func(byte, []byte, []byte) (bool, error) {
called = true
return true, nil
})
full := sfProof(nil)
require.Len(t, full, MinProofLength)
for n := range MinProofLength {
// Spare capacity behind the truncation holds the bytes that
// WOULD have cleared the floor, so a missing bound is visible.
short := full[:n:n]
short = append(bytes.Clone(short), full[n:]...)[:n]
ok, err := Verify(short, nil)
require.False(t, ok, "len=%d", n)
require.Error(t, err, "len=%d", n)
}
require.False(t, called, "a frame too short to state parameters reached the verifier")
require.NoError(t, sfRunErr(full))
}
// sfRunErr is Verify's error for a proof with no public inputs, so the
// control above reads at the same entry point as the sweep.
func sfRunErr(proof []byte) error {
_, err := Verify(proof, nil)
return err
}
// TestSF_FloorRefusalCostsNoVerification pins the ordering that makes
// the floor worth having on this side of the FFI: the refusal is
// decided on the proof's own bytes, before the verifier is looked up,
// so an unregistered node and a registered one answer identically.
func TestSF_FloorRefusalCostsNoVerification(t *testing.T) {
sfClear(t)
weak := sfProofAt(1, 1, nil)
in := sfWire(VersionV1, weak, []byte{0xaa})
require.ErrorIs(t, sfRun(in), ErrInsufficientSoundness,
"with no verifier registered, the floor still decides first")
RegisterVerifier(func(byte, []byte, []byte) (bool, error) {
t.Fatal("verifier consulted for a proof below the floor")
return true, nil
})
require.ErrorIs(t, sfRun(in), ErrInsufficientSoundness)
// Gas is a function of calldata length alone, so the refusal is
// not cheaper for the caller — the saving is the node's work.
require.Equal(t,
StarkFRIVerifyPrecompile.RequiredGas(in),
BaseVerifyGas+uint64(len(in))*PerByteGas)
_, rem, err := StarkFRIVerifyPrecompile.Run(nil, common.Address{},
ContractStarkFRIVerifyAddress, in,
StarkFRIVerifyPrecompile.RequiredGas(in)+42, true)
require.ErrorIs(t, err, ErrInsufficientSoundness)
require.Equal(t, uint64(42), rem, "the full gas charge is still taken")
}
+12
View File
@@ -109,6 +109,18 @@ var (
errBackendPanic = errors.New("starkfri/p3q: backend panicked (caught at FFI boundary)")
)
// backendSoundnessFloor reports the query-phase soundness, in bits,
// that the LINKED verifier requires before it will do any work.
//
// MinSoundnessBits is the same number stated on this side of the FFI,
// so the chain's policy is readable in the chain's own code. Two
// copies of a consensus parameter are only safe while they agree, and
// this is what lets a test ask. cgo cannot be called from a _test.go
// file, so the accessor lives here.
func backendSoundnessFloor() uint32 {
return uint32(C.p3q_min_soundness_bits())
}
// p3qVerify is the cgo bridge to `p3q_verify`. It performs the genuine
// FRI low-degree test (Merkle-cap authentication + per-query
// colinearity folds + final-polynomial check) under a Fiat-Shamir
+41
View File
@@ -27,8 +27,49 @@ import (
"testing"
"github.com/luxfi/geth/common"
"github.com/stretchr/testify/require"
)
// TestStarkFRI_FloorMatchesBackend: the precompile refuses below
// MinSoundnessBits so the policy is legible in the chain's own code,
// but the verifier is where the number comes from. A second copy is
// only safe while it agrees, and only the tagged build can ask — so
// this is where the two are held together.
//
// If this fails, the precompile and the library it links disagree
// about which proofs are worth verifying, and the looser of the two
// decides.
func TestStarkFRI_FloorMatchesBackend(t *testing.T) {
require.Equal(t, uint32(MinSoundnessBits), backendSoundnessFloor(),
"the precompile's floor and the linked verifier's floor have drifted")
}
// TestStarkFRI_FloorRefusesBeforeFFI: with the real backend wired, a
// proof below the floor is refused by the precompile, and the same
// bytes handed straight to the backend are refused there too. Neither
// side is load-bearing alone.
func TestStarkFRI_FloorRefusesBeforeFFI(t *testing.T) {
useRealVerifier(t)
pub, proof := loadGolden(t, "p3q_v1_sha3_golden")
require.NoError(t, runPrecompile(t, pub, proof), "the golden proof must verify")
// The header sits at a fixed offset behind the envelope tag and the
// profile byte; drop num_queries by one and the proof states 126
// bits where the chain demands 128.
weak := append([]byte(nil), proof...)
require.Equal(t, byte(64), weak[offsetQueries])
weak[offsetQueries] = 63
require.ErrorIs(t, runPrecompile(t, pub, weak), ErrInsufficientSoundness)
// And through the backend directly, bypassing the Go floor: the
// verifier refuses it on its own.
ok, err := p3qVerify(weak[len(MagicHeader):], pub)
require.False(t, ok)
require.ErrorIs(t, err, errBackendParse,
"the verifier must refuse sub-floor parameters at header decode")
}
// goldenDir locates p3q/vectors relative to this package
// (precompile/starkfri → ../../p3q/vectors).
func goldenDir(t testing.TB) string {