mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
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.
196 lines
7.6 KiB
Go
196 lines
7.6 KiB
Go
// Copyright (C) 2025-2026, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package starkfri
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"testing"
|
|
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/luxfi/precompile/contract"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestStarkFRI_Address pins the slot allocated to the strict-PQ
|
|
// STARK-FRI verifier dispatch (placeholder 0x012220 pending dedicated
|
|
// LP allocation; see contract.go package doc).
|
|
func TestStarkFRI_Address(t *testing.T) {
|
|
want := common.HexToAddress("0x0000000000000000000000000000000000012220")
|
|
require.Equal(t, want, ContractStarkFRIVerifyAddress)
|
|
}
|
|
|
|
// TestStarkFRI_RequiredGas verifies the gas formula: 200_000 + 10 * len(input).
|
|
func TestStarkFRI_RequiredGas(t *testing.T) {
|
|
for _, n := range []int{0, 1, 64, 256, 4096, 131072} {
|
|
input := make([]byte, n)
|
|
got := StarkFRIVerifyPrecompile.RequiredGas(input)
|
|
want := BaseVerifyGas + uint64(n)*PerByteGas
|
|
require.Equal(t, want, got, "len=%d", n)
|
|
}
|
|
}
|
|
|
|
// TestStarkFRI_RejectsBadMagic ensures proofs without the "P3Q1" header
|
|
// are rejected with ErrInvalidProof (even when a verifier is
|
|
// registered).
|
|
func TestStarkFRI_RejectsBadMagic(t *testing.T) {
|
|
// Register a verifier that would say yes if asked; the structural
|
|
// check must reject before the verifier is invoked.
|
|
called := false
|
|
RegisterVerifier(func(byte, []byte, []byte) (bool, error) {
|
|
called = true
|
|
return true, nil
|
|
})
|
|
defer RegisterVerifier(nil)
|
|
|
|
proof := []byte("BOGUS-not-a-starkfri-proof-but-long-enough")
|
|
pub := []byte{0xde, 0xad, 0xbe, 0xef}
|
|
input := buildInput(VersionV1, proof, pub)
|
|
|
|
gas := StarkFRIVerifyPrecompile.RequiredGas(input) * 2
|
|
_, _, err := StarkFRIVerifyPrecompile.Run(nil, common.Address{}, ContractStarkFRIVerifyAddress, input, gas, true)
|
|
require.ErrorIs(t, err, ErrInvalidProof)
|
|
require.False(t, called, "verifier callback must not run on bad magic")
|
|
}
|
|
|
|
// TestStarkFRI_RejectsShortInput ensures inputs below MinInputLength
|
|
// fail with ErrInvalidInputLength.
|
|
// Length is settled before version: every fixture here carries version
|
|
// byte 0x00, so a parser that read the version first would answer
|
|
// ErrInvalidVersion. Poisoned, so a parser that read the length fields
|
|
// out of the memory behind the input would find 0xA5 rather than the
|
|
// zeros a plain make() leaves.
|
|
func TestStarkFRI_RejectsShortInput(t *testing.T) {
|
|
for _, sz := range []int{0, 1, 5, MinInputLength - 1} {
|
|
input := contract.Poisoned(make([]byte, sz), 256)
|
|
gas := StarkFRIVerifyPrecompile.RequiredGas(input) * 2
|
|
_, _, err := StarkFRIVerifyPrecompile.Run(nil, common.Address{}, ContractStarkFRIVerifyAddress, input, gas, true)
|
|
require.ErrorIs(t, err, ErrInvalidInputLength, "len=%d", sz)
|
|
}
|
|
}
|
|
|
|
// TestStarkFRI_RoundTrip_WithRegisteredVerifier registers a fake
|
|
// verifier that accepts and asserts Run() succeeds end-to-end.
|
|
func TestStarkFRI_RoundTrip_WithRegisteredVerifier(t *testing.T) {
|
|
saw := struct {
|
|
version byte
|
|
proof []byte
|
|
pub []byte
|
|
}{}
|
|
RegisterVerifier(func(v byte, p, pi []byte) (bool, error) {
|
|
saw.version = v
|
|
saw.proof = append([]byte(nil), p...)
|
|
saw.pub = append([]byte(nil), pi...)
|
|
return true, nil
|
|
})
|
|
defer RegisterVerifier(nil)
|
|
|
|
proof := sfProof([]byte("STARK-FRI-cSHAKE256-Goldilocks-payload"))
|
|
pub := []byte{0x01, 0x02, 0x03, 0x04, 0x05}
|
|
input := buildInput(VersionV1, proof, pub)
|
|
|
|
gas := StarkFRIVerifyPrecompile.RequiredGas(input) * 2
|
|
out, gasLeft, err := StarkFRIVerifyPrecompile.Run(nil, common.Address{}, ContractStarkFRIVerifyAddress, input, gas, true)
|
|
require.NoError(t, err)
|
|
require.Empty(t, out)
|
|
require.Less(t, gasLeft, gas)
|
|
require.Equal(t, VersionV1, saw.version)
|
|
require.Equal(t, proof, saw.proof)
|
|
require.Equal(t, pub, saw.pub)
|
|
}
|
|
|
|
// TestStarkFRI_NoVerifierRegistered confirms that without a registered
|
|
// verifier callback, structurally-valid input yields
|
|
// ErrVerifierNotRegistered.
|
|
func TestStarkFRI_NoVerifierRegistered(t *testing.T) {
|
|
RegisterVerifier(nil)
|
|
|
|
proof := sfProof([]byte("payload"))
|
|
pub := []byte{0xaa}
|
|
input := buildInput(VersionV1, proof, pub)
|
|
|
|
gas := StarkFRIVerifyPrecompile.RequiredGas(input) * 2
|
|
_, _, err := StarkFRIVerifyPrecompile.Run(nil, common.Address{}, ContractStarkFRIVerifyAddress, input, gas, true)
|
|
require.ErrorIs(t, err, ErrVerifierNotRegistered)
|
|
}
|
|
|
|
// TestStarkFRI_RejectsBadVersion confirms versions other than 0x01 are
|
|
// rejected with ErrInvalidVersion.
|
|
func TestStarkFRI_RejectsBadVersion(t *testing.T) {
|
|
RegisterVerifier(func(byte, []byte, []byte) (bool, error) { return true, nil })
|
|
defer RegisterVerifier(nil)
|
|
|
|
proof := sfProof([]byte("payload"))
|
|
pub := []byte{0xaa}
|
|
input := buildInput(0x00, proof, pub)
|
|
|
|
gas := StarkFRIVerifyPrecompile.RequiredGas(input) * 2
|
|
_, _, err := StarkFRIVerifyPrecompile.Run(nil, common.Address{}, ContractStarkFRIVerifyAddress, input, gas, true)
|
|
require.ErrorIs(t, err, ErrInvalidVersion)
|
|
}
|
|
|
|
// TestStarkFRI_VerifierReturnsFalse confirms that when the registered
|
|
// verifier reports the proof as invalid we surface ErrInvalidProof.
|
|
func TestStarkFRI_VerifierReturnsFalse(t *testing.T) {
|
|
RegisterVerifier(func(byte, []byte, []byte) (bool, error) { return false, nil })
|
|
defer RegisterVerifier(nil)
|
|
|
|
proof := sfProof([]byte("payload"))
|
|
pub := []byte{0xaa}
|
|
input := buildInput(VersionV1, proof, pub)
|
|
|
|
gas := StarkFRIVerifyPrecompile.RequiredGas(input) * 2
|
|
_, _, err := StarkFRIVerifyPrecompile.Run(nil, common.Address{}, ContractStarkFRIVerifyAddress, input, gas, true)
|
|
require.ErrorIs(t, err, ErrInvalidProof)
|
|
}
|
|
|
|
// TestStarkFRI_RegisterDefaultVerifier_DoesNotClobber pins the init-
|
|
// order invariant: a real verifier registered FIRST (via the imported
|
|
// precompile package's init) is NOT overwritten by a later
|
|
// RegisterDefaultVerifier (the geth-side refuse stub, whose init runs
|
|
// after the imported package's). This is what makes the build-tagged
|
|
// cgo binding win regardless of init order.
|
|
func TestStarkFRI_RegisterDefaultVerifier_DoesNotClobber(t *testing.T) {
|
|
defer RegisterVerifier(nil)
|
|
RegisterVerifier(nil) // start from an empty slot
|
|
realFn := func(byte, []byte, []byte) (bool, error) { return true, nil }
|
|
RegisterVerifier(realFn) // real binding claims the slot first
|
|
installed := RegisterDefaultVerifier(func(byte, []byte, []byte) (bool, error) {
|
|
return false, ErrVerifierNotRegistered // refuse stub, "runs later"
|
|
})
|
|
require.False(t, installed, "default must not install over an existing verifier")
|
|
ok, err := loadVerifier()(VersionV1, nil, nil)
|
|
require.True(t, ok)
|
|
require.NoError(t, err, "the real verifier must survive the default registration")
|
|
}
|
|
|
|
// TestStarkFRI_RegisterDefaultVerifier_EngagesWhenAbsent confirms the
|
|
// safe-refuse default engages when (and only when) no verifier is
|
|
// present — the no-tag / cgo-disabled build path.
|
|
func TestStarkFRI_RegisterDefaultVerifier_EngagesWhenAbsent(t *testing.T) {
|
|
defer RegisterVerifier(nil)
|
|
RegisterVerifier(nil) // empty slot
|
|
installed := RegisterDefaultVerifier(func(byte, []byte, []byte) (bool, error) {
|
|
return false, ErrVerifierNotRegistered
|
|
})
|
|
require.True(t, installed, "default must install into an empty slot")
|
|
_, err := loadVerifier()(VersionV1, nil, nil)
|
|
require.ErrorIs(t, err, ErrVerifierNotRegistered)
|
|
}
|
|
|
|
// buildInput serialises [version][proof_len][proof][pub_len][pub] in
|
|
// the wire format the precompile expects.
|
|
func buildInput(version byte, proof, pub []byte) []byte {
|
|
out := make([]byte, 0, 1+4+len(proof)+4+len(pub))
|
|
out = append(out, version)
|
|
var pl [4]byte
|
|
binary.BigEndian.PutUint32(pl[:], uint32(len(proof)))
|
|
out = append(out, pl[:]...)
|
|
out = append(out, proof...)
|
|
var ul [4]byte
|
|
binary.BigEndian.PutUint32(ul[:], uint32(len(pub)))
|
|
out = append(out, ul[:]...)
|
|
out = append(out, pub...)
|
|
return out
|
|
}
|