mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
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.
73 lines
2.9 KiB
Go
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")
|
|
}
|