Files
zeekay 9cc8cc7bb7 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.
2026-08-28 11:47:04 -07:00

73 lines
2.9 KiB
Go

// Copyright (C) 2025-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package p3q
import (
"crypto/rand"
"encoding/binary"
"testing"
"github.com/luxfi/crypto/mldsa"
"github.com/luxfi/geth/common"
"github.com/stretchr/testify/require"
)
// legacyEncodeNoKind reproduces the pre-decomplection (kind-less) wire
// layout: [mode][sigLen][sig][pkLen][pk][hash]. This is what a Solidity
// P3QLib.encode() that forgot the leading kind byte would produce. The
// precompile MUST reject it (the mode byte 0x44/0x65/0x87 is not a valid
// kind, so kind dispatch hits the default branch → abiFalse).
func legacyEncodeNoKind(mode uint8, sig, pk, hash []byte) []byte {
out := make([]byte, 0, 1+4+len(sig)+4+len(pk)+len(hash))
out = append(out, mode)
var b4 [4]byte
binary.BigEndian.PutUint32(b4[:], uint32(len(sig)))
out = append(out, b4[:]...)
out = append(out, sig...)
binary.BigEndian.PutUint32(b4[:], uint32(len(pk)))
out = append(out, b4[:]...)
out = append(out, pk...)
out = append(out, hash...)
return out
}
// TestP3Q_WireFormat_KindByteMandatory is the regression guard for the
// canonical-encoder invariant: the Go EncodeInput and the Solidity
// P3QLib.encode() MUST both put the kind byte at offset 0. A kind-less
// (mode-first) encoding of an otherwise-valid signature MUST be rejected,
// while the correct kind-first encoding of the same signature verifies.
//
// This test exists because the Solidity mirror encoder previously omitted
// the kind byte, which would have made every on-chain verify return false
// even for valid signatures.
func TestP3Q_WireFormat_KindByteMandatory(t *testing.T) {
sk, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
require.NoError(t, err)
hash := make([]byte, MessageHashSize)
for i := range hash {
hash[i] = byte(i + 1)
}
sig, err := sk.SignCtx(rand.Reader, hash, PrecompileCtx)
require.NoError(t, err)
pk := sk.PublicKey.Bytes()
// Canonical (kind-first) encoding — must verify.
good := EncodeInput(ModeMLDSA65, sig, pk, hash)
require.Equal(t, KindPulsar, good[0], "canonical encoder must place KindPulsar at offset 0")
gas := P3QVerifyPrecompile.RequiredGas(good) * 2
out, _, err := P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, good, gas, true)
require.NoError(t, err)
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.
bad := legacyEncodeNoKind(ModeMLDSA65, sig, pk, hash)
require.Equal(t, ModeMLDSA65, bad[0], "kind-less encoding starts with the mode byte")
gas = P3QVerifyPrecompile.RequiredGas(bad) * 2
out, _, err = P3QVerifyPrecompile.Run(nil, common.Address{}, ContractP3QVerifyAddress, bad, gas, true)
require.NoError(t, err)
require.Equal(t, abiFalse(), out,
"kind-less encoding (mode byte read as kind) must be rejected — this is the Solidity-encoder bug guard")
}