contract: one cursor holds every calldata bound, one verdict cannot be faked

A precompile's input is the two-index slice Memory.GetPtr returns, and
nothing on the path to Run copies it. len is what the caller declared and
paid for; cap is the rest of EVM memory, which that same caller filled with
MSTORE. So a parser reaching past its input does not panic and does not halt
a validator -- it reads bytes the attacker chose and answers over them.
Measured, not assumed: TestCalldataCarriesCallerChosenSpareCapacity drives
the VM and reads back len=8 cap=256 with 0xA5 past the end.

Cursor bounds every read against len, never against cap and never by letting
a slice expression refuse, and caps what it returns so a field cannot be
re-sliced forward. Verdict carries the bit (bool, error) was missing -- did a
verifier run -- so "nothing checked this" stops borrowing the spelling of
"this failed", and its zero value denies. Poisoned is the one definition of
the adversarial fixture: append-built input has spare capacity too, but
zeroed, so an over-read reads as a harmless run of zeros.

p3q parses through the cursor. Its six hand-written bounds become zero; the
class now rests on the single check in Cursor.Bytes, and deleting that check
fails both suites -- including with fixtures degraded back to plain append,
which is what the class used to depend on.
This commit is contained in:
2026-08-28 11:47:04 -07:00
parent 68fd104380
commit 9cc8cc7bb7
12 changed files with 1075 additions and 124 deletions
+58
View File
@@ -0,0 +1,58 @@
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// This file is package contract_test rather than contract because it imports
// geth's core/vm, which imports contract. An external test package is how that
// cycle is broken, and importing the real VM is the point: the claim under
// test is about what geth actually hands a precompile, so reading it from the
// VM beats restating it in a comment.
package contract_test
import (
"testing"
"github.com/luxfi/geth/core/vm"
"github.com/luxfi/precompile/contract"
"github.com/stretchr/testify/require"
)
// TestCalldataCarriesCallerChosenSpareCapacity is the measurement the Cursor
// is designed against, taken from the VM rather than asserted.
//
// The CALL opcode builds a precompile's input with Memory.GetPtr, which
// returns the two-index slice m.store[offset : offset+size]. So cap is the
// rest of EVM memory, and EVM memory is whatever the caller MSTOREd. A parser
// that reads past its declared input does not panic and does not halt a
// validator — it reads bytes the caller chose and returns a verdict over them.
//
// If geth ever copies the input on the way in, this test fails, and the
// reasoning in cursor.go needs rewriting rather than trusting.
func TestCalldataCarriesCallerChosenSpareCapacity(t *testing.T) {
m := vm.NewMemory()
m.Resize(256)
chosen := make([]byte, 256)
for i := range chosen {
chosen[i] = 0xA5
}
m.Set(0, 256, chosen) // the attacker's MSTOREs
// instructions.go opCall, verbatim:
// args := scope.Memory.GetPtr(inOffset.Uint64(), inSize.Uint64())
args := m.GetPtr(0, 8)
require.Len(t, args, 8, "len is the size the caller declared and paid for")
require.Greater(t, cap(args), len(args),
"a precompile's input carries spare capacity — re-read the threat model in cursor.go")
require.Equal(t, byte(0xA5), args[:16][8],
"and reading past the declared end yields bytes the caller chose")
// Poisoned reproduces exactly that shape for use as a fixture.
fixture := contract.Poisoned(make([]byte, 8), 8)
require.Len(t, fixture, 8)
require.Greater(t, cap(fixture), len(fixture))
require.Equal(t, byte(0xA5), fixture[:16][8])
// And the cursor refuses what the slice expression allows.
_, err := contract.Read(args).Bytes(16)
require.ErrorIs(t, err, contract.ErrShort)
}
+142
View File
@@ -0,0 +1,142 @@
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package contract
import (
"encoding/binary"
"fmt"
)
// ErrShort reports that the input ended before the wire format did: either
// fewer bytes remain than a fixed field needs, or a length read off the wire
// declares more bytes than the calldata holds.
//
// ErrTrailing reports bytes left over after the format was fully read.
//
// Both wrap ErrInvalidInput, so a caller that already refuses on
// errors.Is(err, ErrInvalidInput) keeps refusing exactly as before. They are
// built once at init, so returning one allocates nothing.
var (
ErrShort = fmt.Errorf("%w: shorter than the wire format requires", ErrInvalidInput)
ErrTrailing = fmt.Errorf("%w: trailing bytes after the wire format", ErrInvalidInput)
)
// Cursor reads a wire format off calldata one field at a time. It is the one
// way a precompile parses its input.
//
// Why a type rather than hand-written slice expressions:
//
// A precompile's input is a window into the EVM memory store. instructions.go
// (opCall and its three siblings) takes it with Memory.GetPtr, which returns
// the two-index slice m.store[offset : offset+size], and nothing on the path
// to Run copies it. So len(input) is the size the caller declared and paid gas
// for, while cap(input) is the rest of EVM memory — which that same caller
// filled with MSTORE before making the call.
//
// A Go slice expression s[a:b] refuses only when b exceeds cap(s). Past len
// and within cap it succeeds silently. A parser that reaches past its input
// therefore does not panic and does not halt a validator: it reads bytes the
// attacker chose, hands them to a verifier, and returns a verdict computed
// over material the caller never declared and never paid for. That is a
// forgery surface, and it is invisible to any test whose fixture leaves the
// spare region zeroed.
//
// Every read below is bounded against Len, never against cap, and never by
// letting a slice expression refuse. Every returned slice is capped to its own
// length so a downstream re-slice cannot walk forward into the same region. A
// parser written on this type cannot have the bug, and it does not depend on
// how its fixtures were built.
//
// The zero Cursor is an empty input: every read refuses.
type Cursor struct {
buf []byte
off int // invariant: 0 <= off <= len(buf)
}
// Read starts a cursor at the front of b.
func Read(b []byte) *Cursor { return &Cursor{buf: b} }
// Len is the number of bytes not yet read. Never negative.
func (c *Cursor) Len() int { return len(c.buf) - c.off }
// Bytes consumes exactly n bytes and returns them, or refuses.
//
// n is uint64 because it is usually a length read off the wire, and a wire
// length converted to int is negative on a 32-bit build for anything above
// 2^31 — where a slice expression refuses but a hand-written `< len` guard
// passes. The comparison here is a subtraction on the trusted Len, never an
// addition against an attacker-chosen n, so nothing can wrap past it.
//
// The returned slice has cap == len. It is a window on the input, not a copy,
// but it cannot be re-sliced forward: a caller that asks for 32 bytes and then
// reads 64 gets a panic rather than 32 bytes of somebody else's memory. That
// tripwire should never fire, since the point of the cursor is that a caller
// receives exactly the field it named. Between a loud failure and a silent
// forgery, loud is the correct direction.
func (c *Cursor) Bytes(n uint64) ([]byte, error) {
if n > uint64(c.Len()) {
return nil, ErrShort
}
lo := c.off
hi := lo + int(n)
c.off = hi
return c.buf[lo:hi:hi], nil
}
// Byte consumes one byte.
func (c *Cursor) Byte() (byte, error) {
b, err := c.Bytes(1)
if err != nil {
return 0, err
}
return b[0], nil
}
// Uint16 consumes a big-endian uint16.
func (c *Cursor) Uint16() (uint16, error) {
b, err := c.Bytes(2)
if err != nil {
return 0, err
}
return binary.BigEndian.Uint16(b), nil
}
// Uint32 consumes a big-endian uint32.
func (c *Cursor) Uint32() (uint32, error) {
b, err := c.Bytes(4)
if err != nil {
return 0, err
}
return binary.BigEndian.Uint32(b), nil
}
// Uint64 consumes a big-endian uint64.
func (c *Cursor) Uint64() (uint64, error) {
b, err := c.Bytes(8)
if err != nil {
return 0, err
}
return binary.BigEndian.Uint64(b), nil
}
// Rest consumes and returns everything left, capped to its own length.
// Use it where the format ends in an open field; use End where the format is
// pinned exactly.
func (c *Cursor) Rest() []byte {
b, _ := c.Bytes(uint64(c.Len()))
return b
}
// End refuses any bytes left over.
//
// Whether trailing bytes are refused is a property of the wire format, not of
// the parser: a format that admits them lets one signature be presented under
// many distinct calldatas. Formats that already document padding as accepted
// keep accepting it, and simply do not call End.
func (c *Cursor) End() error {
if c.Len() != 0 {
return ErrTrailing
}
return nil
}
+161
View File
@@ -0,0 +1,161 @@
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package contract
import (
"errors"
"testing"
"github.com/stretchr/testify/require"
)
// TestCursorRefusesPastTheDeclaredEnd is the property the whole type exists
// for. A hand-written s[a:b] over this input succeeds and returns poison; the
// cursor refuses.
func TestCursorRefusesPastTheDeclaredEnd(t *testing.T) {
in := Poisoned([]byte{1, 2, 3, 4}, 64)
// The bug, spelled out: a slice expression does not save you here.
require.NotPanics(t, func() {
over := in[:20]
require.Equal(t, byte(0xA5), over[10],
"a hand-written slice reads attacker bytes past the declared end")
})
// The cursor refuses the same read.
c := Read(in)
_, err := c.Bytes(20)
require.ErrorIs(t, err, ErrShort)
require.ErrorIs(t, err, ErrInvalidInput, "callers refusing on ErrInvalidInput keep refusing")
require.Equal(t, 4, c.Len(), "a refused read consumes nothing")
// And every field reader refuses at the boundary rather than reading on.
c = Read(in)
_, err = c.Bytes(4)
require.NoError(t, err)
_, err = c.Byte()
require.ErrorIs(t, err, ErrShort)
_, err = c.Uint16()
require.ErrorIs(t, err, ErrShort)
_, err = c.Uint32()
require.ErrorIs(t, err, ErrShort)
_, err = c.Uint64()
require.ErrorIs(t, err, ErrShort)
}
// TestCursorReturnsCappedSlices pins the tripwire: a field handed back cannot
// be re-sliced forward into the input's spare capacity.
func TestCursorReturnsCappedSlices(t *testing.T) {
in := Poisoned([]byte{1, 2, 3, 4, 5, 6, 7, 8}, 64)
c := Read(in)
got, err := c.Bytes(4)
require.NoError(t, err)
require.Equal(t, []byte{1, 2, 3, 4}, got)
require.Equal(t, len(got), cap(got), "a returned field has no spare capacity")
require.Panics(t, func() { _ = got[:8] },
"re-slicing a field past its length must fail loudly, not read the next field")
rest := c.Rest()
require.Equal(t, []byte{5, 6, 7, 8}, rest)
require.Equal(t, len(rest), cap(rest), "Rest is capped too")
require.Panics(t, func() { _ = rest[:len(rest)+1] })
}
// TestCursorReadsFieldsInOrder walks a whole wire format.
func TestCursorReadsFieldsInOrder(t *testing.T) {
in := Poisoned([]byte{
0x01, // byte
0x02, 0x03, // uint16
0x00, 0x00, 0x00, 0x04, // uint32
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, // uint64
0xAA, 0xBB, // rest
}, 32)
c := Read(in)
require.Equal(t, 17, c.Len())
b, err := c.Byte()
require.NoError(t, err)
require.Equal(t, byte(0x01), b)
u16, err := c.Uint16()
require.NoError(t, err)
require.Equal(t, uint16(0x0203), u16)
u32, err := c.Uint32()
require.NoError(t, err)
require.Equal(t, uint32(4), u32)
u64, err := c.Uint64()
require.NoError(t, err)
require.Equal(t, uint64(5), u64)
require.Equal(t, 2, c.Len())
require.Error(t, c.End())
require.ErrorIs(t, c.End(), ErrTrailing)
require.ErrorIs(t, c.End(), ErrInvalidInput)
require.Equal(t, []byte{0xAA, 0xBB}, c.Rest())
require.Equal(t, 0, c.Len())
require.NoError(t, c.End())
require.Empty(t, c.Rest(), "Rest at the end yields nothing")
}
// TestCursorRefusesADeclaredLengthLargerThanTheInput is the length-prefixed
// shape every parser here uses, and the one that reads attacker bytes when it
// is written by hand.
func TestCursorRefusesADeclaredLengthLargerThanTheInput(t *testing.T) {
for _, claim := range []uint32{5, 1 << 20, 1 << 30, ^uint32(0)} {
body := []byte{byte(claim >> 24), byte(claim >> 16), byte(claim >> 8), byte(claim), 1, 2, 3, 4}
c := Read(Poisoned(body, 1<<16))
n, err := c.Uint32()
require.NoError(t, err)
require.Equal(t, claim, n)
_, err = c.Bytes(uint64(n))
require.ErrorIs(t, err, ErrShort, "declared length %d exceeds the calldata", claim)
}
}
// TestCursorLengthArithmeticDoesNotWrap pins that the bound is a subtraction
// on the trusted length, never an addition against the declared one. A guard
// written as `off+n > len` wraps for n near 2^64 and admits the read.
func TestCursorLengthArithmeticDoesNotWrap(t *testing.T) {
c := Read(Poisoned([]byte{1, 2, 3, 4}, 1<<16))
_, err := c.Bytes(1)
require.NoError(t, err)
for _, n := range []uint64{^uint64(0), ^uint64(0) - 3, 1 << 63, 1 << 40} {
c2 := Read(Poisoned([]byte{1, 2, 3, 4}, 1<<16))
_, err := c2.Bytes(1)
require.NoError(t, err)
_, err = c2.Bytes(n)
require.ErrorIs(t, err, ErrShort, "n=%d must be refused, not wrapped", n)
}
}
// TestZeroCursorRefusesEverything pins the fail-secure default.
func TestZeroCursorRefusesEverything(t *testing.T) {
var c Cursor
require.Equal(t, 0, c.Len())
_, err := c.Byte()
require.ErrorIs(t, err, ErrShort)
_, err = c.Bytes(1)
require.ErrorIs(t, err, ErrShort)
require.NoError(t, c.End())
require.Empty(t, c.Rest())
// A zero-length read on an empty input is the one thing that succeeds,
// and it yields nothing.
got, err := c.Bytes(0)
require.NoError(t, err)
require.Empty(t, got)
}
// TestCursorSentinelsAreDistinct guards the error identities the converted
// parsers depend on.
func TestCursorSentinelsAreDistinct(t *testing.T) {
require.False(t, errors.Is(ErrShort, ErrTrailing))
require.False(t, errors.Is(ErrTrailing, ErrShort))
require.True(t, errors.Is(ErrShort, ErrInvalidInput))
require.True(t, errors.Is(ErrTrailing, ErrInvalidInput))
require.False(t, errors.Is(ErrInvalidInput, ErrShort))
}
+32
View File
@@ -0,0 +1,32 @@
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package contract
// Poisoned returns b with spare capacity behind it, filled with a
// recognisable pattern. It is the one definition of what a precompile's input
// actually looks like, and every refusal test in this repository submits one.
//
// The EVM hands a precompile the two-index slice Memory.GetPtr returns
// (instructions.go opCall and its three siblings), and nothing on the path to
// Run copies it. So len is the size the caller declared and paid gas for,
// while cap is the rest of EVM memory — memory that same caller filled with
// MSTORE before making the call.
//
// A fixture built with append also carries spare capacity, but zeroed or
// stale. An over-read into it looks like a harmless run of zeros, and a test
// over such a fixture passes whether or not the bound it means to exercise is
// present. Chosen bytes are what make an over-read visible as an over-read,
// and what make the test's subject the parser rather than the allocator.
//
// It lives in non-test source because nine packages' tests need the same
// definition, and two definitions of the adversary is one too many. Nothing
// in a Run path calls it.
func Poisoned(b []byte, spare int) []byte {
full := make([]byte, len(b)+spare)
for i := range full {
full[i] = 0xA5
}
copy(full, b)
return full[:len(b)]
}
+126
View File
@@ -0,0 +1,126 @@
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package contract
import "errors"
// ErrRejected is the outcome of a verifier that ran and did not accept.
// ErrNoVerifier is the outcome of a path that could not verify at all — no
// verifier is wired, a key carries none of the material the equation needs,
// an operation is disabled. The two are different facts and a caller that
// treats them alike has misread one of them.
var (
ErrRejected = errors.New("verification failed")
ErrNoVerifier = errors.New("no verifier: nothing checked this")
)
// Verdict is the outcome of one cryptographic check.
//
// It replaces (bool, error) on verification paths, for a reason that was
// measured rather than assumed. Three verifiers in this repository answered
// "valid" without verifying anything: one folded its IPA rounds, assigned the
// result to _, and returned true; one returned a length check as a verdict, so
// every byte string was a valid range proof; one returned true for every
// statement. Three existing tests missed all of it because they discarded the
// verdict into _.
//
// (bool, error) has four representable states and three meanings:
//
// (true, nil) verified, accepted
// (false, nil) verified and rejected — OR nothing ran ← the collision
// (false, err) an error
// (true, err) nonsense, and nothing stops it being written
//
// The collision is the defect. "I did not verify" has no spelling of its own,
// so it borrows the spelling of "I verified and it failed", and a caller
// cannot tell a missing verifier from a bad proof. The missing thing is a bit
// recording whether a verifier ran. Verdict carries it, and its zero value has
// that bit clear — so a fall-through, an unset struct field, a forgotten
// branch and a `var v Verdict` all refuse. Fail-secure is the default rather
// than something each author has to remember.
//
// Why not error alone, with a sentinel for "no verifier". It is the more
// obvious answer and it is worse. `return nil` is the most ordinary line in
// Go: it is what a function tail looks like when nothing went wrong, and under
// an error-only signature it MEANS accepted. The accept becomes the default
// shape of a forgotten path. The halo2 bug written error-only is
// `_ = foldedCommitment; return nil` — the same bug, now indistinguishable
// from ordinary success. Fail-secure needs the default to deny, and for error
// the default is nil, which admits. So error-only inverts exactly the property
// this type exists to hold. (bool, error)'s zero (false, nil) is at least a
// refusal; its problems are the collision above and that `true` is a keyword
// anyone can type. Verdict fixes both: the collision structurally, and the
// keyword by having no exported field — a positive cannot be written as a
// literal from another package, only obtained from Checked.
//
// What that does NOT do: Checked(true) is still writable, and no Go type can
// stop it. What it does instead is make every claim of a positive an
// enumerable call site — `grep -rn 'contract.Checked'` lists all of them for a
// reviewer — and TestCheckedIsNeverALiteral in this package fails the build if
// any of them passes a constant. The lie is not impossible; it is impossible
// to write by accident, and impossible to leave in the tree.
//
// Verdict is for security verdicts only. A lookup's (T, bool) reports presence
// and stays as it is: ring's scalar(b) ([]*big.Int, bool) refuses a scalar
// outside [1, N-1] at parse time and every caller checks it. The difference is
// what a wrong true costs — a missing map entry, or an accepted forgery.
type Verdict struct {
ran bool
passed bool
why error
}
// Checked records the outcome of a check that ran.
//
// passed must be the verifier's own closing predicate — the expression that
// compares computed evidence against the claim. Never a literal: a literal is
// a claim about work, not the work. TestCheckedIsNeverALiteral enforces that
// across every package in this module.
func Checked(passed bool) Verdict { return Verdict{ran: true, passed: passed} }
// Refused records that no verifier ran, and why. A nil or ErrRejected reason
// is replaced by ErrNoVerifier: "nothing checked this" must never be able to
// masquerade as "this was checked and failed", which is the collision the type
// exists to remove.
func Refused(why error) Verdict {
if why == nil || errors.Is(why, ErrRejected) {
why = ErrNoVerifier
}
return Verdict{why: why}
}
// OK reports acceptance, and only acceptance. The zero Verdict is not OK.
func (v Verdict) OK() bool { return v.ran && v.passed }
// Err is nil on acceptance, ErrRejected when a verifier ran and refused, and
// the refusal reason when none ran. errors.Is(v.Err(), ErrNoVerifier)
// distinguishes "nothing checked this" from "this did not check out".
func (v Verdict) Err() error {
switch {
case v.OK():
return nil
case v.ran:
return ErrRejected
case v.why != nil:
return v.why
default:
return ErrNoVerifier
}
}
// Word is the 32-byte EVM-ABI encoding of the verdict as a bool: the last byte
// is 1 on acceptance and 0 otherwise. Fresh on every call, so a caller may
// mutate what it gets without touching the next caller's result.
//
// A refusal encodes as false. A precompile whose contract is "report the
// outcome, never revert" must still say something, and false is the only
// honest thing to say about a check that did not happen. A precompile that can
// revert should return Err instead, which keeps the distinction.
func (v Verdict) Word() []byte {
w := make([]byte, 32)
if v.OK() {
w[31] = 1
}
return w
}
+213
View File
@@ -0,0 +1,213 @@
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package contract
import (
"go/ast"
"go/parser"
"go/token"
"io/fs"
"os"
"path/filepath"
"strings"
"testing"
)
// moduleRoot walks up from this package to the directory holding go.mod.
func moduleRoot(t *testing.T) string {
t.Helper()
dir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
for {
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
return dir
}
parent := filepath.Dir(dir)
if parent == dir {
t.Fatal("no go.mod above the working directory")
}
dir = parent
}
}
// walkSource visits every non-test .go file in the module, skipping vendored
// and nested-module trees.
func walkSource(t *testing.T, visit func(path string, fset *token.FileSet, f *ast.File)) {
t.Helper()
root := moduleRoot(t)
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return nil // an unreadable directory is not this test's business
}
if d.IsDir() {
switch d.Name() {
case ".git", "vendor", "node_modules", "testdata":
return fs.SkipDir
}
if path != root {
if _, err := os.Stat(filepath.Join(path, "go.mod")); err == nil {
return fs.SkipDir // a nested module is not this module
}
}
return nil
}
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return nil
}
fset := token.NewFileSet()
f, perr := parser.ParseFile(fset, path, nil, 0)
if perr != nil {
return nil // unparseable source belongs to whoever is editing it
}
visit(path, fset, f)
return nil
})
if err != nil {
t.Fatal(err)
}
}
// isConstBool reports whether e is a compile-time true/false, including a
// parenthesised or negated one.
func isConstBool(e ast.Expr) bool {
switch x := e.(type) {
case *ast.Ident:
return x.Name == "true" || x.Name == "false"
case *ast.ParenExpr:
return isConstBool(x.X)
case *ast.UnaryExpr:
return x.Op == token.NOT && isConstBool(x.X)
}
return false
}
// calls reports whether e is a call to the named function, either bare
// (inside package contract) or qualified as contract.Name.
func calls(e ast.Expr, name string) (*ast.CallExpr, bool) {
c, ok := e.(*ast.CallExpr)
if !ok {
return nil, false
}
switch fn := c.Fun.(type) {
case *ast.Ident:
return c, fn.Name == name
case *ast.SelectorExpr:
id, ok := fn.X.(*ast.Ident)
return c, ok && id.Name == "contract" && fn.Sel.Name == name
}
return nil, false
}
// TestCheckedIsNeverALiteral is the enforcement half of the Verdict argument.
//
// The type makes a positive impossible to write as a struct literal from
// another package, and makes the zero value a refusal. What it cannot stop is
// Checked(true) — no Go type can. This walks every non-test file in the module
// and fails if any call to Checked passes a constant.
//
// That is what closes the measured defect. `return true, nil` is one of the
// most common lines in Go and reads as ordinary success; Checked(true) is a
// single greppable shape that fails the build. A positive verdict now has to
// come from an expression that computed something.
func TestCheckedIsNeverALiteral(t *testing.T) {
var bad []string
walkSource(t, func(path string, fset *token.FileSet, f *ast.File) {
ast.Inspect(f, func(n ast.Node) bool {
e, isExpr := n.(ast.Expr)
if !isExpr {
return true
}
c, ok := calls(e, "Checked")
if !ok || len(c.Args) != 1 {
return true
}
if isConstBool(c.Args[0]) {
bad = append(bad, fset.Position(c.Pos()).String())
}
return true
})
})
if len(bad) > 0 {
t.Fatalf("Checked was passed a constant — a verdict claimed without a computation:\n %s",
strings.Join(bad, "\n "))
}
}
// TestVerdictIsNeverDiscarded pins the other half: a caller must not obtain a
// Verdict and throw it away.
//
// The single-value signature already does most of this. Under (bool, error) a
// caller could write `_, err := verify(...)` and keep the error while dropping
// the verdict — which is exactly how three tests came to miss three verifiers
// that answered valid without verifying. A Verdict cannot be split that way:
// the verdict and the reason are one value, so there is nothing to take while
// leaving the rest.
//
// What remains possible is dropping the whole thing: `_ = verify(...)`, or the
// call as a bare statement. This refuses both, in files that deal in Verdicts.
func TestVerdictIsNeverDiscarded(t *testing.T) {
var bad []string
walkSource(t, func(path string, fset *token.FileSet, f *ast.File) {
src, err := os.ReadFile(path)
if err != nil || !strings.Contains(string(src), "Verdict") {
return // only files that traffic in verdicts
}
ast.Inspect(f, func(n ast.Node) bool {
switch s := n.(type) {
case *ast.AssignStmt:
// `_ = f()` / `_, _ = f()` with a Verdict-shaped callee.
allBlank := true
for _, lhs := range s.Lhs {
id, ok := lhs.(*ast.Ident)
if !ok || id.Name != "_" {
allBlank = false
}
}
if !allBlank {
return true
}
for _, rhs := range s.Rhs {
if verdictShaped(rhs) {
bad = append(bad, fset.Position(s.Pos()).String())
}
}
case *ast.ExprStmt:
if verdictShaped(s.X) {
bad = append(bad, fset.Position(s.Pos()).String())
}
}
return true
})
})
if len(bad) > 0 {
t.Fatalf("a verdict was obtained and discarded:\n %s", strings.Join(bad, "\n "))
}
}
// verdictShaped reports whether e is a call that yields a Verdict: either a
// constructor, or a function whose name says it verifies.
func verdictShaped(e ast.Expr) bool {
if _, ok := calls(e, "Checked"); ok {
return true
}
if _, ok := calls(e, "Refused"); ok {
return true
}
c, ok := e.(*ast.CallExpr)
if !ok {
return false
}
var name string
switch fn := c.Fun.(type) {
case *ast.Ident:
name = fn.Name
case *ast.SelectorExpr:
name = fn.Sel.Name
default:
return false
}
return strings.Contains(strings.ToLower(name), "verify")
}
+206
View File
@@ -0,0 +1,206 @@
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package contract
import (
"errors"
"go/ast"
"go/parser"
"go/token"
"testing"
"github.com/stretchr/testify/require"
)
// TestZeroVerdictRefuses is the property the type exists for: the default,
// the fall-through, the forgotten branch and the unset struct field all deny.
//
// This is what (bool, error) could not give and what an error-only signature
// gives backwards — under error-only the zero value is nil, which admits.
func TestZeroVerdictRefuses(t *testing.T) {
var v Verdict
require.False(t, v.OK())
require.ErrorIs(t, v.Err(), ErrNoVerifier)
require.Equal(t, byte(0), v.Word()[31])
// A struct that forgot to set its verdict field denies too.
var holder struct{ V Verdict }
require.False(t, holder.V.OK())
require.ErrorIs(t, holder.V.Err(), ErrNoVerifier)
}
// TestVerdictStatesAreDistinct pins the collision (bool, error) could not
// express: "nothing checked this" and "this did not check out" are different
// facts with different errors.
func TestVerdictStatesAreDistinct(t *testing.T) {
ran := func(b bool) bool { return b } // never a literal; see the gate
accepted := Checked(ran(true))
require.True(t, accepted.OK())
require.NoError(t, accepted.Err())
require.Equal(t, byte(1), accepted.Word()[31])
rejected := Checked(ran(false))
require.False(t, rejected.OK())
require.ErrorIs(t, rejected.Err(), ErrRejected)
require.NotErrorIs(t, rejected.Err(), ErrNoVerifier,
"a verifier that ran and refused is not a missing verifier")
require.Equal(t, byte(0), rejected.Word()[31])
missing := Refused(ErrNoVerifier)
require.False(t, missing.OK())
require.ErrorIs(t, missing.Err(), ErrNoVerifier)
require.NotErrorIs(t, missing.Err(), ErrRejected,
"a missing verifier is not a failed verification")
require.Equal(t, byte(0), missing.Word()[31])
}
// TestRefusedNamesItsReason pins that a refusal carries which one it was, so a
// caller can tell an incomplete key from a disabled op from an unwired FFI.
func TestRefusedNamesItsReason(t *testing.T) {
incomplete := errors.New("verifying key carries no IPA commitment key")
v := Refused(incomplete)
require.ErrorIs(t, v.Err(), incomplete)
require.False(t, v.OK())
}
// TestRefusedCannotImpersonateRejection is the collision guard from the other
// side. Refused(nil) and Refused(ErrRejected) would both let "nothing ran"
// answer as "ran and failed", which is the exact state the type removes.
func TestRefusedCannotImpersonateRejection(t *testing.T) {
require.ErrorIs(t, Refused(nil).Err(), ErrNoVerifier)
require.ErrorIs(t, Refused(ErrRejected).Err(), ErrNoVerifier)
wrapped := errors.Join(ErrRejected, errors.New("context"))
require.ErrorIs(t, Refused(wrapped).Err(), ErrNoVerifier,
"a reason that wraps ErrRejected is still not a rejection")
}
// TestVerdictWordIsFreshEachCall pins that a caller may scribble on a result
// without corrupting the next one. A shared word would let one consumer flip
// another's verification outcome.
func TestVerdictWordIsFreshEachCall(t *testing.T) {
ran := func(b bool) bool { return b }
v := Checked(ran(true))
first := v.Word()
require.Len(t, first, 32)
for i := range first {
first[i] = 0xAA
}
require.Equal(t, byte(1), v.Word()[31])
require.Equal(t, byte(0), v.Word()[0])
}
// TestVerdictCannotBeSplit is the structural half of "callers that ignore the
// result should be visible".
//
// Under (bool, error) a caller could write `_, err := verify(...)` and keep the
// error while dropping the verdict — which is how three tests came to miss
// three verifiers that answered valid without verifying. A Verdict is one
// value carrying both, so there is nothing to take while leaving the rest:
// `_, err := f()` does not compile against a single-value signature. This
// asserts the two readers agree, which is what makes carrying one safe.
func TestVerdictCannotBeSplit(t *testing.T) {
ran := func(b bool) bool { return b }
for _, v := range []Verdict{Checked(ran(true)), Checked(ran(false)), Refused(nil), {}} {
require.Equal(t, v.Err() == nil, v.OK(), "OK and Err must never disagree")
require.Equal(t, v.OK(), v.Word()[31] == 1, "Word must never disagree either")
}
}
// --- the gates, tested against source that violates them -------------------
//
// A gate that has never failed is not known to be a gate. These parse
// synthetic files and assert the detection logic fires, so the two repo-wide
// gates are themselves under test rather than merely passing vacuously.
func parseExprs(t *testing.T, src string) *ast.File {
t.Helper()
f, err := parser.ParseFile(token.NewFileSet(), "x.go", src, 0)
require.NoError(t, err)
return f
}
func TestGateCatchesALiteralPositive(t *testing.T) {
const src = `package p
func a() Verdict { return Checked(true) }
func b() Verdict { return contract.Checked(false) }
func c() Verdict { return Checked(!true) }
func d() Verdict { return Checked((true)) }
func e(x bool) Verdict { return Checked(x) }
func f(a, b []byte) Verdict { return contract.Checked(bytesEqual(a, b)) }
`
var caught, allowed int
ast.Inspect(parseExprs(t, src), func(n ast.Node) bool {
e, ok := n.(ast.Expr)
if !ok {
return true
}
c, ok := calls(e, "Checked")
if !ok || len(c.Args) != 1 {
return true
}
if isConstBool(c.Args[0]) {
caught++
} else {
allowed++
}
return true
})
require.Equal(t, 4, caught, "bare, qualified, negated and parenthesised literals must all be caught")
require.Equal(t, 2, allowed, "a computed predicate must pass")
}
func TestGateCatchesADiscardedVerdict(t *testing.T) {
const src = `package p
func a() { _ = verifyThing(x) }
func b() { verifyThing(x) }
func c() { _ = Checked(y) }
func d() { v := verifyThing(x); use(v) }
func e() { _ = harmless(x) }
func f() { _, _ = contract.Refused(nil), 1 }
`
var caught int
ast.Inspect(parseExprs(t, src), func(n ast.Node) bool {
switch s := n.(type) {
case *ast.AssignStmt:
for _, lhs := range s.Lhs {
if id, ok := lhs.(*ast.Ident); !ok || id.Name != "_" {
return true
}
}
for _, rhs := range s.Rhs {
if verdictShaped(rhs) {
caught++
}
}
case *ast.ExprStmt:
if verdictShaped(s.X) {
caught++
}
}
return true
})
require.Equal(t, 4, caught,
"discarded verify calls, discarded constructors and bare-statement calls must all be caught")
}
func TestGateIgnoresUnrelatedCode(t *testing.T) {
require.False(t, isConstBool(&ast.Ident{Name: "ok"}))
require.False(t, isConstBool(&ast.BasicLit{Kind: token.INT, Value: "1"}))
require.False(t, isConstBool(&ast.UnaryExpr{Op: token.SUB, X: &ast.Ident{Name: "true"}}))
_, ok := calls(&ast.Ident{Name: "Checked"}, "Checked")
require.False(t, ok, "a bare identifier is not a call")
_, ok = calls(&ast.CallExpr{Fun: &ast.SelectorExpr{
X: &ast.Ident{Name: "other"}, Sel: &ast.Ident{Name: "Checked"},
}}, "Checked")
require.False(t, ok, "another package's Checked is not ours")
_, ok = calls(&ast.CallExpr{Fun: &ast.ArrayType{}}, "Checked")
require.False(t, ok)
require.False(t, verdictShaped(&ast.Ident{Name: "x"}))
require.False(t, verdictShaped(&ast.CallExpr{Fun: &ast.ArrayType{}}))
require.False(t, verdictShaped(&ast.CallExpr{Fun: &ast.Ident{Name: "hash"}}))
}
+99 -99
View File
@@ -228,15 +228,26 @@ var PrecompileCtx = []byte("lux-evm-precompile-p3q-v1")
// ErrUnsupportedMode is returned by Verify, the in-process entry
// point, for a mode byte outside {0x44, 0x65, 0x87}.
//
// Run returns no sentinel of its own except contract.ErrOutOfGas.
// Run's own error return carries nothing but contract.ErrOutOfGas.
// Every verification failure — malformed input, unknown kind, unknown
// mode, length mismatch, FIPS 204 reject — is surfaced as
// `(out=[0x00…00], nil error)`, per LP-218 §"Failure modes": no revert
// on cryptographic failure, the Solidity caller observes `false` and
// decides whether that is fatal. Sentinels for those cases would be
// unreachable, and an unreachable sentinel is worse than none: a
// caller's errors.Is against it silently answers false forever.
var ErrUnsupportedMode = errors.New("p3q: unsupported ML-DSA mode (must be 0x44, 0x65, or 0x87)")
// decides whether that is fatal.
//
// The sentinels below never reach a Solidity caller either. They are
// what verify hands contract.Refused, and they exist because the ABI
// word cannot tell a check that failed from a check that never ran,
// while the code must: a reserved kind and a bad signature are the same
// byte on the wire and different facts in the verifier. Reading them
// back is what the refusal tests do.
var (
ErrUnsupportedMode = errors.New("p3q: unsupported ML-DSA mode (must be 0x44, 0x65, or 0x87)")
ErrKindReserved = errors.New("p3q: kind reserved, no verifier wired")
ErrUnknownKind = errors.New("p3q: kind names no primitive")
ErrSigSize = errors.New("p3q: declared signature length is not the mode's size")
ErrPubKeySize = errors.New("p3q: declared public key length is not the mode's size")
)
type p3qVerifyPrecompile struct{}
@@ -267,19 +278,9 @@ func modeParams(mode uint8) (m mldsa.Mode, pkSize, sigSize int, err error) {
}
}
// abiFalse returns a fresh 32-byte zero slice — the EVM-ABI encoding
// of `bool false`. Callers may mutate the return value.
func abiFalse() []byte { return make([]byte, 32) }
// abiTrueClone returns a fresh 32-byte slice with the last byte set
// to 0x01 — the EVM-ABI encoding of `bool true`. Callers may mutate
// the return value.
func abiTrueClone() []byte { b := make([]byte, 32); b[31] = 1; return b }
// Run verifies a P3Q-family signature per LP-218 / LP-220. Today only
// KindPulsar (FIPS 204 ML-DSA byte-equal output) is wired; KindCorona
// and KindMagnetar are reserved and return abiFalse until their
// verifiers land.
// and KindMagnetar are reserved and refuse until their verifiers land.
//
// Wire format (single-call interface, no function selector):
//
@@ -297,9 +298,12 @@ func abiTrueClone() []byte { b := make([]byte, 32); b[31] = 1; return b }
// Error returns:
// - contract.ErrOutOfGas when suppliedGas < RequiredGas(input).
//
// Verification failures (malformed input, mode mismatch, length
// mismatch, FIPS 204 reject) return `(abiFalse, gasLeft, nil)` — the
// LP-218 contract: no revert on cryptographic failure.
// Every other outcome — malformed input, unknown kind, unknown mode,
// length mismatch, FIPS 204 reject — is `(word, gasLeft, nil)` with the
// word false, per the LP-218 contract: no revert on cryptographic
// failure. The word is the verdict's own encoding, so the two cases
// the ABI collapses (a check that failed, a check that never ran) stay
// distinct inside this function even though the caller sees one byte.
func (p *p3qVerifyPrecompile) Run(
_ contract.AccessibleState,
_ common.Address,
@@ -313,103 +317,99 @@ func (p *p3qVerifyPrecompile) Run(
if err != nil {
return nil, 0, err
}
return verify(input).Word(), remaining, nil
}
// Structural validation — all on PUBLIC fields (kind byte + mode
// byte + length fields + total length). No data-dependent branches
// on signature or public-key contents.
if len(input) < MinInputLength {
return abiFalse(), remaining, nil
}
// verify parses the wire format and runs the FIPS 204 verifier.
//
// It reads through a contract.Cursor, so no bound in this function is
// written by hand and none can be deleted: an over-read is refused by
// the cursor rather than by a slice expression, which under a real
// precompile input would not refuse at all. See contract/cursor.go for
// why — the short version is that calldata's spare capacity belongs to
// the caller, so reaching past the declared end reads chosen bytes and
// returns a verdict over them.
//
// Every branch on kind, mode and the two length fields is on PUBLIC
// data. No branch here depends on signature or public-key contents,
// which is what keeps the FIPS 204 verifier's constant-time contract
// intact at the precompile layer.
func verify(input []byte) contract.Verdict {
in := contract.Read(input)
// Family-kind dispatch per LP-218 / LP-220 §"Single slot, kind-byte
// dispatch". Today only KindPulsar is wired; KindCorona / KindMagnetar
// reserve their slot and fall through to abiFalse. Adding their
// verifier paths is a follow-up that does not change the precompile
// surface — only the dispatch table grows.
kind := input[0]
// reserve their slot and refuse. Adding their verifier paths is a
// one-line change here and nowhere else.
kind, err := in.Byte()
if err != nil {
return contract.Refused(err)
}
switch kind {
case KindPulsar:
// fall through to the existing FIPS 204 ML-DSA verify path
// fall through to the FIPS 204 ML-DSA verify path
case KindCorona, KindMagnetar:
// Reserved kinds answer false under the LP-218 "no revert on
// cryptographic failure" contract, so the surface stays
// consistent while their verifiers land. The arm is named
// rather than folded into default so that adding a verifier
// is a one-line change here and nowhere else.
return abiFalse(), remaining, nil
return contract.Refused(ErrKindReserved)
default:
// Unknown kind — abiFalse, same as any malformed input.
return abiFalse(), remaining, nil
return contract.Refused(ErrUnknownKind)
}
mode := input[KindByte]
m, pkSize, sigSize, mErr := modeParams(mode)
if mErr != nil {
return abiFalse(), remaining, nil
mode, err := in.Byte()
if err != nil {
return contract.Refused(err)
}
m, pkSize, sigSize, err := modeParams(mode)
if err != nil {
return contract.Refused(err)
}
// Parse pulsarSig: [4 bytes len][N bytes sig].
off := KindByte + ModeByte
if len(input)-off < LengthFieldSize {
return abiFalse(), remaining, nil
// pulsarSig: [4 bytes len][N bytes sig]. The declared length must
// EQUAL the parameter set's size, not merely fit — a length that
// only fits would let a caller pad the field and shift every later
// offset, so the bytes the verifier sees are chosen by the length
// field rather than by the layout.
sigLen, err := in.Uint32()
if err != nil {
return contract.Refused(err)
}
sigLen := binary.BigEndian.Uint32(input[off : off+LengthFieldSize])
off += LengthFieldSize
if uint64(len(input)-off) < uint64(sigLen) {
return abiFalse(), remaining, nil
if uint64(sigLen) != uint64(sigSize) {
return contract.Refused(ErrSigSize)
}
if int(sigLen) != sigSize {
// Length mismatch against the mode's expected FIPS 204
// signature size. The FIPS 204 verifier would reject anyway;
// short-circuit here so we don't risk feeding a wrong-length
// slice to the underlying primitive.
return abiFalse(), remaining, nil
}
pulsarSig := input[off : off+int(sigLen)]
off += int(sigLen)
// Parse groupPubKey: [4 bytes len][M bytes pk].
if len(input)-off < LengthFieldSize {
return abiFalse(), remaining, nil
}
pkLen := binary.BigEndian.Uint32(input[off : off+LengthFieldSize])
off += LengthFieldSize
if uint64(len(input)-off) < uint64(pkLen) {
return abiFalse(), remaining, nil
}
if int(pkLen) != pkSize {
return abiFalse(), remaining, nil
}
groupPubKey := input[off : off+int(pkLen)]
off += int(pkLen)
// Parse messageHash: exactly 32 bytes at the tail. The wire layout
// places the hash AFTER the variable-length blobs so the offset is
// content-independent (parseable by reading only the length fields,
// no scanning).
if len(input)-off < MessageHashSize {
return abiFalse(), remaining, nil
}
messageHash := input[off : off+MessageHashSize]
// Trailing bytes (off + MessageHashSize < len(input)) are accepted
// to match the convention used by the sibling STARK-FRI precompile
// (precompile/starkfri/contract.go::TestStarkFRI_Fuzz_PaddedInput).
// Padding does not affect verification.
// Construct typed ML-DSA PublicKey. The mldsa package parses the
// FIPS 204 packed-pk byte layout into the runtime struct used by
// the verifier.
pk, perr := mldsa.PublicKeyFromBytes(groupPubKey, m)
if perr != nil {
return abiFalse(), remaining, nil
pulsarSig, err := in.Bytes(uint64(sigLen))
if err != nil {
return contract.Refused(err)
}
// FIPS 204 ML-DSA.Verify under the P3Q domain-separation context.
if !pk.VerifySignatureCtx(messageHash, pulsarSig, PrecompileCtx) {
return abiFalse(), remaining, nil
// groupPubKey: [4 bytes len][M bytes pk], same rule.
pkLen, err := in.Uint32()
if err != nil {
return contract.Refused(err)
}
return abiTrueClone(), remaining, nil
if uint64(pkLen) != uint64(pkSize) {
return contract.Refused(ErrPubKeySize)
}
groupPubKey, err := in.Bytes(uint64(pkLen))
if err != nil {
return contract.Refused(err)
}
// messageHash: exactly 32 bytes. The wire layout places it AFTER the
// variable-length blobs so its offset is content-independent —
// parseable by reading only the length fields, with no scanning.
messageHash, err := in.Bytes(MessageHashSize)
if err != nil {
return contract.Refused(err)
}
// Trailing bytes are accepted, matching the sibling STARK-FRI
// precompile. That is why in.End() is not called here: admitting
// padding is this format's documented behaviour, not an oversight.
pk, err := mldsa.PublicKeyFromBytes(groupPubKey, m)
if err != nil {
return contract.Refused(err)
}
return contract.Checked(pk.VerifySignatureCtx(messageHash, pulsarSig, PrecompileCtx))
}
// Verify is the in-process Pulsar verification entry-point for callers
+6 -6
View File
@@ -45,14 +45,14 @@ func TestP3Q_OutOfGas(t *testing.T) {
}
// TestP3Q_ABIBoolEncoding pins the 32-byte EVM-ABI bool output
// convention. abiTrueClone() must be 31 zero bytes + 0x01;
// convention. abiTrue() must be 31 zero bytes + 0x01;
// abiFalse() must be 32 zero bytes.
func TestP3Q_ABIBoolEncoding(t *testing.T) {
tr := abiTrueClone()
tr := abiTrue()
require.Len(t, tr, 32)
require.Equal(t, byte(0x01), tr[31])
for i := 0; i < 31; i++ {
require.Equal(t, byte(0x00), tr[i], "abiTrueClone byte %d", i)
require.Equal(t, byte(0x00), tr[i], "abiTrue byte %d", i)
}
fl := abiFalse()
require.Len(t, fl, 32)
@@ -222,7 +222,7 @@ func TestP3Q_RoundTrip_RealMLDSA(t *testing.T) {
supplied := gas * 2
out, gasLeft, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, supplied, true)
require.NoError(t, err)
require.Equal(t, abiTrueClone(), out, "precompile must return abiTrue for a real verifying signature")
require.Equal(t, abiTrue(), out, "precompile must return abiTrue for a real verifying signature")
require.Equal(t, supplied-gas, gasLeft, "gas accounting must be base + N*per-byte")
})
}
@@ -349,7 +349,7 @@ func TestP3Q_Verify_StandaloneAPI(t *testing.T) {
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
out, _, runErr := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas, true)
require.NoError(t, runErr)
require.Equal(t, abiTrueClone(), out)
require.Equal(t, abiTrue(), out)
}
// TestP3Q_Verify_SizeMismatchTyped confirms the standalone Verify
@@ -411,5 +411,5 @@ func TestP3Q_AcceptsTrailingPadding(t *testing.T) {
gas := P3QVerifyPrecompile.RequiredGas(padded) * 2
out, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, padded, gas, true)
require.NoError(t, err)
require.Equal(t, abiTrueClone(), out, "trailing padding must be ignored, signature must verify")
require.Equal(t, abiTrue(), out, "trailing padding must be ignored, signature must verify")
}
+30 -17
View File
@@ -8,29 +8,34 @@ import (
"testing"
"github.com/luxfi/geth/common"
"github.com/luxfi/precompile/contract"
"github.com/stretchr/testify/require"
)
// exact returns a copy of b whose capacity equals its length.
// exact returns b with caller-chosen bytes behind its declared end.
//
// This matters more than it looks. A Go slice expression s[a:b] panics
// only when b exceeds cap(s), not len(s) — so an input assembled with
// append, which leaves spare capacity, lets a MISSING bounds check
// read uninitialised bytes and carry on. The test then passes while
// the guard it was meant to exercise is absent. Calldata handed to a
// precompile by the EVM has no spare capacity, so the same input
// panics in production, and a panic in geth's precompile dispatch has
// no recover: it halts every validator processing the transaction.
// This matters more than it looks, and it is not what the name once
// promised. A precompile's input is the two-index slice
// Memory.GetPtr returns (instructions.go opCall), and nothing on the
// path to Run copies it: len is the size the caller declared and paid
// gas for, cap is the rest of EVM memory, and the caller filled that
// memory with MSTORE before making the call.
//
// Every refusal test below therefore submits an exact-capacity slice.
func exact(b []byte) []byte {
out := make([]byte, len(b))
copy(out, b)
return out
}
// So a Go slice expression s[a:b] reaching past len does NOT panic and
// does NOT halt a validator — it succeeds, returning bytes the attacker
// chose, and the verifier answers over material nobody declared. The
// failure is silent forgery, not a crash.
//
// A fixture built with append also has spare capacity, but zeroed or
// stale, so an over-read looks like a harmless run of zeros. Filling
// the spare region with a recognisable pattern is what makes an
// over-read visible as one. Every refusal test below submits such an
// input; contract.Poisoned holds the one definition.
func exact(b []byte) []byte { return contract.Poisoned(b, 256) }
// verdictExact is verdict() over an exact-capacity input, and it
// requires that no input can panic the precompile.
// verdictExact is verdict() over an input whose spare capacity holds
// attacker bytes, and it requires that no input can panic the
// precompile.
func verdictExact(t *testing.T, input []byte) bool {
t.Helper()
in := exact(input)
@@ -40,6 +45,14 @@ func verdictExact(t *testing.T, input []byte) bool {
return out
}
// abiTrue and abiFalse are the only two words Run can return: the
// EVM-ABI encoding of bool. Production builds them through
// contract.Verdict.Word, which is why they are spelled out here rather
// than imported — a test that expected the same helper the code under
// test uses would agree with it by construction and check nothing.
func abiTrue() []byte { w := make([]byte, 32); w[31] = 1; return w }
func abiFalse() []byte { return make([]byte, 32) }
func be32(v uint32) []byte {
var b [4]byte
binary.BigEndian.PutUint32(b[:], v)
+1 -1
View File
@@ -60,7 +60,7 @@ func TestP3Q_KAT_Accept(t *testing.T) {
gas := P3QVerifyPrecompile.RequiredGas(input) * 2
out, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, input, gas, true)
require.NoError(t, err)
require.Equal(t, abiTrueClone(), out, "frozen KAT vector must verify true on-chain")
require.Equal(t, abiTrue(), out, "frozen KAT vector must verify true on-chain")
}
// TestP3Q_KAT_TamperSig is the negative KAT: flipping a single bit
+1 -1
View File
@@ -58,7 +58,7 @@ func TestP3Q_WireFormat_KindByteMandatory(t *testing.T) {
gas := P3QVerifyPrecompile.RequiredGas(good) * 2
out, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, good, gas, true)
require.NoError(t, err)
require.Equal(t, abiTrueClone(), out, "canonical kind-first encoding must verify")
require.Equal(t, abiTrue(), out, "canonical kind-first encoding must verify")
// Kind-less (legacy / buggy Solidity) encoding of the SAME valid
// signature — must be rejected on the kind-dispatch default branch.