mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
crypto precompiles: unvalidated points, a price that could not be paid, and free calls
Across the curve and hash precompiles: decode refuses a point that is not on its curve or not canonically encoded, rather than handing an off-curve point to an arithmetic routine; pasta's inversion no longer treats a multiple of p as invertible, where ModInverse returns nil and leaves the divisor untouched; compute refuses a quoted price wider than 256 bits instead of returning it modulo, and enforces readOnly; poseidon's sponge fee grows with its input; hqc reports an unwired backend as itself rather than as a verification failure; and sr25519 pins that the cgo and non-cgo verifiers name the same backend and agree, which is a consensus split when they do not. Every path deducts gas before it works, so no call is free. KAT vectors accompany each.
This commit is contained in:
+33
-16
@@ -51,7 +51,6 @@ import (
|
||||
"errors"
|
||||
"math/big"
|
||||
|
||||
"github.com/consensys/gnark-crypto/ecc/bn254/fr"
|
||||
"github.com/consensys/gnark-crypto/ecc/bn254/twistededwards"
|
||||
|
||||
"github.com/luxfi/geth/common"
|
||||
@@ -131,14 +130,32 @@ func (p *babyJubJubPrecompile) Run(
|
||||
}
|
||||
}
|
||||
|
||||
func decodePoint(data []byte) (twistededwards.PointAffine, error) {
|
||||
// readPoint decodes (x, y) without asserting curve membership.
|
||||
//
|
||||
// Coordinates are elements of F_r, so each has exactly one 32-byte encoding.
|
||||
// A value >= r is refused rather than reduced: silent reduction admitted r
|
||||
// distinct encodings per point, so InCurve answered true for coordinates a
|
||||
// caller's own range check would reject, and two encodings of one point
|
||||
// produced one output -- a free collision for anything keying off calldata.
|
||||
func readPoint(data []byte) (twistededwards.PointAffine, error) {
|
||||
if len(data) < PointLen {
|
||||
return twistededwards.PointAffine{}, ErrInvalidInput
|
||||
}
|
||||
var x, y fr.Element
|
||||
x.SetBytes(data[:32])
|
||||
y.SetBytes(data[32:64])
|
||||
pt := twistededwards.PointAffine{X: x, Y: y}
|
||||
var pt twistededwards.PointAffine
|
||||
if err := pt.X.SetBytesCanonical(data[:32]); err != nil {
|
||||
return twistededwards.PointAffine{}, ErrInvalidInput
|
||||
}
|
||||
if err := pt.Y.SetBytesCanonical(data[32:64]); err != nil {
|
||||
return twistededwards.PointAffine{}, ErrInvalidInput
|
||||
}
|
||||
return pt, nil
|
||||
}
|
||||
|
||||
func decodePoint(data []byte) (twistededwards.PointAffine, error) {
|
||||
pt, err := readPoint(data)
|
||||
if err != nil {
|
||||
return twistededwards.PointAffine{}, err
|
||||
}
|
||||
if !pt.IsOnCurve() {
|
||||
return twistededwards.PointAffine{}, ErrNotOnCurve
|
||||
}
|
||||
@@ -178,21 +195,21 @@ func (p *babyJubJubPrecompile) scalarMul(data []byte, gas uint64) ([]byte, uint6
|
||||
if err != nil {
|
||||
return nil, gas, err
|
||||
}
|
||||
var s fr.Element
|
||||
s.SetBytes(data[PointLen : PointLen+32])
|
||||
sBig := s.BigInt(new(big.Int))
|
||||
pt.ScalarMultiplication(&pt, sBig)
|
||||
// The multiplier is an integer, not a field element. Routing it through
|
||||
// fr.Element reduced it modulo r, the BN254 field -- which is NOT the Baby
|
||||
// Jubjub group order (that is r/8's prime factor, 2736...041), so the answer
|
||||
// was (s mod r)*P instead of s*P for every s >= r. gnark's windowed ladder
|
||||
// takes any magnitude, so hand it the scalar untouched.
|
||||
s := new(big.Int).SetBytes(data[PointLen : PointLen+32])
|
||||
pt.ScalarMultiplication(&pt, s)
|
||||
return encodePoint(&pt), gas, nil
|
||||
}
|
||||
|
||||
func (p *babyJubJubPrecompile) inCurve(data []byte, gas uint64) ([]byte, uint64, error) {
|
||||
if len(data) < PointLen {
|
||||
return nil, gas, ErrInvalidInput
|
||||
pt, err := readPoint(data)
|
||||
if err != nil {
|
||||
return nil, gas, err
|
||||
}
|
||||
var x, y fr.Element
|
||||
x.SetBytes(data[:32])
|
||||
y.SetBytes(data[32:64])
|
||||
pt := twistededwards.PointAffine{X: x, Y: y}
|
||||
result := make([]byte, 32)
|
||||
if pt.IsOnCurve() {
|
||||
result[31] = 1
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package babyjubjub
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/consensys/gnark-crypto/ecc/bn254/twistededwards"
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/precompile/contract"
|
||||
"github.com/luxfi/precompile/modules"
|
||||
"github.com/luxfi/precompile/precompileconfig"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// subgroupOrder is the prime order of the Baby Jubjub subgroup generated by the
|
||||
// reduced-form base point: r/8 with the cofactor removed. Published in EIP-2494
|
||||
// and circomlib.
|
||||
func subgroupOrder() *big.Int {
|
||||
l, ok := new(big.Int).SetString(
|
||||
"2736030358979909402780800718157159386076813972158567259200215660948447373041", 10)
|
||||
if !ok {
|
||||
panic("bad subgroup order literal")
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func w32(v *big.Int) []byte {
|
||||
b := make([]byte, 32)
|
||||
v.FillBytes(b)
|
||||
return b
|
||||
}
|
||||
|
||||
func basePoint(tb testing.TB) (*big.Int, *big.Int) {
|
||||
tb.Helper()
|
||||
c := twistededwards.GetEdwardsCurve()
|
||||
return c.Base.X.BigInt(new(big.Int)), c.Base.Y.BigInt(new(big.Int))
|
||||
}
|
||||
|
||||
func pointWords(x, y *big.Int) []byte {
|
||||
return append(w32(x), w32(y)...)
|
||||
}
|
||||
|
||||
func invoke(tb testing.TB, op byte, args ...[]byte) ([]byte, uint64, error) {
|
||||
tb.Helper()
|
||||
in := []byte{op}
|
||||
for _, a := range args {
|
||||
in = append(in, a...)
|
||||
}
|
||||
return BabyJubJubPrecompile.Run(nil, common.Address{}, ContractAddress, in, 1<<22, true)
|
||||
}
|
||||
|
||||
func mulBy(tb testing.TB, pt []byte, s *big.Int) []byte {
|
||||
tb.Helper()
|
||||
out, _, err := invoke(tb, OpScalarMul, pt, w32(s))
|
||||
require.NoError(tb, err)
|
||||
return out
|
||||
}
|
||||
|
||||
// TestKAT_SubgroupOrder pins the published order of the base point. L*B is the
|
||||
// identity and (L+1)*B is B: two facts a wrong ladder cannot fake.
|
||||
func TestKAT_SubgroupOrder(t *testing.T) {
|
||||
bx, by := basePoint(t)
|
||||
B := pointWords(bx, by)
|
||||
l := subgroupOrder()
|
||||
|
||||
identity := pointWords(big.NewInt(0), big.NewInt(1))
|
||||
|
||||
require.Equal(t, identity, mulBy(t, B, l), "L*B must be the identity")
|
||||
require.Equal(t, B, mulBy(t, B, new(big.Int).Add(l, big.NewInt(1))), "(L+1)*B must be B")
|
||||
require.Equal(t, identity, mulBy(t, B, new(big.Int).Lsh(l, 1)), "2L*B must be the identity")
|
||||
|
||||
// (L-1)*B is -B = (r-x, y).
|
||||
negX := new(big.Int).Sub(rBN254, bx)
|
||||
require.Equal(t, pointWords(negX, by),
|
||||
mulBy(t, B, new(big.Int).Sub(l, big.NewInt(1))), "(L-1)*B must be -B")
|
||||
|
||||
// And -B + B closes the loop.
|
||||
sum, _, err := invoke(t, OpPointAdd, pointWords(negX, by), B)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, identity, sum)
|
||||
}
|
||||
|
||||
// TestScalarIsNotReducedModField is the regression for the defect this file was
|
||||
// written around: the multiplier used to be pushed through fr.Element, which
|
||||
// reduces modulo r -- the BN254 FIELD, not the Baby Jubjub group order. Every
|
||||
// scalar at or above r therefore produced (s mod r)*P instead of s*P.
|
||||
//
|
||||
// r*B is the sharpest witness. Correctly it is (r mod L)*B, a point far from
|
||||
// the identity; under the old reduction it was 0*B, the identity exactly.
|
||||
func TestScalarIsNotReducedModField(t *testing.T) {
|
||||
bx, by := basePoint(t)
|
||||
B := pointWords(bx, by)
|
||||
l := subgroupOrder()
|
||||
identity := pointWords(big.NewInt(0), big.NewInt(1))
|
||||
|
||||
rB := mulBy(t, B, rBN254)
|
||||
require.NotEqual(t, identity, rB,
|
||||
"r*B is not the identity: r is the field modulus, not the group order")
|
||||
require.Equal(t, mulBy(t, B, new(big.Int).Mod(rBN254, l)), rB,
|
||||
"r*B must equal (r mod L)*B")
|
||||
|
||||
// (r+1)*B must not collapse onto 1*B either.
|
||||
rPlus1 := new(big.Int).Add(rBN254, big.NewInt(1))
|
||||
require.NotEqual(t, B, mulBy(t, B, rPlus1),
|
||||
"(r+1)*B must not be B: that was the reduction bug")
|
||||
require.Equal(t, mulBy(t, B, new(big.Int).Mod(rPlus1, l)), mulBy(t, B, rPlus1))
|
||||
|
||||
// The identity s*P == (s mod L)*P must hold across the whole 32-byte range,
|
||||
// including the largest word there is.
|
||||
maxWord := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1))
|
||||
for _, s := range []*big.Int{
|
||||
new(big.Int).Sub(rBN254, big.NewInt(1)),
|
||||
new(big.Int).Add(rBN254, big.NewInt(7)),
|
||||
new(big.Int).Lsh(l, 3),
|
||||
maxWord,
|
||||
} {
|
||||
require.Equal(t, mulBy(t, B, new(big.Int).Mod(s, l)), mulBy(t, B, s),
|
||||
"s*P must equal (s mod L)*P for s = %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
// TestKAT_ScalarMulIsRepeatedAddition ties the ladder to the addition law:
|
||||
// k*B must equal B added to itself k times, for small k where both are cheap.
|
||||
func TestKAT_ScalarMulIsRepeatedAddition(t *testing.T) {
|
||||
bx, by := basePoint(t)
|
||||
B := pointWords(bx, by)
|
||||
|
||||
acc := pointWords(big.NewInt(0), big.NewInt(1))
|
||||
for k := 1; k <= 12; k++ {
|
||||
var err error
|
||||
acc, _, err = invoke(t, OpPointAdd, acc, B)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, acc, mulBy(t, B, big.NewInt(int64(k))), "%d*B", k)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNonCanonicalCoordinateRejected is the regression for the second defect
|
||||
// here: coordinates went through fr.Element.SetBytes, which reduces modulo r,
|
||||
// so x and x+r named the same point. InCurve answered true for a coordinate any
|
||||
// caller's own range check would reject, and two distinct calldata encodings
|
||||
// produced one output.
|
||||
func TestNonCanonicalCoordinateRejected(t *testing.T) {
|
||||
bx, by := basePoint(t)
|
||||
|
||||
ncX := new(big.Int).Add(bx, rBN254)
|
||||
ncY := new(big.Int).Add(by, rBN254)
|
||||
require.LessOrEqual(t, ncX.BitLen(), 256, "the non-canonical form must still fit a word")
|
||||
require.LessOrEqual(t, ncY.BitLen(), 256)
|
||||
|
||||
B := pointWords(bx, by)
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
pt []byte
|
||||
}{
|
||||
{"x + r", pointWords(ncX, by)},
|
||||
{"y + r", pointWords(bx, ncY)},
|
||||
{"both + r", pointWords(ncX, ncY)},
|
||||
{"x = r", pointWords(rBN254, by)},
|
||||
{"y = r", pointWords(bx, rBN254)},
|
||||
{"x = 2^256-1", pointWords(new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1)), by)},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
for _, op := range []struct {
|
||||
name string
|
||||
args [][]byte
|
||||
op byte
|
||||
}{
|
||||
{"inCurve", [][]byte{tc.pt}, OpInCurve},
|
||||
{"pointAdd first", [][]byte{tc.pt, B}, OpPointAdd},
|
||||
{"pointAdd second", [][]byte{B, tc.pt}, OpPointAdd},
|
||||
{"scalarMul", [][]byte{tc.pt, w32(big.NewInt(2))}, OpScalarMul},
|
||||
} {
|
||||
out, _, err := invoke(t, op.op, op.args...)
|
||||
require.ErrorIs(t, err, ErrInvalidInput,
|
||||
"%s must refuse a non-canonical coordinate, not reduce it", op.name)
|
||||
require.Nil(t, out)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// The canonical encoding of the same point is still accepted, so the
|
||||
// refusals above come from canonicality and not from a broken fixture.
|
||||
out, _, err := invoke(t, OpInCurve, B)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, byte(1), out[31])
|
||||
}
|
||||
|
||||
// TestRefusal_PointNotOnCurve is the invalid-curve refusal: a well-formed pair
|
||||
// of canonical coordinates that does not satisfy the curve equation.
|
||||
func TestRefusal_PointNotOnCurve(t *testing.T) {
|
||||
bx, by := basePoint(t)
|
||||
B := pointWords(bx, by)
|
||||
|
||||
offCurve := pointWords(bx, new(big.Int).Add(by, big.NewInt(1)))
|
||||
// Confirm the fixture really is off the curve before relying on it.
|
||||
res, _, err := invoke(t, OpInCurve, offCurve)
|
||||
require.NoError(t, err, "InCurve answers, it does not refuse")
|
||||
require.Equal(t, byte(0), res[31], "fixture must be off the curve")
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
args [][]byte
|
||||
op byte
|
||||
}{
|
||||
{"pointAdd first", [][]byte{offCurve, B}, OpPointAdd},
|
||||
{"pointAdd second", [][]byte{B, offCurve}, OpPointAdd},
|
||||
{"scalarMul", [][]byte{offCurve, w32(big.NewInt(3))}, OpScalarMul},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
out, _, err := invoke(t, tc.op, tc.args...)
|
||||
require.ErrorIs(t, err, ErrNotOnCurve)
|
||||
require.Nil(t, out)
|
||||
})
|
||||
}
|
||||
|
||||
// (0, 0) satisfies neither the curve equation nor the identity: a caller
|
||||
// spelling "point at infinity" the Weierstrass way must be refused.
|
||||
zero := make([]byte, PointLen)
|
||||
res, _, err = invoke(t, OpInCurve, zero)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, byte(0), res[31], "(0,0) is not on this twisted Edwards curve")
|
||||
|
||||
out, _, err := invoke(t, OpPointAdd, zero, B)
|
||||
require.ErrorIs(t, err, ErrNotOnCurve)
|
||||
require.Nil(t, out)
|
||||
}
|
||||
|
||||
// TestInCurveAcceptsTorsionPoints records the property a caller must guard for
|
||||
// itself. InCurve answers the curve equation, which the identity and the three
|
||||
// non-trivial cofactor points satisfy. A protocol that treats "InCurve == 1" as
|
||||
// "this is a valid public key" is open to a small-subgroup attack: multiplying
|
||||
// by a secret leaks that secret modulo 8.
|
||||
func TestInCurveAcceptsTorsionPoints(t *testing.T) {
|
||||
torsion := [][]byte{
|
||||
pointWords(big.NewInt(0), big.NewInt(1)), // identity, order 1
|
||||
pointWords(big.NewInt(0), new(big.Int).Sub(rBN254, big.NewInt(1))), // order 2
|
||||
}
|
||||
identity := pointWords(big.NewInt(0), big.NewInt(1))
|
||||
|
||||
for _, pt := range torsion {
|
||||
out, _, err := invoke(t, OpInCurve, pt)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, byte(1), out[31], "cofactor point satisfies the curve equation")
|
||||
|
||||
// Its order divides 8, so the eighth multiple is the identity -- the
|
||||
// check a caller has to perform itself.
|
||||
require.Equal(t, identity, mulBy(t, pt, big.NewInt(8)))
|
||||
}
|
||||
|
||||
// The base point is emphatically not torsion.
|
||||
bx, by := basePoint(t)
|
||||
require.NotEqual(t, identity, mulBy(t, pointWords(bx, by), big.NewInt(8)))
|
||||
}
|
||||
|
||||
// TestRefusal_Length walks every truncation and the unknown-opcode arm.
|
||||
func TestRefusal_Length(t *testing.T) {
|
||||
bx, by := basePoint(t)
|
||||
B := pointWords(bx, by)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
in []byte
|
||||
err error
|
||||
}{
|
||||
{"nil", nil, ErrInvalidInput},
|
||||
{"empty", []byte{}, ErrInvalidInput},
|
||||
{"pointAdd op only", []byte{OpPointAdd}, ErrInvalidInput},
|
||||
{"pointAdd one point", append([]byte{OpPointAdd}, B...), ErrInvalidInput},
|
||||
{"pointAdd one byte short", append([]byte{OpPointAdd}, append(append([]byte{}, B...), B[:PointLen-1]...)...), ErrInvalidInput},
|
||||
{"scalarMul op only", []byte{OpScalarMul}, ErrInvalidInput},
|
||||
{"scalarMul no scalar", append([]byte{OpScalarMul}, B...), ErrInvalidInput},
|
||||
{"scalarMul one byte short", append([]byte{OpScalarMul}, append(append([]byte{}, B...), make([]byte, 31)...)...), ErrInvalidInput},
|
||||
{"inCurve op only", []byte{OpInCurve}, ErrInvalidInput},
|
||||
{"inCurve one byte short", append([]byte{OpInCurve}, B[:PointLen-1]...), ErrInvalidInput},
|
||||
{"unknown op 0x00", []byte{0x00}, ErrInvalidOp},
|
||||
{"unknown op 0x04", []byte{0x04}, ErrInvalidOp},
|
||||
{"unknown op 0xff", append([]byte{0xFF}, B...), ErrInvalidOp},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
out, _, err := BabyJubJubPrecompile.Run(nil, common.Address{}, ContractAddress, tc.in, 1<<22, true)
|
||||
require.ErrorIs(t, err, tc.err)
|
||||
require.Nil(t, out)
|
||||
})
|
||||
}
|
||||
|
||||
// Trailing data past the fixed operand width is ignored, not refused.
|
||||
short, _, err := invoke(t, OpInCurve, B)
|
||||
require.NoError(t, err)
|
||||
long, _, err := invoke(t, OpInCurve, B, []byte{0xAA, 0xBB})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, short, long)
|
||||
}
|
||||
|
||||
// TestGas_Schedule covers every arm and proves the zero-priced arms buy nothing.
|
||||
func TestGas_Schedule(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
in []byte
|
||||
want uint64
|
||||
}{
|
||||
{"nil", nil, 0},
|
||||
{"empty", []byte{}, 0},
|
||||
{"pointAdd", []byte{OpPointAdd}, GasPointAdd},
|
||||
{"scalarMul", []byte{OpScalarMul}, GasScalarMul},
|
||||
{"inCurve", []byte{OpInCurve}, GasInCurve},
|
||||
{"unknown 0x00", []byte{0x00}, 0},
|
||||
{"unknown 0x04", []byte{0x04}, 0},
|
||||
{"unknown 0xff", []byte{0xFF}, 0},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Equal(t, tc.want, BabyJubJubPrecompile.RequiredGas(tc.in))
|
||||
})
|
||||
}
|
||||
|
||||
// Every operand is fixed width, so the price must not move with input size.
|
||||
require.Equal(t, uint64(GasScalarMul),
|
||||
BabyJubJubPrecompile.RequiredGas(append([]byte{OpScalarMul}, make([]byte, 1<<16)...)))
|
||||
|
||||
// Scalar multiplication is the expensive arm and must be priced above the
|
||||
// membership test, which does no group arithmetic at all.
|
||||
require.Greater(t, uint64(GasScalarMul), uint64(GasPointAdd))
|
||||
require.Greater(t, uint64(GasPointAdd), uint64(GasInCurve))
|
||||
|
||||
for _, in := range [][]byte{nil, {}, {0x00}, {0x04}, {0xFF}} {
|
||||
require.Zero(t, BabyJubJubPrecompile.RequiredGas(in))
|
||||
out, _, err := BabyJubJubPrecompile.Run(nil, common.Address{}, ContractAddress, in, 0, true)
|
||||
require.Error(t, err, "a zero-gas input must not execute")
|
||||
require.Nil(t, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGas_Deduction(t *testing.T) {
|
||||
bx, by := basePoint(t)
|
||||
in := append([]byte{OpInCurve}, pointWords(bx, by)...)
|
||||
|
||||
_, left, err := BabyJubJubPrecompile.Run(nil, common.Address{}, ContractAddress, in, GasInCurve+21, true)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(21), left)
|
||||
|
||||
_, left, err = BabyJubJubPrecompile.Run(nil, common.Address{}, ContractAddress, in, GasInCurve, true)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, left)
|
||||
|
||||
_, left, err = BabyJubJubPrecompile.Run(nil, common.Address{}, ContractAddress, in, GasInCurve-1, true)
|
||||
require.ErrorIs(t, err, contract.ErrOutOfGas)
|
||||
require.Zero(t, left)
|
||||
}
|
||||
|
||||
// --- module + config ----------------------------------------------------
|
||||
|
||||
func TestModule_Wiring(t *testing.T) {
|
||||
require.Equal(t, ConfigKey, Module.ConfigKey)
|
||||
require.Equal(t, ContractAddress, Module.Address)
|
||||
require.Same(t, BabyJubJubPrecompile, Module.Contract)
|
||||
require.NotNil(t, Module.Configurator)
|
||||
require.Equal(t,
|
||||
common.HexToAddress("0x0500000000000000000000000000000000000007"),
|
||||
ContractAddress)
|
||||
}
|
||||
|
||||
func TestConfigurator_Surface(t *testing.T) {
|
||||
c := &configurator{}
|
||||
require.IsType(t, &Config{}, c.MakeConfig())
|
||||
require.NoError(t, c.Configure(nil, nil, nil, nil))
|
||||
}
|
||||
|
||||
func TestConfig_Surface(t *testing.T) {
|
||||
c := &Config{}
|
||||
require.Equal(t, ConfigKey, c.Key())
|
||||
require.Nil(t, c.Timestamp())
|
||||
require.False(t, c.IsDisabled())
|
||||
require.NoError(t, c.Verify(nil))
|
||||
|
||||
ts := uint64(21)
|
||||
a := &Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts}}
|
||||
require.Equal(t, &ts, a.Timestamp())
|
||||
require.True(t, a.Equal(&Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts}}))
|
||||
|
||||
other := uint64(22)
|
||||
require.False(t, a.Equal(&Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &other}}))
|
||||
require.False(t, a.Equal(nil))
|
||||
require.True(t, (&Config{Upgrade: precompileconfig.Upgrade{Disable: true}}).IsDisabled())
|
||||
}
|
||||
|
||||
func TestRegisterModuleIsIdempotentlyRefused(t *testing.T) {
|
||||
require.Error(t, modules.RegisterModule(Module))
|
||||
}
|
||||
+25
-2
@@ -47,6 +47,7 @@ var (
|
||||
ErrJobNotFound = errors.New("job not found")
|
||||
ErrJobAlreadyClaimed = errors.New("job already claimed")
|
||||
ErrReadOnlyState = errors.New("state modification in read-only context")
|
||||
ErrPriceOverflow = errors.New("quoted price exceeds 256 bits")
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -65,6 +66,9 @@ const (
|
||||
StatusOpen = 0
|
||||
StatusClaimed = 1
|
||||
StatusVerified = 2
|
||||
|
||||
// pricePerByte is the base quote rate, in wei per byte of input.
|
||||
pricePerByte = 1000
|
||||
)
|
||||
|
||||
type computeMarketPrecompile struct{}
|
||||
@@ -116,6 +120,14 @@ func (p *computeMarketPrecompile) Run(
|
||||
return claimReward(accessibleState, caller, data, gas)
|
||||
|
||||
case OpVerifyCompute:
|
||||
// verifyCompute flips a job's status to verified, so it is a writer and
|
||||
// must refuse a read-only context like every other writer here. Without
|
||||
// this, a STATICCALL mutates state: the EVM guarantees a static frame
|
||||
// makes no state change, and a precompile that writes anyway leaves the
|
||||
// caller's snapshot disagreeing with the world.
|
||||
if readOnly {
|
||||
return nil, suppliedGas, ErrReadOnlyState
|
||||
}
|
||||
gas, err := contract.DeductGas(suppliedGas, GasVerify)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
@@ -273,7 +285,12 @@ func verifyCompute(state contract.AccessibleState, data []byte, gas uint64) ([]b
|
||||
|
||||
// getPrice: data = [modelHash (32)] [inputSize (32)]
|
||||
// Returns: price in wei (32 bytes)
|
||||
// Base price = 1000 wei per input byte, scaled by model complexity
|
||||
// Base price = 1000 wei per input byte.
|
||||
//
|
||||
// inputSize comes straight from calldata, so it can be any 256-bit value. A
|
||||
// price that does not fit in 256 bits is refused rather than returned modulo
|
||||
// 2^256: wrapping would let a caller choose an inputSize whose truncated price
|
||||
// is arbitrarily small, quoting a near-zero price for unbounded work.
|
||||
func getPrice(data []byte, gas uint64) ([]byte, uint64, error) {
|
||||
if len(data) < 64 {
|
||||
return nil, gas, ErrInvalidInput
|
||||
@@ -281,7 +298,10 @@ func getPrice(data []byte, gas uint64) ([]byte, uint64, error) {
|
||||
inputSize := new(big.Int).SetBytes(data[32:64])
|
||||
|
||||
// Price = inputSize * 1000 (base rate per byte)
|
||||
price := new(big.Int).Mul(inputSize, big.NewInt(1000))
|
||||
price := new(big.Int).Mul(inputSize, big.NewInt(pricePerByte))
|
||||
if price.BitLen() > 256 {
|
||||
return nil, gas, ErrPriceOverflow
|
||||
}
|
||||
return padTo32(price.Bytes()), gas, nil
|
||||
}
|
||||
|
||||
@@ -290,6 +310,9 @@ func storageSlot(prefix string, key []byte) common.Hash {
|
||||
return common.BytesToHash(crypto.Keccak256([]byte(prefix), key))
|
||||
}
|
||||
|
||||
// padTo32 left-pads a big-endian value to 32 bytes. A longer value is reduced
|
||||
// to its low 32 bytes, which is the modular behaviour a nonce wants; every
|
||||
// caller that must not wrap checks its own range first (see getPrice).
|
||||
func padTo32(b []byte) []byte {
|
||||
if len(b) >= 32 {
|
||||
return b[len(b)-32:]
|
||||
|
||||
@@ -0,0 +1,601 @@
|
||||
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package compute
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/crypto"
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/precompile/contract"
|
||||
"github.com/luxfi/precompile/modules"
|
||||
"github.com/luxfi/precompile/precompileconfig"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var caller3 = common.HexToAddress("0x3333333333333333333333333333333333333333")
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func run(t *testing.T, st contract.AccessibleState, from common.Address, op byte, body []byte, gas uint64, readOnly bool) ([]byte, uint64, error) {
|
||||
t.Helper()
|
||||
return Precompile.Run(st, from, ContractAddress, append([]byte{op}, body...), gas, readOnly)
|
||||
}
|
||||
|
||||
func register(t *testing.T, st contract.AccessibleState, from common.Address) []byte {
|
||||
t.Helper()
|
||||
body := make([]byte, 64)
|
||||
copy(body[:32], padTo32([]byte("H100")))
|
||||
id, _, err := run(t, st, from, OpRegisterProvider, body, 1_000_000, false)
|
||||
require.NoError(t, err)
|
||||
return id
|
||||
}
|
||||
|
||||
func submit(t *testing.T, st contract.AccessibleState, from common.Address, model string) []byte {
|
||||
t.Helper()
|
||||
body := make([]byte, 96)
|
||||
copy(body[:32], padTo32([]byte(model)))
|
||||
copy(body[32:64], padTo32([]byte("input")))
|
||||
copy(body[64:96], padTo32(big.NewInt(500).Bytes()))
|
||||
id, _, err := run(t, st, from, OpSubmitJob, body, 1_000_000, false)
|
||||
require.NoError(t, err)
|
||||
return id
|
||||
}
|
||||
|
||||
func claim(t *testing.T, st contract.AccessibleState, from common.Address, jobID, outputHash []byte) error {
|
||||
t.Helper()
|
||||
body := make([]byte, 0, 64)
|
||||
body = append(body, jobID...)
|
||||
body = append(body, outputHash...)
|
||||
_, _, err := run(t, st, from, OpClaimReward, body, 1_000_000, false)
|
||||
return err
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// readOnly: every state-mutating op must refuse a static frame
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// TestEveryWriterRefusesReadOnly covers all four mutating opcodes. Before this
|
||||
// test, OpVerifyCompute had no readOnly check and wrote job.status from a
|
||||
// STATICCALL — a state change inside a frame the EVM promises is inert.
|
||||
func TestEveryWriterRefusesReadOnly(t *testing.T) {
|
||||
writers := []struct {
|
||||
name string
|
||||
op byte
|
||||
body []byte
|
||||
}{
|
||||
{"registerProvider", OpRegisterProvider, make([]byte, 64)},
|
||||
{"submitJob", OpSubmitJob, make([]byte, 96)},
|
||||
{"claimReward", OpClaimReward, make([]byte, 64)},
|
||||
{"verifyCompute", OpVerifyCompute, make([]byte, 64)},
|
||||
}
|
||||
|
||||
for _, w := range writers {
|
||||
t.Run(w.name, func(t *testing.T) {
|
||||
st := newMockState()
|
||||
_, remaining, err := run(t, st, caller1, w.op, w.body, 1_000_000, true)
|
||||
require.ErrorIs(t, err, ErrReadOnlyState)
|
||||
require.Equal(t, uint64(1_000_000), remaining)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestReadOnlyVerifyLeavesStateUntouched is the assertion that matters: the
|
||||
// refusal must happen BEFORE any SetState, not merely be reported afterwards.
|
||||
func TestReadOnlyVerifyLeavesStateUntouched(t *testing.T) {
|
||||
st := newMockState()
|
||||
|
||||
register(t, st, caller2)
|
||||
jobID := submit(t, st, caller1, "model")
|
||||
outputHash := crypto.Keccak256([]byte("the output"))
|
||||
require.NoError(t, claim(t, st, caller2, jobID, outputHash))
|
||||
|
||||
db := st.GetStateDB()
|
||||
statusSlot := storageSlot("job.status", jobID)
|
||||
before := db.GetState(ContractAddress, statusSlot)
|
||||
require.Equal(t, common.BytesToHash([]byte{StatusClaimed}), before)
|
||||
|
||||
// A correct attestation, submitted from a static frame.
|
||||
att := crypto.Keccak256(jobID, outputHash)
|
||||
_, _, err := run(t, st, caller1, OpVerifyCompute, append(append([]byte{}, jobID...), att...), 1_000_000, true)
|
||||
require.ErrorIs(t, err, ErrReadOnlyState)
|
||||
|
||||
require.Equal(t, before, db.GetState(ContractAddress, statusSlot),
|
||||
"a read-only call must not have advanced the job status")
|
||||
}
|
||||
|
||||
// TestGetPriceIsAllowedReadOnly — a pure quote has no business being refused in
|
||||
// a static frame; the readOnly gate must be on the writers only.
|
||||
func TestGetPriceIsAllowedReadOnly(t *testing.T) {
|
||||
st := newMockState()
|
||||
body := make([]byte, 64)
|
||||
copy(body[32:64], padTo32(big.NewInt(7).Bytes()))
|
||||
ret, _, err := run(t, st, caller1, OpGetPrice, body, 100_000, true)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, big.NewInt(7000), new(big.Int).SetBytes(ret))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getPrice arithmetic
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestGetPriceRefusesOverflowInsteadOfWrapping(t *testing.T) {
|
||||
// inputSize is 256 bits of attacker-chosen calldata. Multiplying by 1000
|
||||
// overflows, and truncating to the low 32 bytes quotes a price modulo
|
||||
// 2^256 — for the value below, a price of ZERO for a maximal input.
|
||||
st := newMockState()
|
||||
twoTo256 := new(big.Int).Lsh(big.NewInt(1), 256)
|
||||
|
||||
// 1000 = 2^3 * 125, so 2^253 * 1000 = 125 * 2^256, which is exactly zero
|
||||
// modulo 2^256. Truncating to the low 32 bytes therefore quotes a price of
|
||||
// ZERO for an input of 2^253 bytes. This is the fixture that proves the
|
||||
// wrap is exploitable, not merely inaccurate.
|
||||
wrapToZero := new(big.Int).Lsh(big.NewInt(1), 253)
|
||||
require.Zero(t,
|
||||
new(big.Int).Mod(new(big.Int).Mul(wrapToZero, big.NewInt(pricePerByte)), twoTo256).Sign(),
|
||||
"fixture must wrap to exactly zero")
|
||||
|
||||
for name, inputSize := range map[string]*big.Int{
|
||||
"exact wrap to zero": wrapToZero,
|
||||
"max uint256": new(big.Int).Sub(twoTo256, big.NewInt(1)),
|
||||
"one past the limit": new(big.Int).Add(new(big.Int).Div(new(big.Int).Sub(twoTo256, big.NewInt(1)), big.NewInt(pricePerByte)), big.NewInt(1)),
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
body := make([]byte, 64)
|
||||
copy(body[32:64], padTo32(inputSize.Bytes()))
|
||||
ret, _, err := run(t, st, caller1, OpGetPrice, body, 100_000, false)
|
||||
require.ErrorIs(t, err, ErrPriceOverflow,
|
||||
"an unrepresentable price must be refused, never wrapped")
|
||||
require.Nil(t, ret)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPriceAcceptsTheLargestRepresentablePrice(t *testing.T) {
|
||||
// One below the overflow point must still quote. An off-by-one in the
|
||||
// BitLen check would reject legitimate large jobs.
|
||||
st := newMockState()
|
||||
twoTo256 := new(big.Int).Lsh(big.NewInt(1), 256)
|
||||
maxInput := new(big.Int).Div(new(big.Int).Sub(twoTo256, big.NewInt(1)), big.NewInt(pricePerByte))
|
||||
|
||||
body := make([]byte, 64)
|
||||
copy(body[32:64], padTo32(maxInput.Bytes()))
|
||||
ret, _, err := run(t, st, caller1, OpGetPrice, body, 100_000, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
want := new(big.Int).Mul(maxInput, big.NewInt(pricePerByte))
|
||||
require.Equal(t, want, new(big.Int).SetBytes(ret))
|
||||
require.LessOrEqual(t, want.BitLen(), 256)
|
||||
}
|
||||
|
||||
func TestGetPriceIsLinearAndZeroAtZero(t *testing.T) {
|
||||
st := newMockState()
|
||||
quote := func(n int64) *big.Int {
|
||||
body := make([]byte, 64)
|
||||
copy(body[32:64], padTo32(big.NewInt(n).Bytes()))
|
||||
ret, _, err := run(t, st, caller1, OpGetPrice, body, 100_000, false)
|
||||
require.NoError(t, err)
|
||||
return new(big.Int).SetBytes(ret)
|
||||
}
|
||||
require.Zero(t, quote(0).Sign(), "zero bytes must quote zero")
|
||||
require.Zero(t, quote(1).Cmp(big.NewInt(pricePerByte)))
|
||||
require.Zero(t, quote(2).Cmp(big.NewInt(2*pricePerByte)))
|
||||
// Doubling the input doubles the price — a rate constant folded into the
|
||||
// wrong place would break this while leaving quote(1) correct.
|
||||
require.Zero(t, quote(2000).Cmp(new(big.Int).Mul(quote(1000), big.NewInt(2))))
|
||||
}
|
||||
|
||||
func TestGetPriceIgnoresModelHash(t *testing.T) {
|
||||
// The quote is documented as inputSize * rate. If a model term were ever
|
||||
// added, this test says so rather than the price silently changing.
|
||||
st := newMockState()
|
||||
price := func(model string) *big.Int {
|
||||
body := make([]byte, 64)
|
||||
copy(body[:32], padTo32([]byte(model)))
|
||||
copy(body[32:64], padTo32(big.NewInt(42).Bytes()))
|
||||
ret, _, err := run(t, st, caller1, OpGetPrice, body, 100_000, false)
|
||||
require.NoError(t, err)
|
||||
return new(big.Int).SetBytes(ret)
|
||||
}
|
||||
require.Equal(t, price("llama"), price("gpt"))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Input length refusals
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestEveryOpRefusesShortInput(t *testing.T) {
|
||||
// Each handler slices fixed offsets out of data. One byte short must be a
|
||||
// clean refusal, never a slice panic and never a zero-filled read.
|
||||
cases := []struct {
|
||||
name string
|
||||
op byte
|
||||
need int
|
||||
}{
|
||||
{"registerProvider", OpRegisterProvider, 64},
|
||||
{"submitJob", OpSubmitJob, 96},
|
||||
{"claimReward", OpClaimReward, 64},
|
||||
{"verifyCompute", OpVerifyCompute, 64},
|
||||
{"getPrice", OpGetPrice, 64},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
st := newMockState()
|
||||
for _, n := range []int{0, 1, c.need / 2, c.need - 1} {
|
||||
_, _, err := run(t, st, caller1, c.op, make([]byte, n), 1_000_000, false)
|
||||
require.ErrorIs(t, err, ErrInvalidInput, "%d bytes must be refused", n)
|
||||
}
|
||||
// Exactly the required length is accepted (or fails for a reason
|
||||
// other than length).
|
||||
_, _, err := run(t, st, caller1, c.op, make([]byte, c.need), 1_000_000, false)
|
||||
require.NotErrorIs(t, err, ErrInvalidInput, "exactly %d bytes must not be a length error", c.need)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtraTrailingBytesAreIgnored(t *testing.T) {
|
||||
// Handlers read a prefix. Trailing bytes must not change the outcome, or
|
||||
// two encodings of one call would produce two different jobs.
|
||||
st := newMockState()
|
||||
body := make([]byte, 96)
|
||||
copy(body[:32], padTo32([]byte("model")))
|
||||
copy(body[64:96], padTo32(big.NewInt(1).Bytes()))
|
||||
|
||||
a, _, err := run(t, st, caller1, OpSubmitJob, body, 1_000_000, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
st2 := newMockState()
|
||||
b, _, err := run(t, st2, caller1, OpSubmitJob, append(body, 0xde, 0xad, 0xbe, 0xef), 1_000_000, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, a, b, "trailing bytes changed the jobID")
|
||||
}
|
||||
|
||||
func TestEmptyInputAndUnknownOp(t *testing.T) {
|
||||
st := newMockState()
|
||||
|
||||
_, remaining, err := Precompile.Run(st, caller1, ContractAddress, nil, 500, false)
|
||||
require.ErrorIs(t, err, ErrInvalidInput)
|
||||
require.Equal(t, uint64(500), remaining)
|
||||
|
||||
for _, op := range []byte{0x00, 0x06, 0x7f, 0xff} {
|
||||
_, _, err := run(t, st, caller1, op, make([]byte, 96), 1_000_000, false)
|
||||
require.ErrorIs(t, err, ErrUnknownOp, "op %#x must be unknown", op)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Gas
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestEveryOpChargesItsFeeAndRefusesBelowIt(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
op byte
|
||||
need int
|
||||
fee uint64
|
||||
}{
|
||||
{"registerProvider", OpRegisterProvider, 64, GasRegister},
|
||||
{"submitJob", OpSubmitJob, 96, GasSubmit},
|
||||
{"claimReward", OpClaimReward, 64, GasClaim},
|
||||
{"verifyCompute", OpVerifyCompute, 64, GasVerify},
|
||||
{"getPrice", OpGetPrice, 64, GasGetPrice},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
st := newMockState()
|
||||
|
||||
// One gas short of the fee is out-of-gas with nothing left.
|
||||
_, remaining, err := run(t, st, caller1, c.op, make([]byte, c.need), c.fee-1, false)
|
||||
require.ErrorIs(t, err, contract.ErrOutOfGas)
|
||||
require.Zero(t, remaining, "an out-of-gas refusal must not report gas remaining")
|
||||
|
||||
// Exactly the fee is enough, and it is all consumed.
|
||||
_, remaining, _ = run(t, st, caller1, c.op, make([]byte, c.need), c.fee, false)
|
||||
require.Zero(t, remaining)
|
||||
|
||||
// A surplus is returned intact.
|
||||
_, remaining, _ = run(t, st, caller1, c.op, make([]byte, c.need), c.fee+777, false)
|
||||
require.Equal(t, uint64(777), remaining)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGasIsCheckedBeforeAnyWork(t *testing.T) {
|
||||
// An op that cannot pay must leave no trace, even with otherwise valid
|
||||
// arguments — otherwise a caller gets a free registration by underpaying.
|
||||
st := newMockState()
|
||||
body := make([]byte, 64)
|
||||
copy(body[:32], padTo32([]byte("H100")))
|
||||
|
||||
_, _, err := run(t, st, caller1, OpRegisterProvider, body, GasRegister-1, false)
|
||||
require.ErrorIs(t, err, contract.ErrOutOfGas)
|
||||
|
||||
db := st.GetStateDB()
|
||||
require.Equal(t, common.Hash{}, db.GetState(ContractAddress, storageSlot("nonce", caller1.Bytes())),
|
||||
"an unpaid call incremented the caller's nonce")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Job lifecycle refusals
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestClaimRefusesUnknownJob(t *testing.T) {
|
||||
st := newMockState()
|
||||
register(t, st, caller2)
|
||||
err := claim(t, st, caller2, crypto.Keccak256([]byte("no such job")), crypto.Keccak256([]byte("out")))
|
||||
require.ErrorIs(t, err, ErrJobNotFound)
|
||||
}
|
||||
|
||||
func TestClaimRefusesUnregisteredProvider(t *testing.T) {
|
||||
// caller3 never registered, so it has no provider nonce.
|
||||
st := newMockState()
|
||||
jobID := submit(t, st, caller1, "model")
|
||||
err := claim(t, st, caller3, jobID, crypto.Keccak256([]byte("out")))
|
||||
require.ErrorIs(t, err, ErrNotProvider)
|
||||
|
||||
// And the job is untouched — a refused claim must not record an output.
|
||||
db := st.GetStateDB()
|
||||
require.Equal(t, common.Hash{}, db.GetState(ContractAddress, storageSlot("job.output", jobID)))
|
||||
require.Equal(t, common.Hash{}, db.GetState(ContractAddress, storageSlot("job.status", jobID)))
|
||||
}
|
||||
|
||||
func TestClaimRefusesSecondClaim(t *testing.T) {
|
||||
st := newMockState()
|
||||
register(t, st, caller2)
|
||||
register(t, st, caller3)
|
||||
jobID := submit(t, st, caller1, "model")
|
||||
|
||||
require.NoError(t, claim(t, st, caller2, jobID, crypto.Keccak256([]byte("first"))))
|
||||
|
||||
// A different registered provider cannot take the job away.
|
||||
require.ErrorIs(t, claim(t, st, caller3, jobID, crypto.Keccak256([]byte("second"))), ErrJobAlreadyClaimed)
|
||||
|
||||
db := st.GetStateDB()
|
||||
require.Equal(t, common.BytesToHash(caller2.Bytes()),
|
||||
db.GetState(ContractAddress, storageSlot("job.provider", jobID)),
|
||||
"the second claim overwrote the provider")
|
||||
require.Equal(t, common.BytesToHash(crypto.Keccak256([]byte("first"))),
|
||||
db.GetState(ContractAddress, storageSlot("job.output", jobID)))
|
||||
}
|
||||
|
||||
func TestVerifyRefusesUnclaimedJobWithoutWriting(t *testing.T) {
|
||||
st := newMockState()
|
||||
jobID := submit(t, st, caller1, "model")
|
||||
|
||||
// An open job cannot be verified: the result is a zero word, not an error,
|
||||
// and nothing is written.
|
||||
att := crypto.Keccak256(jobID, make([]byte, 32))
|
||||
ret, _, err := run(t, st, caller1, OpVerifyCompute, append(append([]byte{}, jobID...), att...), 1_000_000, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, make([]byte, 32), ret)
|
||||
|
||||
db := st.GetStateDB()
|
||||
require.Equal(t, common.Hash{}, db.GetState(ContractAddress, storageSlot("job.status", jobID)))
|
||||
}
|
||||
|
||||
func TestVerifyRefusesWrongAttestationWithoutWriting(t *testing.T) {
|
||||
st := newMockState()
|
||||
register(t, st, caller2)
|
||||
jobID := submit(t, st, caller1, "model")
|
||||
outputHash := crypto.Keccak256([]byte("real output"))
|
||||
require.NoError(t, claim(t, st, caller2, jobID, outputHash))
|
||||
|
||||
db := st.GetStateDB()
|
||||
for _, wrong := range [][]byte{
|
||||
make([]byte, 32),
|
||||
crypto.Keccak256([]byte("guess")),
|
||||
crypto.Keccak256(outputHash, jobID), // arguments transposed
|
||||
crypto.Keccak256(jobID), // outputHash omitted
|
||||
} {
|
||||
ret, _, err := run(t, st, caller1, OpVerifyCompute,
|
||||
append(append([]byte{}, jobID...), wrong...), 1_000_000, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, make([]byte, 32), ret, "a wrong attestation must report failure")
|
||||
require.Equal(t, common.BytesToHash([]byte{StatusClaimed}),
|
||||
db.GetState(ContractAddress, storageSlot("job.status", jobID)),
|
||||
"a wrong attestation advanced the status")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyIsNotReplayable(t *testing.T) {
|
||||
// Once verified, the same call must not re-verify: status is no longer
|
||||
// StatusClaimed, so the guard short-circuits.
|
||||
st := newMockState()
|
||||
register(t, st, caller2)
|
||||
jobID := submit(t, st, caller1, "model")
|
||||
outputHash := crypto.Keccak256([]byte("out"))
|
||||
require.NoError(t, claim(t, st, caller2, jobID, outputHash))
|
||||
|
||||
body := append(append([]byte{}, jobID...), crypto.Keccak256(jobID, outputHash)...)
|
||||
ret, _, err := run(t, st, caller1, OpVerifyCompute, body, 1_000_000, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, byte(1), ret[31])
|
||||
|
||||
ret, _, err = run(t, st, caller1, OpVerifyCompute, body, 1_000_000, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, make([]byte, 32), ret, "a verified job must not verify twice")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Identifier derivation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestProviderAndJobIdsAreUniquePerCallerAndNonce(t *testing.T) {
|
||||
st := newMockState()
|
||||
|
||||
// Same caller, successive registrations → different ids.
|
||||
a := register(t, st, caller1)
|
||||
b := register(t, st, caller1)
|
||||
require.NotEqual(t, a, b, "a second registration reused the first providerID")
|
||||
|
||||
// Different callers at the same nonce → different ids.
|
||||
c := register(t, st, caller2)
|
||||
require.NotEqual(t, a, c)
|
||||
|
||||
// Job ids come from a separate nonce space, so a caller's first job id is
|
||||
// not its first provider id.
|
||||
j1 := submit(t, st, caller1, "m")
|
||||
j2 := submit(t, st, caller1, "m")
|
||||
require.NotEqual(t, j1, j2)
|
||||
}
|
||||
|
||||
func TestProviderAndJobNoncesAreIndependent(t *testing.T) {
|
||||
// Sharing one nonce would make a submit change the next providerID, so two
|
||||
// callers interleaving the two ops could collide.
|
||||
st := newMockState()
|
||||
register(t, st, caller1)
|
||||
providerNonce := st.GetStateDB().GetState(ContractAddress, storageSlot("nonce", caller1.Bytes()))
|
||||
submit(t, st, caller1, "m")
|
||||
require.Equal(t, providerNonce,
|
||||
st.GetStateDB().GetState(ContractAddress, storageSlot("nonce", caller1.Bytes())),
|
||||
"submitJob advanced the provider nonce")
|
||||
}
|
||||
|
||||
func TestStorageSlotsAreDomainSeparated(t *testing.T) {
|
||||
// Every record for one id lives under a distinct prefix. Two prefixes
|
||||
// hashing to one slot would make a job's price overwrite its status.
|
||||
id := crypto.Keccak256([]byte("id"))
|
||||
seen := map[common.Hash]string{}
|
||||
for _, prefix := range []string{
|
||||
"provider", "provider.owner", "provider.tee",
|
||||
"job", "job.submitter", "job.price", "job.input",
|
||||
"job.status", "job.output", "job.provider",
|
||||
"nonce", "job.nonce",
|
||||
} {
|
||||
s := storageSlot(prefix, id)
|
||||
if prev, dup := seen[s]; dup {
|
||||
t.Fatalf("prefixes %q and %q collide at slot %s", prev, prefix, s.Hex())
|
||||
}
|
||||
seen[s] = prefix
|
||||
}
|
||||
|
||||
// And the same prefix with different ids does not collide.
|
||||
require.NotEqual(t,
|
||||
storageSlot("job", crypto.Keccak256([]byte("a"))),
|
||||
storageSlot("job", crypto.Keccak256([]byte("b"))))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// KNOWN GAP — reported, awaiting a design decision
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// TestVerifyComputeAttestationIsDerivableFromPublicState pins a security gap
|
||||
// rather than a desired behaviour.
|
||||
//
|
||||
// The "attestation" verifyCompute checks is keccak256(jobID, outputHash). jobID
|
||||
// is supplied by the caller and outputHash is read straight out of public
|
||||
// contract storage, so ANY address can compute a passing attestation for ANY
|
||||
// claimed job without having run the computation and without any relationship
|
||||
// to the job. verifyCompute also takes no caller argument, so there is no
|
||||
// access control on who may finalise a job.
|
||||
//
|
||||
// Nothing of value moves inside this precompile (maxPrice is recorded and never
|
||||
// transferred), so the damage is confined to contracts that read job.status and
|
||||
// believe it means "a TEE attested this result". The fix is a design decision —
|
||||
// bind the check to a real TEE quote (attestation.VerifyCompute already does
|
||||
// exactly this) and restrict finalisation to the job's submitter — so it is
|
||||
// reported rather than changed here.
|
||||
//
|
||||
// When that fix lands, this test must be replaced by its inverse.
|
||||
func TestVerifyComputeAttestationIsDerivableFromPublicState(t *testing.T) {
|
||||
st := newMockState()
|
||||
register(t, st, caller2)
|
||||
jobID := submit(t, st, caller1, "model")
|
||||
outputHash := crypto.Keccak256([]byte("provider output"))
|
||||
require.NoError(t, claim(t, st, caller2, jobID, outputHash))
|
||||
|
||||
// caller3 is a stranger: not the submitter, not the provider, not even a
|
||||
// registered provider. It reads outputHash from public storage.
|
||||
publicOutput := st.GetStateDB().GetState(ContractAddress, storageSlot("job.output", jobID))
|
||||
forged := crypto.Keccak256(jobID, publicOutput.Bytes())
|
||||
|
||||
ret, _, err := run(t, st, caller3, OpVerifyCompute,
|
||||
append(append([]byte{}, jobID...), forged...), 1_000_000, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, byte(1), ret[31],
|
||||
"KNOWN GAP: a stranger finalised a job using only public state")
|
||||
require.Equal(t, common.BytesToHash([]byte{StatusVerified}),
|
||||
st.GetStateDB().GetState(ContractAddress, storageSlot("job.status", jobID)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Module registration and config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestModuleIsRegisteredAtItsAddress(t *testing.T) {
|
||||
require.Equal(t, ConfigKey, Module.ConfigKey)
|
||||
require.Equal(t, ContractAddress, Module.Address)
|
||||
require.Same(t, Precompile, Module.Contract)
|
||||
|
||||
m, ok := modules.GetPrecompileModuleByAddress(ContractAddress)
|
||||
require.True(t, ok, "init() must have registered the compute precompile")
|
||||
require.Equal(t, ConfigKey, m.ConfigKey)
|
||||
|
||||
byKey, ok := modules.GetPrecompileModule(ConfigKey)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, ContractAddress, byKey.Address)
|
||||
|
||||
require.True(t, modules.ReservedAddress(ContractAddress),
|
||||
"the compute address must sit in a reserved range")
|
||||
require.False(t, Module.AlwaysOn, "compute activates by config, not at genesis")
|
||||
}
|
||||
|
||||
func TestConfigLifecycle(t *testing.T) {
|
||||
c := &configurator{}
|
||||
|
||||
cfg, ok := c.MakeConfig().(*Config)
|
||||
require.True(t, ok, "MakeConfig must produce this package's Config")
|
||||
require.Equal(t, ConfigKey, cfg.Key())
|
||||
require.Nil(t, cfg.Timestamp())
|
||||
require.False(t, cfg.IsDisabled())
|
||||
require.NoError(t, cfg.Verify(nil))
|
||||
|
||||
ts := uint64(1_700_000_000)
|
||||
at := &Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts}}
|
||||
require.Equal(t, ts, *at.Timestamp())
|
||||
|
||||
off := &Config{Upgrade: precompileconfig.Upgrade{Disable: true}}
|
||||
require.True(t, off.IsDisabled())
|
||||
}
|
||||
|
||||
func TestConfigEqual(t *testing.T) {
|
||||
ts := uint64(5)
|
||||
other := uint64(6)
|
||||
|
||||
a := &Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts}}
|
||||
b := &Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts}}
|
||||
require.True(t, a.Equal(b))
|
||||
require.True(t, b.Equal(a))
|
||||
|
||||
require.False(t, a.Equal(&Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &other}}))
|
||||
require.False(t, a.Equal(&Config{}))
|
||||
require.False(t, a.Equal(nil), "Equal against nil must be false, not a panic")
|
||||
|
||||
// A config of a different precompile is never equal, whatever its fields.
|
||||
require.False(t, a.Equal(foreignConfig{}))
|
||||
}
|
||||
|
||||
type foreignConfig struct{}
|
||||
|
||||
func (foreignConfig) Key() string { return "other" }
|
||||
func (foreignConfig) Timestamp() *uint64 { return nil }
|
||||
func (foreignConfig) IsDisabled() bool { return false }
|
||||
func (foreignConfig) Equal(precompileconfig.Config) bool { return false }
|
||||
func (foreignConfig) Verify(precompileconfig.ChainConfig) error { return nil }
|
||||
|
||||
func TestConfigureIsANoOpButSucceeds(t *testing.T) {
|
||||
// compute keeps no genesis state, so Configure writes nothing. Assert that
|
||||
// explicitly: a Configure that silently wrote would corrupt genesis.
|
||||
st := newMockState()
|
||||
db := st.GetStateDB().(*mockStateDB)
|
||||
before := len(db.storage)
|
||||
|
||||
require.NoError(t, (&configurator{}).Configure(nil, &Config{}, db, nil))
|
||||
require.Equal(t, before, len(db.storage))
|
||||
}
|
||||
@@ -111,22 +111,21 @@ func (p *curve25519Precompile) Run(
|
||||
}
|
||||
}
|
||||
|
||||
// decodePoint rejects any encoding that is not a point on Edwards25519. Length
|
||||
// is settled twice over -- every caller slices exactly CompressedLen bytes, and
|
||||
// SetBytes refuses another width -- so there is no third check here.
|
||||
func decodePoint(data []byte) (*edwards25519.Point, error) {
|
||||
if len(data) < CompressedLen {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
pt, err := new(edwards25519.Point).SetBytes(data[:CompressedLen])
|
||||
pt, err := new(edwards25519.Point).SetBytes(data)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidPoint
|
||||
}
|
||||
return pt, nil
|
||||
}
|
||||
|
||||
// decodeScalar takes canonical scalars only: s >= L is refused rather than
|
||||
// reduced, so each group element has exactly one multiplier that names it.
|
||||
func decodeScalar(data []byte) (*edwards25519.Scalar, error) {
|
||||
if len(data) < ScalarLen {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
s, err := new(edwards25519.Scalar).SetCanonicalBytes(data[:ScalarLen])
|
||||
s, err := new(edwards25519.Scalar).SetCanonicalBytes(data)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
|
||||
@@ -0,0 +1,522 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package curve25519
|
||||
|
||||
import (
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"filippo.io/edwards25519"
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/precompile/contract"
|
||||
"github.com/luxfi/precompile/modules"
|
||||
"github.com/luxfi/precompile/precompileconfig"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func kat(tb testing.TB, s string) []byte {
|
||||
tb.Helper()
|
||||
b, err := hex.DecodeString(s)
|
||||
require.NoError(tb, err)
|
||||
require.Len(tb, b, 32)
|
||||
return b
|
||||
}
|
||||
|
||||
func call(tb testing.TB, op byte, args ...[]byte) ([]byte, uint64, error) {
|
||||
tb.Helper()
|
||||
in := []byte{op}
|
||||
for _, a := range args {
|
||||
in = append(in, a...)
|
||||
}
|
||||
return Curve25519Precompile.Run(nil, common.Address{}, ContractAddress, in, 1<<22, true)
|
||||
}
|
||||
|
||||
// scalarLE encodes a small integer as a canonical 32-byte little-endian scalar.
|
||||
func scalarLE(v uint64) []byte {
|
||||
s := make([]byte, 32)
|
||||
for i := 0; v > 0; i++ {
|
||||
s[i] = byte(v)
|
||||
v >>= 8
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Reference encodings of k*B on Edwards25519, computed with an independent
|
||||
// implementation of the twisted-Edwards addition law and checked against the
|
||||
// RFC 8032 base point.
|
||||
const (
|
||||
basepointEnc = "5866666666666666666666666666666666666666666666666666666666666666"
|
||||
identityEnc = "0100000000000000000000000000000000000000000000000000000000000000"
|
||||
twoB = "c9a3f86aae465f0e56513864510f3997561fa2c9e85ea21dc2292309f3cd6022"
|
||||
threeB = "d4b4f5784868c3020403246717ec169ff79e26608ea126a1ab69ee77d1b16712"
|
||||
sevenB = "b862409fb5c4c4123df2abf7462b88f041ad36dd6864ce872fd5472be363c5b1"
|
||||
tenB = "2c7be86ab07488ba43e8e03d85a67625cfbf98c8544de4c877241b7aaafc7fe3"
|
||||
thirteenB = "801f40eaaee1ef8723279a28b2cf4037b889dad222604678748b53ed0db0db92"
|
||||
bigB = "ef4f62f8479733ad879cfaced3c89a9c39dd4fc795ef2efa1c3eafe4d729a081" // 12345*B
|
||||
negB = "58666666666666666666666666666666666666666666666666666666666666e6" // (L-1)*B
|
||||
)
|
||||
|
||||
// TestKAT_BasepointMul pins scalar-base multiplication against reference
|
||||
// encodings. Round-tripping the precompile against itself would pass even if
|
||||
// every one of these were wrong.
|
||||
func TestKAT_BasepointMul(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
k uint64
|
||||
want string
|
||||
}{
|
||||
{1, basepointEnc},
|
||||
{2, twoB},
|
||||
{3, threeB},
|
||||
{7, sevenB},
|
||||
{13, thirteenB},
|
||||
{12345, bigB},
|
||||
} {
|
||||
out, _, err := call(t, OpBasepointMul, scalarLE(tc.k))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, kat(t, tc.want), out, "%d*B", tc.k)
|
||||
}
|
||||
|
||||
// k = 0 is the identity; k = L-1 is -B.
|
||||
out, _, err := call(t, OpBasepointMul, make([]byte, ScalarLen))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, kat(t, identityEnc), out, "0*B is the identity")
|
||||
|
||||
lMinus1 := new(big.Int).Sub(groupOrder(), big.NewInt(1))
|
||||
out, _, err = call(t, OpBasepointMul, leBytes(lMinus1))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, kat(t, negB), out, "(L-1)*B is -B")
|
||||
}
|
||||
|
||||
// TestKAT_BasepointMulMatchesRFC8032KeyDerivation crosses this precompile with
|
||||
// a published Ed25519 vector: RFC 8032 test 1's public key IS [a]B for the
|
||||
// clamped scalar derived from its seed. Reproducing it here proves the scalar
|
||||
// multiplication agrees with an entirely separate standard.
|
||||
func TestKAT_BasepointMulMatchesRFC8032KeyDerivation(t *testing.T) {
|
||||
seed := kat(t, "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60")
|
||||
h := sha512.Sum512(seed)
|
||||
|
||||
// RFC 8032 clamping.
|
||||
h[0] &= 248
|
||||
h[31] &= 127
|
||||
h[31] |= 64
|
||||
|
||||
// The clamped scalar exceeds L, and decodeScalar rightly refuses
|
||||
// non-canonical scalars, so reduce first: [a]B == [a mod L]B.
|
||||
a := new(big.Int).SetBytes(reverse(h[:32]))
|
||||
a.Mod(a, groupOrder())
|
||||
|
||||
out, _, err := call(t, OpBasepointMul, leBytes(a))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t,
|
||||
kat(t, "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"), out,
|
||||
"must reproduce the RFC 8032 TEST 1 public key")
|
||||
}
|
||||
|
||||
// TestKAT_PointAdd pins addition and cross-checks it against multiplication.
|
||||
func TestKAT_PointAdd(t *testing.T) {
|
||||
B := kat(t, basepointEnc)
|
||||
id := kat(t, identityEnc)
|
||||
|
||||
out, _, err := call(t, OpPointAdd, B, B)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, kat(t, twoB), out, "B + B == 2B")
|
||||
|
||||
out, _, err = call(t, OpPointAdd, kat(t, twoB), B)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, kat(t, threeB), out, "2B + B == 3B")
|
||||
|
||||
// Identity is neutral, in both argument positions.
|
||||
for _, args := range [][2][]byte{{B, id}, {id, B}} {
|
||||
out, _, err := call(t, OpPointAdd, args[0], args[1])
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, B, out)
|
||||
}
|
||||
|
||||
// B + (-B) is the identity.
|
||||
out, _, err = call(t, OpPointAdd, B, kat(t, negB))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, id, out)
|
||||
|
||||
// Addition is commutative and associative over the reference values.
|
||||
ab, _, err := call(t, OpPointAdd, kat(t, threeB), kat(t, sevenB))
|
||||
require.NoError(t, err)
|
||||
ba, _, err := call(t, OpPointAdd, kat(t, sevenB), kat(t, threeB))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ab, ba)
|
||||
require.Equal(t, kat(t, tenB), ab, "3B + 7B == 10B")
|
||||
}
|
||||
|
||||
// TestKAT_ScalarMulMatchesBasepointMul is the cross-implementation check
|
||||
// between the variable-base ladder and the fixed-base table. Both must land on
|
||||
// the same reference encodings.
|
||||
func TestKAT_ScalarMulMatchesBasepointMul(t *testing.T) {
|
||||
B := kat(t, basepointEnc)
|
||||
for _, k := range []uint64{0, 1, 2, 3, 7, 13, 12345, 1 << 32} {
|
||||
viaBase, _, err := call(t, OpBasepointMul, scalarLE(k))
|
||||
require.NoError(t, err)
|
||||
viaMul, _, err := call(t, OpScalarMul, B, scalarLE(k))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, viaBase, viaMul, "k=%d: the two ladders must agree", k)
|
||||
}
|
||||
|
||||
// Doubling by scalar equals doubling by addition, on a non-generator point.
|
||||
P := kat(t, sevenB)
|
||||
byMul, _, err := call(t, OpScalarMul, P, scalarLE(2))
|
||||
require.NoError(t, err)
|
||||
byAdd, _, err := call(t, OpPointAdd, P, P)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, byAdd, byMul)
|
||||
}
|
||||
|
||||
// TestKAT_MSM pins the multiscalar path against the reference: 3*B + 5*(2B)
|
||||
// is 13B, which is a published-by-construction value computed independently.
|
||||
func TestKAT_MSM(t *testing.T) {
|
||||
B := kat(t, basepointEnc)
|
||||
out, _, err := call(t, OpMSM, B, scalarLE(3), kat(t, twoB), scalarLE(5))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, kat(t, thirteenB), out, "3*B + 5*(2B) == 13B")
|
||||
|
||||
// A single pair degenerates to scalar multiplication.
|
||||
single, _, err := call(t, OpMSM, B, scalarLE(7))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, kat(t, sevenB), single)
|
||||
|
||||
// An all-zero scalar set sums to the identity.
|
||||
zeros, _, err := call(t, OpMSM, B, make([]byte, 32), kat(t, twoB), make([]byte, 32))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, kat(t, identityEnc), zeros)
|
||||
|
||||
// Order does not matter.
|
||||
swapped, _, err := call(t, OpMSM, kat(t, twoB), scalarLE(5), B, scalarLE(3))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, out, swapped)
|
||||
}
|
||||
|
||||
// smallOrder is the full torsion subgroup of Edwards25519, canonical encodings.
|
||||
var smallOrder = []string{
|
||||
identityEnc, // order 1
|
||||
"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", // order 2
|
||||
"0000000000000000000000000000000000000000000000000000000000000000", // order 4
|
||||
"0000000000000000000000000000000000000000000000000000000000000080", // order 4
|
||||
"26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05", // order 8
|
||||
"26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85", // order 8
|
||||
"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a", // order 8
|
||||
"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa", // order 8
|
||||
}
|
||||
|
||||
// TestSmallOrderPointsAreAccepted records, deliberately, that this precompile
|
||||
// is raw group arithmetic and admits the torsion subgroup. Multiplying any of
|
||||
// these by 8 lands on the identity, which is exactly the property a caller
|
||||
// building ring signatures, key images or a VRF must check for itself before
|
||||
// trusting a point it did not generate. If this ever starts refusing them, the
|
||||
// arithmetic surface shrank and downstream protocols break.
|
||||
func TestSmallOrderPointsAreAccepted(t *testing.T) {
|
||||
id := kat(t, identityEnc)
|
||||
for _, enc := range smallOrder {
|
||||
P := kat(t, enc)
|
||||
|
||||
out, _, err := call(t, OpScalarMul, P, scalarLE(8))
|
||||
require.NoError(t, err, "torsion point %s is valid input", enc[:12])
|
||||
require.Equal(t, id, out, "8 * torsion point is the identity")
|
||||
|
||||
// Adding it to the base point is accepted and moves the point: this is
|
||||
// how a cofactor-unaware caller loses uniqueness of the representative.
|
||||
shifted, _, err := call(t, OpPointAdd, kat(t, basepointEnc), P)
|
||||
require.NoError(t, err)
|
||||
if enc == identityEnc {
|
||||
require.Equal(t, kat(t, basepointEnc), shifted)
|
||||
} else {
|
||||
require.NotEqual(t, kat(t, basepointEnc), shifted,
|
||||
"a non-identity torsion point must change the encoding")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNonCanonicalPointEncodingAccepted pins the decoder's documented
|
||||
// behaviour: it follows ref10, so y and y+p decode to the same point and one
|
||||
// point has several accepted encodings. The result is always re-encoded
|
||||
// canonically, so no operation can be made to emit a non-canonical point --
|
||||
// but calldata is not a unique name for an input.
|
||||
func TestNonCanonicalPointEncodingAccepted(t *testing.T) {
|
||||
canonical := kat(t, identityEnc)
|
||||
// y = p+1 reduces to y = 1, which is the identity.
|
||||
noncanonical := kat(t, "eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f")
|
||||
require.NotEqual(t, canonical, noncanonical)
|
||||
|
||||
B := kat(t, basepointEnc)
|
||||
viaCanonical, _, err := call(t, OpPointAdd, B, canonical)
|
||||
require.NoError(t, err)
|
||||
viaOther, _, err := call(t, OpPointAdd, B, noncanonical)
|
||||
require.NoError(t, err, "the ref10 decoder accepts the non-canonical encoding")
|
||||
require.Equal(t, viaCanonical, viaOther, "both encodings name the same point")
|
||||
require.Equal(t, B, viaOther)
|
||||
require.Equal(t, canonical, edwards25519.NewIdentityPoint().Bytes(),
|
||||
"output is always the canonical encoding")
|
||||
}
|
||||
|
||||
// TestRefusal_PointNotOnCurve is the invalid-curve refusal. Every one of these
|
||||
// is a well-formed 32-byte word whose y-coordinate has no matching x on the
|
||||
// curve, so decoding must fail rather than produce a point on some other curve.
|
||||
func TestRefusal_PointNotOnCurve(t *testing.T) {
|
||||
B := kat(t, basepointEnc)
|
||||
|
||||
var offCurve [][]byte
|
||||
for y := byte(2); len(offCurve) < 6 && y < 200; y++ {
|
||||
enc := make([]byte, 32)
|
||||
enc[0] = y
|
||||
if _, err := new(edwards25519.Point).SetBytes(enc); err != nil {
|
||||
offCurve = append(offCurve, enc)
|
||||
}
|
||||
}
|
||||
require.NotEmpty(t, offCurve, "must find genuinely off-curve encodings")
|
||||
|
||||
for i, bad := range offCurve {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
in [][]byte
|
||||
op byte
|
||||
}{
|
||||
{"pointAdd first", [][]byte{bad, B}, OpPointAdd},
|
||||
{"pointAdd second", [][]byte{B, bad}, OpPointAdd},
|
||||
{"scalarMul", [][]byte{bad, scalarLE(2)}, OpScalarMul},
|
||||
{"msm", [][]byte{bad, scalarLE(2)}, OpMSM},
|
||||
{"msm second pair", [][]byte{B, scalarLE(1), bad, scalarLE(2)}, OpMSM},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
out, _, err := call(t, tc.op, tc.in...)
|
||||
require.ErrorIs(t, err, ErrInvalidPoint, "case %d", i)
|
||||
require.Nil(t, out)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRefusal_NonCanonicalScalar is the malleability refusal on the scalar
|
||||
// side: L, L+1 and 2^255-1 all reduce into range, and accepting them would give
|
||||
// every scalar multiple several encodings.
|
||||
func TestRefusal_NonCanonicalScalar(t *testing.T) {
|
||||
B := kat(t, basepointEnc)
|
||||
l := groupOrder()
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
s []byte
|
||||
}{
|
||||
{"s = L", leBytes(l)},
|
||||
{"s = L+1", leBytes(new(big.Int).Add(l, big.NewInt(1)))},
|
||||
{"s = 2L-1", leBytes(new(big.Int).Sub(new(big.Int).Lsh(l, 1), big.NewInt(1)))},
|
||||
{"s = all ones", func() []byte {
|
||||
b := make([]byte, 32)
|
||||
for i := range b {
|
||||
b[i] = 0xFF
|
||||
}
|
||||
return b
|
||||
}()},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Len(t, tc.s, ScalarLen)
|
||||
for op, args := range map[byte][][]byte{
|
||||
OpScalarMul: {B, tc.s},
|
||||
OpBasepointMul: {tc.s},
|
||||
OpMSM: {B, tc.s},
|
||||
} {
|
||||
out, _, err := call(t, op, args...)
|
||||
require.ErrorIs(t, err, ErrInvalidInput, "op %#x must refuse s >= L", op)
|
||||
require.Nil(t, out)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// L-1 is the largest accepted scalar; it must still work.
|
||||
out, _, err := call(t, OpBasepointMul, leBytes(new(big.Int).Sub(l, big.NewInt(1))))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, kat(t, negB), out)
|
||||
}
|
||||
|
||||
// TestRefusal_Length walks every short, empty and misaligned shape.
|
||||
func TestRefusal_Length(t *testing.T) {
|
||||
B := kat(t, basepointEnc)
|
||||
s := scalarLE(3)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
in []byte
|
||||
err error
|
||||
}{
|
||||
{"nil", nil, ErrInvalidInput},
|
||||
{"empty", []byte{}, ErrInvalidInput},
|
||||
{"pointAdd op only", []byte{OpPointAdd}, ErrInvalidInput},
|
||||
{"pointAdd one point", append([]byte{OpPointAdd}, B...), ErrInvalidInput},
|
||||
{"pointAdd one byte short", append([]byte{OpPointAdd}, append(append([]byte{}, B...), B[:31]...)...), ErrInvalidInput},
|
||||
{"scalarMul op only", []byte{OpScalarMul}, ErrInvalidInput},
|
||||
{"scalarMul no scalar", append([]byte{OpScalarMul}, B...), ErrInvalidInput},
|
||||
{"scalarMul one byte short", append([]byte{OpScalarMul}, append(append([]byte{}, B...), s[:31]...)...), ErrInvalidInput},
|
||||
{"basepointMul op only", []byte{OpBasepointMul}, ErrInvalidInput},
|
||||
{"basepointMul one byte short", append([]byte{OpBasepointMul}, s[:31]...), ErrInvalidInput},
|
||||
{"msm op only", []byte{OpMSM}, ErrInvalidInput},
|
||||
{"msm half pair", append([]byte{OpMSM}, B...), ErrInvalidInput},
|
||||
{"msm one byte long", append([]byte{OpMSM}, append(append(append([]byte{}, B...), s...), 0)...), ErrInvalidInput},
|
||||
{"msm one byte short", append([]byte{OpMSM}, append(append([]byte{}, B...), s[:31]...)...), ErrInvalidInput},
|
||||
{"unknown op 0x00", []byte{0x00}, ErrInvalidOp},
|
||||
{"unknown op 0x05", []byte{0x05}, ErrInvalidOp},
|
||||
{"unknown op 0xff", append([]byte{0xFF}, B...), ErrInvalidOp},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
out, _, err := Curve25519Precompile.Run(nil, common.Address{}, ContractAddress, tc.in, 1<<22, true)
|
||||
require.ErrorIs(t, err, tc.err)
|
||||
require.Nil(t, out)
|
||||
})
|
||||
}
|
||||
|
||||
// Trailing data past a fixed-width operand is ignored, not refused.
|
||||
short, _, err := call(t, OpPointAdd, B, B)
|
||||
require.NoError(t, err)
|
||||
long, _, err := call(t, OpPointAdd, B, B, []byte{0xAA, 0xBB})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, short, long, "trailing bytes are ignored on the fixed-width ops")
|
||||
}
|
||||
|
||||
// TestGas_Schedule covers every arm, and asserts that MSM is priced per pair --
|
||||
// a flat price there would let one call buy an unbounded multiscalar.
|
||||
func TestGas_Schedule(t *testing.T) {
|
||||
pair := CompressedLen + ScalarLen
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
in []byte
|
||||
want uint64
|
||||
}{
|
||||
{"nil", nil, 0},
|
||||
{"empty", []byte{}, 0},
|
||||
{"pointAdd", []byte{OpPointAdd}, GasPointAdd},
|
||||
{"scalarMul", []byte{OpScalarMul}, GasScalarMul},
|
||||
{"basepointMul", []byte{OpBasepointMul}, GasBasepointMul},
|
||||
{"unknown 0x00", []byte{0x00}, 0},
|
||||
{"unknown 0x05", []byte{0x05}, 0},
|
||||
{"unknown 0xff", []byte{0xFF}, 0},
|
||||
{"msm no data", []byte{OpMSM}, 0},
|
||||
{"msm short of one pair", append([]byte{OpMSM}, make([]byte, pair-1)...), 0},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Equal(t, tc.want, Curve25519Precompile.RequiredGas(tc.in))
|
||||
})
|
||||
}
|
||||
|
||||
// Price rises by exactly one per-pair unit for each additional pair.
|
||||
prev := uint64(0)
|
||||
for n := 1; n <= 32; n++ {
|
||||
in := append([]byte{OpMSM}, make([]byte, n*pair)...)
|
||||
got := Curve25519Precompile.RequiredGas(in)
|
||||
require.Equal(t, uint64(GasMSMBase+n*GasMSMPerPair), got, "n=%d", n)
|
||||
if n > 1 {
|
||||
require.Equal(t, uint64(GasMSMPerPair), got-prev, "each pair costs the same")
|
||||
}
|
||||
prev = got
|
||||
}
|
||||
|
||||
// A huge declared pair count neither overflows nor wraps: the count comes
|
||||
// from the actual byte length, so it cannot be inflated independently.
|
||||
huge := append([]byte{OpMSM}, make([]byte, 4096*pair)...)
|
||||
require.Equal(t, uint64(GasMSMBase+4096*GasMSMPerPair), Curve25519Precompile.RequiredGas(huge))
|
||||
require.Greater(t, Curve25519Precompile.RequiredGas(huge), uint64(GasMSMBase))
|
||||
|
||||
// Every zero-priced input must refuse: nothing is bought for free.
|
||||
for _, in := range [][]byte{nil, {}, {0x00}, {0x05}, {0xFF}, {OpMSM}, append([]byte{OpMSM}, make([]byte, pair-1)...)} {
|
||||
require.Zero(t, Curve25519Precompile.RequiredGas(in))
|
||||
out, _, err := Curve25519Precompile.Run(nil, common.Address{}, ContractAddress, in, 0, true)
|
||||
require.Error(t, err, "a zero-gas input must not execute")
|
||||
require.Nil(t, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGas_Deduction(t *testing.T) {
|
||||
in := append([]byte{OpBasepointMul}, scalarLE(3)...)
|
||||
|
||||
_, left, err := Curve25519Precompile.Run(nil, common.Address{}, ContractAddress, in, GasBasepointMul+9, true)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(9), left)
|
||||
|
||||
_, left, err = Curve25519Precompile.Run(nil, common.Address{}, ContractAddress, in, GasBasepointMul, true)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, left)
|
||||
|
||||
_, left, err = Curve25519Precompile.Run(nil, common.Address{}, ContractAddress, in, GasBasepointMul-1, true)
|
||||
require.ErrorIs(t, err, contract.ErrOutOfGas)
|
||||
require.Zero(t, left)
|
||||
|
||||
// MSM must be unaffordable at the single-pair price when two pairs are sent.
|
||||
pair := CompressedLen + ScalarLen
|
||||
two := append([]byte{OpMSM}, make([]byte, 2*pair)...)
|
||||
_, _, err = Curve25519Precompile.Run(nil, common.Address{}, ContractAddress, two,
|
||||
GasMSMBase+GasMSMPerPair, true)
|
||||
require.ErrorIs(t, err, contract.ErrOutOfGas, "two pairs must cost more than one")
|
||||
}
|
||||
|
||||
// --- helpers ------------------------------------------------------------
|
||||
|
||||
func groupOrder() *big.Int {
|
||||
l, ok := new(big.Int).SetString(
|
||||
"7237005577332262213973186563042994240857116359379907606001950938285454250989", 10)
|
||||
if !ok {
|
||||
panic("bad order literal")
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func reverse(b []byte) []byte {
|
||||
out := make([]byte, len(b))
|
||||
for i := range b {
|
||||
out[len(b)-1-i] = b[i]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// leBytes encodes v as 32 little-endian bytes.
|
||||
func leBytes(v *big.Int) []byte {
|
||||
be := make([]byte, 32)
|
||||
v.FillBytes(be)
|
||||
return reverse(be)
|
||||
}
|
||||
|
||||
// --- module + config ----------------------------------------------------
|
||||
|
||||
func TestModule_Wiring(t *testing.T) {
|
||||
require.Equal(t, ConfigKey, Module.ConfigKey)
|
||||
require.Equal(t, ContractAddress, Module.Address)
|
||||
require.Same(t, Curve25519Precompile, Module.Contract)
|
||||
require.NotNil(t, Module.Configurator)
|
||||
require.Equal(t,
|
||||
common.HexToAddress("0x0000000000000000000000000000000000009204"),
|
||||
ContractAddress)
|
||||
}
|
||||
|
||||
func TestConfigurator_Surface(t *testing.T) {
|
||||
c := &configurator{}
|
||||
require.IsType(t, &Config{}, c.MakeConfig())
|
||||
require.NoError(t, c.Configure(nil, nil, nil, nil))
|
||||
}
|
||||
|
||||
func TestConfig_Surface(t *testing.T) {
|
||||
c := &Config{}
|
||||
require.Equal(t, ConfigKey, c.Key())
|
||||
require.Nil(t, c.Timestamp())
|
||||
require.False(t, c.IsDisabled())
|
||||
require.NoError(t, c.Verify(nil))
|
||||
|
||||
ts := uint64(11)
|
||||
a := &Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts}}
|
||||
require.Equal(t, &ts, a.Timestamp())
|
||||
require.True(t, a.Equal(&Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts}}))
|
||||
|
||||
other := uint64(12)
|
||||
require.False(t, a.Equal(&Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &other}}))
|
||||
require.False(t, a.Equal(nil))
|
||||
require.True(t, (&Config{Upgrade: precompileconfig.Upgrade{Disable: true}}).IsDisabled())
|
||||
}
|
||||
|
||||
func TestRegisterModuleIsIdempotentlyRefused(t *testing.T) {
|
||||
require.Error(t, modules.RegisterModule(Module))
|
||||
}
|
||||
+3
-6
@@ -85,16 +85,13 @@ func (c *ed25519VerifyPrecompile) Run(
|
||||
return nil, remainingGas, nil
|
||||
}
|
||||
|
||||
// Extract components
|
||||
// Extract components. The length check above fixes every slice, so no
|
||||
// further size test is possible here: PublicKeySize == ed25519.PublicKeySize
|
||||
// and the slice bounds are constants.
|
||||
message := input[0:MessageHashSize]
|
||||
signature := input[MessageHashSize : MessageHashSize+SignatureSize]
|
||||
publicKey := input[MessageHashSize+SignatureSize : MessageHashSize+SignatureSize+PublicKeySize]
|
||||
|
||||
// Validate public key length
|
||||
if len(publicKey) != ed25519.PublicKeySize {
|
||||
return nil, remainingGas, nil
|
||||
}
|
||||
|
||||
// CPU is the single source of truth, via the canonical luxfi/crypto/ed25519
|
||||
// verifier. Ed25519 verification disagreement across implementations is a
|
||||
// well-known hazard (cofactored vs cofactorless checks, non-canonical S>=L,
|
||||
|
||||
@@ -0,0 +1,491 @@
|
||||
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package ed25519
|
||||
|
||||
import (
|
||||
stded "crypto/ed25519"
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"filippo.io/edwards25519"
|
||||
luxed "github.com/luxfi/crypto/ed25519"
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/precompile/contract"
|
||||
"github.com/luxfi/precompile/modules"
|
||||
"github.com/luxfi/precompile/precompileconfig"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func katHex(tb testing.TB, s string) []byte {
|
||||
tb.Helper()
|
||||
b, err := hex.DecodeString(s)
|
||||
require.NoError(tb, err)
|
||||
return b
|
||||
}
|
||||
|
||||
// rfc8032 carries the Ed25519 vectors from RFC 8032 section 7.1. Each was
|
||||
// re-derived from the seed and re-verified against an independent reference
|
||||
// implementation of the verification equation before being written down here,
|
||||
// so a uniformly wrong verifier cannot satisfy them.
|
||||
var rfc8032 = []struct {
|
||||
name string
|
||||
seed string
|
||||
pub string
|
||||
msg string
|
||||
sig string
|
||||
}{
|
||||
{
|
||||
name: "RFC8032/TEST1/empty-message",
|
||||
seed: "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60",
|
||||
pub: "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
|
||||
msg: "",
|
||||
sig: "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555f" +
|
||||
"b8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b",
|
||||
},
|
||||
{
|
||||
name: "RFC8032/TEST2/one-byte",
|
||||
seed: "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb",
|
||||
pub: "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c",
|
||||
msg: "72",
|
||||
sig: "92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da08" +
|
||||
"5ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00",
|
||||
},
|
||||
{
|
||||
name: "RFC8032/TEST3/two-byte",
|
||||
seed: "c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7",
|
||||
pub: "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025",
|
||||
msg: "af82",
|
||||
sig: "6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18" +
|
||||
"ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a",
|
||||
},
|
||||
{
|
||||
name: "RFC8032/TEST-SHA(abc)/64-byte",
|
||||
seed: "833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42",
|
||||
pub: "ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf",
|
||||
msg: "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a21" +
|
||||
"92992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f",
|
||||
sig: "dc2a4459e7369633a52b1bf277839a00201009a3efbf3ecb69bea2186c26b58909" +
|
||||
"351fc9ac90b3ecfdfbc7c66431e0303dca179c138ac17ad9bef1177331a704",
|
||||
},
|
||||
}
|
||||
|
||||
// TestKAT_RFC8032 pins the verifier against published vectors. A round trip
|
||||
// (sign then verify with the same code) passes even when the whole scheme is
|
||||
// uniformly wrong; these do not.
|
||||
func TestKAT_RFC8032(t *testing.T) {
|
||||
for _, v := range rfc8032 {
|
||||
t.Run(v.name, func(t *testing.T) {
|
||||
seed, pub := katHex(t, v.seed), katHex(t, v.pub)
|
||||
msg, sig := katHex(t, v.msg), katHex(t, v.sig)
|
||||
|
||||
// The seed must produce the published public key: this binds key
|
||||
// derivation, not just the verification equation.
|
||||
priv := stded.NewKeyFromSeed(seed)
|
||||
require.Equal(t, pub, []byte(priv.Public().(stded.PublicKey)))
|
||||
require.Equal(t, sig, stded.Sign(priv, msg))
|
||||
|
||||
require.True(t, VerifyRaw(msg, sig, pub), "published vector must verify")
|
||||
require.True(t, Verify(msg, sig, pub))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestKAT_RFC8032_BitFlipsRejected mutates one bit of every field of every
|
||||
// published vector and requires rejection. Without this a verifier that always
|
||||
// answers true still passes TestKAT_RFC8032.
|
||||
func TestKAT_RFC8032_BitFlipsRejected(t *testing.T) {
|
||||
for _, v := range rfc8032 {
|
||||
pub, msg, sig := katHex(t, v.pub), katHex(t, v.msg), katHex(t, v.sig)
|
||||
|
||||
for _, f := range []struct {
|
||||
field string
|
||||
at int
|
||||
buf []byte
|
||||
}{
|
||||
{"R", 0, sig}, {"R", 31, sig},
|
||||
{"S", 32, sig}, {"S", 63, sig},
|
||||
{"pubkey", 0, pub}, {"pubkey", 31, pub},
|
||||
} {
|
||||
t.Run(v.name+"/"+f.field+"/byte", func(t *testing.T) {
|
||||
m := append([]byte{}, f.buf...)
|
||||
m[f.at] ^= 0x01
|
||||
p, s := pub, sig
|
||||
if f.field == "pubkey" {
|
||||
p = m
|
||||
} else {
|
||||
s = m
|
||||
}
|
||||
require.False(t, VerifyRaw(msg, s, p), "mutated %s must not verify", f.field)
|
||||
})
|
||||
}
|
||||
if len(msg) > 0 {
|
||||
t.Run(v.name+"/message", func(t *testing.T) {
|
||||
m := append([]byte{}, msg...)
|
||||
m[0] ^= 0x01
|
||||
require.False(t, VerifyRaw(m, sig, pub))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// order is the Ed25519 group order L = 2^252 + 27742317777372353535851937790883648493.
|
||||
func order() *big.Int {
|
||||
l, ok := new(big.Int).SetString(
|
||||
"7237005577332262213973186563042994240857116359379907606001950938285454250989", 10)
|
||||
if !ok {
|
||||
panic("bad order literal")
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// TestKAT_MalleableScalarRejected is the malleability refusal that matters for
|
||||
// a precompile: (R, S) and (R, S+L) satisfy the same verification equation over
|
||||
// the group, so a verifier that reduces S instead of refusing it hands every
|
||||
// caller a second valid encoding of one signature. RFC 8032 requires 0 <= S < L.
|
||||
func TestKAT_MalleableScalarRejected(t *testing.T) {
|
||||
l := order()
|
||||
for _, v := range rfc8032 {
|
||||
pub, msg, sig := katHex(t, v.pub), katHex(t, v.msg), katHex(t, v.sig)
|
||||
require.True(t, VerifyRaw(msg, sig, pub), "precondition")
|
||||
|
||||
// S is little-endian. S + k*L stays inside 64 bytes for k = 1, 2, 3.
|
||||
s := new(big.Int).SetBytes(reversed(sig[32:]))
|
||||
require.Negative(t, s.Cmp(l), "published S must already be reduced")
|
||||
|
||||
for k := int64(1); k <= 3; k++ {
|
||||
t.Run(v.name+"/S+"+string(rune('0'+k))+"L", func(t *testing.T) {
|
||||
sk := new(big.Int).Add(s, new(big.Int).Mul(big.NewInt(k), l))
|
||||
if sk.BitLen() > 256 {
|
||||
t.Skipf("S+%dL does not fit in 32 bytes", k)
|
||||
}
|
||||
be := make([]byte, 32)
|
||||
sk.FillBytes(be)
|
||||
mal := append(append([]byte{}, sig[:32]...), reversed(be)...)
|
||||
require.Len(t, mal, 64)
|
||||
require.NotEqual(t, sig, mal, "mutation must change the encoding")
|
||||
require.False(t, VerifyRaw(msg, mal, pub),
|
||||
"non-canonical S (>= L) must be refused, not reduced")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func reversed(b []byte) []byte {
|
||||
out := make([]byte, len(b))
|
||||
for i := range b {
|
||||
out[len(b)-1-i] = b[i]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// smallOrder lists the canonical encodings of the eight points of order
|
||||
// dividing 8 on Edwards25519 -- the whole torsion subgroup.
|
||||
var smallOrder = []string{
|
||||
"0100000000000000000000000000000000000000000000000000000000000000", // identity, order 1
|
||||
"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", // order 2
|
||||
"0000000000000000000000000000000000000000000000000000000000000000", // order 4
|
||||
"0000000000000000000000000000000000000000000000000000000000000080", // order 4
|
||||
"26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05", // order 8
|
||||
"26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85", // order 8
|
||||
"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a", // order 8
|
||||
"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa", // order 8
|
||||
}
|
||||
|
||||
// TestKAT_SmallOrderKeyForgesWithZeroSignature demonstrates the one accepting
|
||||
// path a caller must defend against, and pins it so nobody has to rediscover
|
||||
// it.
|
||||
//
|
||||
// The verifier is cofactorless, exactly like RFC 8032, ref10, libsodium and the
|
||||
// Go standard library it delegates to: it checks [S]B == R + [k]A without ever
|
||||
// asking whether A generates the prime-order group. For a torsion point A the
|
||||
// discrete log is public (everyone "owns" the key), so the all-zero signature
|
||||
// (R = the order-4 point, S = 0) satisfies the equation whenever
|
||||
// k = SHA512(R || A || M) lands in the right residue class -- about one message
|
||||
// in four. An attacker who picks the message hash simply grinds it.
|
||||
//
|
||||
// This is not a defect in the arithmetic; diverging from every other Ed25519
|
||||
// stack would break the very external-chain signatures the precompile exists to
|
||||
// check. It IS a caller obligation: a contract that treats "verify succeeded"
|
||||
// as "a specific person signed" must bind the public key to a pre-registered
|
||||
// account, or reject torsion keys itself. Losing this test means the accepting
|
||||
// path moved, which is a consensus event either way.
|
||||
func TestKAT_SmallOrderKeyForgesWithZeroSignature(t *testing.T) {
|
||||
for _, enc := range smallOrder {
|
||||
pub := katHex(t, enc)
|
||||
// Confirm the encoding really is a torsion point: 8*P is the identity.
|
||||
p, err := new(edwards25519.Point).SetBytes(pub)
|
||||
require.NoError(t, err, "vector must decode")
|
||||
require.Equal(t, byte(1),
|
||||
new(edwards25519.Point).MultByCofactor(p).Bytes()[0],
|
||||
"vector must be small order")
|
||||
|
||||
// A signature made for a real key never transfers to a torsion key.
|
||||
for _, v := range rfc8032 {
|
||||
require.False(t, VerifyRaw(katHex(t, "af82"), katHex(t, v.sig), pub),
|
||||
"a genuine signature must not verify under key %s", enc)
|
||||
}
|
||||
}
|
||||
|
||||
// The forgery itself: the order-4 key with the all-zero signature, over
|
||||
// message hashes an attacker enumerates. Roughly a quarter land.
|
||||
pub := katHex(t, "0000000000000000000000000000000000000000000000000000000000000000")
|
||||
zeroSig := make([]byte, SignatureSize)
|
||||
|
||||
accepted := 0
|
||||
for i := range 64 {
|
||||
msg := make([]byte, MessageHashSize)
|
||||
msg[MessageHashSize-1] = byte(i)
|
||||
if VerifyRaw(msg, zeroSig, pub) {
|
||||
accepted++
|
||||
}
|
||||
}
|
||||
require.Positive(t, accepted,
|
||||
"cofactorless verification accepts the zero signature for a torsion key; "+
|
||||
"if this stopped being true the verifier changed semantics")
|
||||
require.Less(t, accepted, 64,
|
||||
"it must not accept every message -- that would be a broken equation, not cofactorlessness")
|
||||
|
||||
// And through the precompile, so the finding is stated where it bites.
|
||||
msg := make([]byte, MessageHashSize)
|
||||
msg[MessageHashSize-1] = 3
|
||||
require.True(t, VerifyRaw(msg, zeroSig, pub), "witness message must be one of the accepting ones")
|
||||
out, _, err := runPrecompile(t, buildRun(msg, zeroSig, pub), GasEd25519Verify)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, successResult, out,
|
||||
"precompile inherits the standard-library verdict for torsion keys")
|
||||
}
|
||||
|
||||
// TestKAT_SmallOrderRRejected substitutes a torsion point for R. The signature
|
||||
// no longer satisfies the equation for the published message.
|
||||
func TestKAT_SmallOrderRRejected(t *testing.T) {
|
||||
for _, v := range rfc8032 {
|
||||
pub, msg, sig := katHex(t, v.pub), katHex(t, v.msg), katHex(t, v.sig)
|
||||
for _, enc := range smallOrder {
|
||||
bad := append(append([]byte{}, katHex(t, enc)...), sig[32:]...)
|
||||
require.False(t, VerifyRaw(msg, bad, pub),
|
||||
"small-order R %s must not verify", enc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestKAT_NonCanonicalPointEncodingAccepted records, deliberately, that the
|
||||
// verifier follows ref10 and Go's standard library: a y-coordinate encoded as
|
||||
// y+p decodes to y, so one point has several accepted encodings. It is not a
|
||||
// forgery -- the signature still has to satisfy the equation -- but calldata is
|
||||
// therefore NOT a unique identifier for a (key, signature) pair. Any caller
|
||||
// keying a replay cache off raw calldata must canonicalise first. If this ever
|
||||
// starts failing, the verifier has diverged from every other Ed25519 stack and
|
||||
// that is a consensus event, not an improvement.
|
||||
func TestKAT_NonCanonicalPointEncodingAccepted(t *testing.T) {
|
||||
// p = 2^255 - 19; encodings of y and y+p decode to the same field element.
|
||||
for _, enc := range []string{
|
||||
"edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", // y = p -> 0
|
||||
"eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", // y = p+1 -> 1 (identity)
|
||||
} {
|
||||
_, err := new(edwards25519.Point).SetBytes(katHex(t, enc))
|
||||
require.NoError(t, err, "non-canonical encoding %s is accepted by the decoder", enc)
|
||||
}
|
||||
|
||||
// The identity has two accepted encodings; both are refused as public keys
|
||||
// because neither can carry a real signature.
|
||||
msg := katHex(t, "af82")
|
||||
for _, enc := range []string{
|
||||
"0100000000000000000000000000000000000000000000000000000000000000",
|
||||
"eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
|
||||
} {
|
||||
require.False(t, VerifyRaw(msg, katHex(t, rfc8032[2].sig), katHex(t, enc)))
|
||||
}
|
||||
}
|
||||
|
||||
// --- precompile surface -------------------------------------------------
|
||||
|
||||
// runPrecompile is the single call shape used below.
|
||||
func runPrecompile(tb testing.TB, in []byte, gas uint64) ([]byte, uint64, error) {
|
||||
tb.Helper()
|
||||
return Ed25519VerifyPrecompile.Run(nil, common.Address{}, ContractAddress, in, gas, true)
|
||||
}
|
||||
|
||||
// buildRun lays out the 128-byte calldata: msg(32) || sig(64) || pub(32).
|
||||
func buildRun(msg, sig, pub []byte) []byte {
|
||||
in := make([]byte, 0, InputLength)
|
||||
in = append(in, msg...)
|
||||
in = append(in, sig...)
|
||||
return append(in, pub...)
|
||||
}
|
||||
|
||||
// TestRun_AcceptsSignatureOverThirtyTwoByteMessage exercises the precompile's
|
||||
// fixed layout. The key comes from an RFC 8032 seed, so the public key half of
|
||||
// the calldata is still a published constant; only the message differs (the
|
||||
// precompile carries exactly 32 message bytes, and no RFC vector is that long).
|
||||
func TestRun_AcceptsSignatureOverThirtyTwoByteMessage(t *testing.T) {
|
||||
seed := katHex(t, rfc8032[0].seed)
|
||||
priv := stded.NewKeyFromSeed(seed)
|
||||
pub := []byte(priv.Public().(stded.PublicKey))
|
||||
require.Equal(t, katHex(t, rfc8032[0].pub), pub)
|
||||
|
||||
msg := katHex(t, "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20")
|
||||
require.Len(t, msg, MessageHashSize)
|
||||
sig := stded.Sign(priv, msg)
|
||||
|
||||
in := buildRun(msg, sig, pub)
|
||||
require.Len(t, in, InputLength)
|
||||
|
||||
out, left, err := runPrecompile(t, in, GasEd25519Verify+7)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, successResult, out)
|
||||
require.Equal(t, uint64(7), left, "exactly RequiredGas is deducted")
|
||||
|
||||
// Every single-byte mutation of every field must flip the verdict.
|
||||
for at := range in {
|
||||
bad := append([]byte{}, in...)
|
||||
bad[at] ^= 0x01
|
||||
out, _, err := runPrecompile(t, bad, GasEd25519Verify)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, out, "byte %d mutation must not verify", at)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRun_LengthRefusals covers one byte short, one byte long, empty and nil.
|
||||
// Gas is still charged: length is not known until the call is made.
|
||||
func TestRun_LengthRefusals(t *testing.T) {
|
||||
seed := katHex(t, rfc8032[1].seed)
|
||||
priv := stded.NewKeyFromSeed(seed)
|
||||
pub := []byte(priv.Public().(stded.PublicKey))
|
||||
msg := make([]byte, MessageHashSize)
|
||||
valid := buildRun(msg, stded.Sign(priv, msg), pub)
|
||||
require.Len(t, valid, InputLength)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
in []byte
|
||||
}{
|
||||
{"nil", nil},
|
||||
{"empty", []byte{}},
|
||||
{"one byte short", valid[:InputLength-1]},
|
||||
{"one byte long", append(append([]byte{}, valid...), 0)},
|
||||
{"truncated to 64", valid[:64]},
|
||||
{"all zero", make([]byte, InputLength)},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
out, left, err := runPrecompile(t, tc.in, GasEd25519Verify+11)
|
||||
require.NoError(t, err, "a wrong length is a failed verify, not a revert")
|
||||
require.Nil(t, out)
|
||||
require.Equal(t, uint64(11), left, "gas is charged regardless of length")
|
||||
})
|
||||
}
|
||||
|
||||
// The valid input at the exact length still verifies -- proves the refusals
|
||||
// above come from the length check and not from a broken vector.
|
||||
out, _, err := runPrecompile(t, valid, GasEd25519Verify)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, successResult, out)
|
||||
}
|
||||
|
||||
func TestRun_OutOfGasIsExact(t *testing.T) {
|
||||
in := make([]byte, InputLength)
|
||||
_, left, err := runPrecompile(t, in, GasEd25519Verify-1)
|
||||
require.ErrorIs(t, err, contract.ErrOutOfGas)
|
||||
require.Zero(t, left)
|
||||
|
||||
_, left, err = runPrecompile(t, in, GasEd25519Verify)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, left)
|
||||
}
|
||||
|
||||
// TestRequiredGas_IndependentOfInput: the work is fixed-size, so the price is
|
||||
// flat. A size-dependent price here would be wrong, not just different.
|
||||
func TestRequiredGas_IndependentOfInput(t *testing.T) {
|
||||
for _, n := range []int{0, 1, 127, 128, 129, 1 << 16} {
|
||||
require.Equal(t, uint64(GasEd25519Verify),
|
||||
Ed25519VerifyPrecompile.RequiredGas(make([]byte, n)), "len=%d", n)
|
||||
}
|
||||
require.Equal(t, uint64(GasEd25519Verify), Ed25519VerifyPrecompile.RequiredGas(nil))
|
||||
}
|
||||
|
||||
// TestVerifyHelpers_LengthGuards covers the exported convenience wrappers.
|
||||
func TestVerifyHelpers_LengthGuards(t *testing.T) {
|
||||
v := rfc8032[2]
|
||||
pub, msg, sig := katHex(t, v.pub), katHex(t, v.msg), katHex(t, v.sig)
|
||||
|
||||
require.True(t, Verify(msg, sig, pub))
|
||||
require.True(t, VerifyRaw(msg, sig, pub))
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
sig, pub []byte
|
||||
}{
|
||||
{"short pubkey", sig, pub[:31]},
|
||||
{"long pubkey", sig, append(append([]byte{}, pub...), 0)},
|
||||
{"nil pubkey", sig, nil},
|
||||
{"short sig", sig[:63], pub},
|
||||
{"long sig", append(append([]byte{}, sig...), 0), pub},
|
||||
{"nil sig", nil, pub},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.False(t, Verify(msg, tc.sig, tc.pub))
|
||||
require.False(t, VerifyRaw(msg, tc.sig, tc.pub))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- module + config ----------------------------------------------------
|
||||
|
||||
func TestRegistryModule_Wiring(t *testing.T) {
|
||||
require.Equal(t, ConfigKey, RegistryModule.ConfigKey)
|
||||
require.Equal(t, ContractAddress, RegistryModule.Address)
|
||||
require.Same(t, Ed25519VerifyPrecompile, RegistryModule.Contract)
|
||||
require.NotNil(t, RegistryModule.Configurator)
|
||||
|
||||
// The address is the LP-3211 slot; a silent move is a chain fork.
|
||||
require.Equal(t,
|
||||
common.HexToAddress("0x3211000000000000000000000000000000000000"),
|
||||
ContractAddress)
|
||||
}
|
||||
|
||||
func TestConfigurator_Surface(t *testing.T) {
|
||||
c := &configurator{}
|
||||
cfg := c.MakeConfig()
|
||||
require.IsType(t, &Config{}, cfg)
|
||||
require.NoError(t, c.Configure(nil, nil, nil, nil))
|
||||
}
|
||||
|
||||
func TestConfig_Surface(t *testing.T) {
|
||||
c := &Config{}
|
||||
require.Equal(t, ConfigKey, c.Key())
|
||||
require.Nil(t, c.Timestamp())
|
||||
require.False(t, c.IsDisabled())
|
||||
require.NoError(t, c.Verify(nil))
|
||||
|
||||
ts := uint64(4242)
|
||||
a := &Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts}}
|
||||
require.Equal(t, &ts, a.Timestamp())
|
||||
require.True(t, a.Equal(&Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts}}))
|
||||
|
||||
other := uint64(4243)
|
||||
require.False(t, a.Equal(&Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &other}}))
|
||||
require.False(t, a.Equal(nil), "a nil config is not equal to a set one")
|
||||
|
||||
d := &Config{Upgrade: precompileconfig.Upgrade{Disable: true}}
|
||||
require.True(t, d.IsDisabled())
|
||||
require.False(t, d.Equal(a))
|
||||
}
|
||||
|
||||
// TestSizeConstantsMatchLibrary is the guard that replaced the unreachable
|
||||
// length check deleted from Run: the slice bounds there are constants, so the
|
||||
// only way the extraction can go wrong is if these constants drift apart.
|
||||
func TestSizeConstantsMatchLibrary(t *testing.T) {
|
||||
require.Equal(t, luxed.PublicKeySize, PublicKeySize)
|
||||
require.Equal(t, luxed.SignatureSize, SignatureSize)
|
||||
require.Equal(t, MessageHashSize+SignatureSize+PublicKeySize, InputLength)
|
||||
}
|
||||
|
||||
// TestRegisterModuleIsIdempotentlyRefused exercises the failure the init()
|
||||
// panic exists for. The panic statement itself runs before any test can, so it
|
||||
// is unreachable from a test binary by construction; this asserts the condition
|
||||
// it fires on instead.
|
||||
func TestRegisterModuleIsIdempotentlyRefused(t *testing.T) {
|
||||
require.Error(t, modules.RegisterModule(RegistryModule),
|
||||
"registering the same address twice must be refused, which is what init() panics on")
|
||||
}
|
||||
+24
-12
@@ -45,6 +45,18 @@ var _ contract.StatefulPrecompiledContract = &hqcPrecompile{}
|
||||
// the ML-KEM precompile.
|
||||
const SeedSize = 32
|
||||
|
||||
// OpEncapsulate is the only operation. Decapsulate would need the private
|
||||
// key in calldata, which every validator can read.
|
||||
const OpEncapsulate uint8 = 0x01
|
||||
|
||||
// Mode bytes select the parameter set, mirroring the ML-KEM precompile's
|
||||
// ordering (128 / 192 / 256 as 0x00 / 0x01 / 0x02).
|
||||
const (
|
||||
ModeHQC128 uint8 = 0x00
|
||||
ModeHQC192 uint8 = 0x01
|
||||
ModeHQC256 uint8 = 0x02
|
||||
)
|
||||
|
||||
// Gas schedule. HQC encapsulation is dominated by polynomial multiplication
|
||||
// in F_2[X]/(X^n - 1) with n ≈ 17-58 K bits depending on mode. Charge a
|
||||
// flat base plus a per-byte cost.
|
||||
@@ -53,12 +65,13 @@ const (
|
||||
PerByteGas uint64 = 10
|
||||
)
|
||||
|
||||
// Error sentinels.
|
||||
// Error sentinels. A backend that is not wired reports itself: the
|
||||
// precompile passes luxfi/crypto/hqc.ErrBackendNotWired straight through
|
||||
// rather than restating it under a second name.
|
||||
var (
|
||||
ErrInvalidInputLength = errors.New("hqc: invalid input length")
|
||||
ErrInvalidMode = errors.New("hqc: invalid mode byte")
|
||||
ErrInvalidOperation = errors.New("hqc: invalid operation byte")
|
||||
ErrBackendNotWired = errors.New("hqc: backend not wired (see luxfi/crypto/hqc; build with hqc_pqclean or hqc_circl)")
|
||||
)
|
||||
|
||||
// deterministicReader — same construction as mlkem's. SHA-256 over
|
||||
@@ -101,8 +114,8 @@ func (*hqcPrecompile) RequiredGas(input []byte) uint64 {
|
||||
|
||||
// Wire format
|
||||
//
|
||||
// [1 byte op] 0x01 = Encapsulate
|
||||
// [1 byte mode] 0x00 = HQC-128, 0x01 = HQC-192, 0x02 = HQC-256
|
||||
// [1 byte op] OpEncapsulate
|
||||
// [1 byte mode] ModeHQC128 / ModeHQC192 / ModeHQC256
|
||||
// [32 bytes seed] deterministic-rng seed
|
||||
// [N bytes pk] HQC public key, N = params.PublicKeySize for mode
|
||||
//
|
||||
@@ -117,11 +130,10 @@ func (p *hqcPrecompile) Run(
|
||||
suppliedGas uint64,
|
||||
_ bool,
|
||||
) ([]byte, uint64, error) {
|
||||
gas := p.RequiredGas(input)
|
||||
if suppliedGas < gas {
|
||||
return nil, 0, contract.ErrOutOfGas
|
||||
remaining, err := contract.DeductGas(suppliedGas, p.RequiredGas(input))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
remaining := suppliedGas - gas
|
||||
|
||||
const minLen = 1 + 1 + SeedSize
|
||||
if len(input) < minLen {
|
||||
@@ -133,17 +145,17 @@ func (p *hqcPrecompile) Run(
|
||||
seed := input[2 : 2+SeedSize]
|
||||
pkBytes := input[2+SeedSize:]
|
||||
|
||||
if op != 0x01 {
|
||||
if op != OpEncapsulate {
|
||||
return nil, remaining, ErrInvalidOperation
|
||||
}
|
||||
|
||||
var mode hqc.Mode
|
||||
switch modeByte {
|
||||
case 0x00:
|
||||
case ModeHQC128:
|
||||
mode = hqc.HQC128
|
||||
case 0x01:
|
||||
case ModeHQC192:
|
||||
mode = hqc.HQC192
|
||||
case 0x02:
|
||||
case ModeHQC256:
|
||||
mode = hqc.HQC256
|
||||
default:
|
||||
return nil, remaining, ErrInvalidMode
|
||||
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package hqc
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/crypto/hqc"
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/precompile/contract"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Nothing is free, including the empty call.
|
||||
func TestGas_EveryInputCharges(t *testing.T) {
|
||||
for name, in := range map[string][]byte{
|
||||
"nil": nil,
|
||||
"empty": {},
|
||||
"op only": {OpEncapsulate},
|
||||
"unknown op": {0xEE, ModeHQC128},
|
||||
"bad mode": {OpEncapsulate, 0xEE},
|
||||
"valid frame": frame(ModeHQC128, 0x01),
|
||||
} {
|
||||
require.NotZerof(t, HQCPrecompile.RequiredGas(in), "%s is an unmetered branch", name)
|
||||
}
|
||||
require.Equal(t, BaseEncapsulateGas, HQCPrecompile.RequiredGas(nil))
|
||||
}
|
||||
|
||||
// The fee is base + len*k over the whole call, and it strictly rises with
|
||||
// length -- HQC's polynomial multiplication is over the key, so a bigger
|
||||
// key really is more work.
|
||||
func TestGas_RisesWithInputLength(t *testing.T) {
|
||||
prev := HQCPrecompile.RequiredGas(nil)
|
||||
for _, n := range []int{1, 2, 34, 2251, 4556, 7279, 1 << 20} {
|
||||
got := HQCPrecompile.RequiredGas(make([]byte, n))
|
||||
require.Equal(t, BaseEncapsulateGas+uint64(n)*PerByteGas, got, "length %d", n)
|
||||
require.Greaterf(t, got, prev, "length %d cost no more than the length before it", n)
|
||||
prev = got
|
||||
}
|
||||
}
|
||||
|
||||
// A larger parameter set costs more, because its key is longer and the fee
|
||||
// is charged over the call.
|
||||
func TestGas_RisesWithParameterSet(t *testing.T) {
|
||||
prev := uint64(0)
|
||||
for _, mode := range []uint8{ModeHQC128, ModeHQC192, ModeHQC256} {
|
||||
got := HQCPrecompile.RequiredGas(frame(mode, 0x01))
|
||||
require.Greaterf(t, got, prev, "mode 0x%02x cost no more than the set below it", mode)
|
||||
prev = got
|
||||
}
|
||||
}
|
||||
|
||||
// The per-byte term is multiplied by len(input), which is the size of a
|
||||
// buffer the caller actually paid the EVM to materialise -- not a number
|
||||
// it declared. The ML-DSA precompile next door needs a saturating cap
|
||||
// because its length is a 256-bit calldata field; this one cannot reach a
|
||||
// wrap, and here is the arithmetic that says so.
|
||||
func TestGas_LengthCannotWrapTheFee(t *testing.T) {
|
||||
wrapAt := (math.MaxUint64 - BaseEncapsulateGas) / PerByteGas
|
||||
require.Greater(t, wrapAt, uint64(1)<<60,
|
||||
"the fee would wrap at a length the EVM could plausibly reach")
|
||||
|
||||
// Everything the fee is ever asked about is many orders below that,
|
||||
// and the largest legitimate call is a rounding error against it.
|
||||
widest := len(frame(ModeHQC256, 0x01))
|
||||
require.Less(t, uint64(widest), wrapAt/(1<<40))
|
||||
require.Greater(t, HQCPrecompile.RequiredGas(frame(ModeHQC256, 0x01)), BaseEncapsulateGas)
|
||||
}
|
||||
|
||||
// The quoted fee is exactly what a call consumes, and one unit short is
|
||||
// refused before the input is even looked at.
|
||||
func TestGas_QuotedFeeIsCharged(t *testing.T) {
|
||||
in := frame(ModeHQC128, 0x01)
|
||||
fee := HQCPrecompile.RequiredGas(in)
|
||||
|
||||
_, remaining, err := HQCPrecompile.Run(nil, common.Address{}, ContractAddress, in, fee-1, true)
|
||||
require.ErrorIs(t, err, contract.ErrOutOfGas)
|
||||
require.Zero(t, remaining)
|
||||
|
||||
_, remaining, _ = HQCPrecompile.Run(nil, common.Address{}, ContractAddress, in, fee+6, true)
|
||||
require.Equal(t, uint64(6), remaining, "only the fee is consumed")
|
||||
}
|
||||
|
||||
// Refused input keeps its fee: a malformed call is not a free call.
|
||||
func TestGas_RefusalConsumesTheFee(t *testing.T) {
|
||||
for name, in := range map[string][]byte{
|
||||
"empty": {},
|
||||
"unknown op": {0xEE, ModeHQC128},
|
||||
"unknown mode": frame(ModeHQC128, 0x01),
|
||||
"short key": append([]byte{OpEncapsulate, ModeHQC128}, make([]byte, SeedSize+7)...),
|
||||
} {
|
||||
call := append([]byte{}, in...)
|
||||
if name == "unknown mode" {
|
||||
call[1] = 0xEE
|
||||
}
|
||||
fee := HQCPrecompile.RequiredGas(call)
|
||||
_, remaining, err := HQCPrecompile.Run(nil, common.Address{}, ContractAddress, call, fee+8, true)
|
||||
require.Errorf(t, err, "%s", name)
|
||||
require.Equalf(t, uint64(8), remaining, "%s: fee not consumed on refusal", name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGas_ZeroSuppliedIsRefused(t *testing.T) {
|
||||
for name, in := range map[string][]byte{
|
||||
"valid frame": frame(ModeHQC128, 0x01),
|
||||
"unknown op": {0xEE},
|
||||
"empty": {},
|
||||
} {
|
||||
_, remaining, err := HQCPrecompile.Run(nil, common.Address{}, ContractAddress, in, 0, true)
|
||||
require.ErrorIsf(t, err, contract.ErrOutOfGas, "%s", name)
|
||||
require.Zerof(t, remaining, "%s", name)
|
||||
}
|
||||
}
|
||||
|
||||
// The size constants this precompile frames against are the NIST IR 8528
|
||||
// ones, read from the wrapped library rather than restated here.
|
||||
func TestGas_ParameterSizesAreTheNISTOnes(t *testing.T) {
|
||||
for mode, want := range map[uint8][3]int{
|
||||
ModeHQC128: {2249, 4433, 64},
|
||||
ModeHQC192: {4522, 8978, 64},
|
||||
ModeHQC256: {7245, 14421, 64},
|
||||
} {
|
||||
p := hqc.MustParamsFor(modes[mode])
|
||||
require.Equalf(t, want[0], p.PublicKeySize, "mode 0x%02x public key", mode)
|
||||
require.Equalf(t, want[1], p.CiphertextSize, "mode 0x%02x ciphertext", mode)
|
||||
require.Equalf(t, want[2], p.SharedSecretSize, "mode 0x%02x shared secret", mode)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package hqc
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/crypto/hqc"
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/precompile/precompileconfig"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var modes = map[uint8]hqc.Mode{
|
||||
ModeHQC128: hqc.HQC128,
|
||||
ModeHQC192: hqc.HQC192,
|
||||
ModeHQC256: hqc.HQC256,
|
||||
}
|
||||
|
||||
// frame builds op || mode || seed(32) || pk of the mode's exact length.
|
||||
// The public key bytes are a fixed pattern: no backend inspects them
|
||||
// before the length is checked, and the length is what is under test.
|
||||
func frame(mode uint8, seed byte) []byte {
|
||||
params := hqc.MustParamsFor(modes[mode])
|
||||
in := make([]byte, 2+SeedSize+params.PublicKeySize)
|
||||
in[0] = OpEncapsulate
|
||||
in[1] = mode
|
||||
for i := range SeedSize {
|
||||
in[2+i] = seed
|
||||
}
|
||||
for i := 2 + SeedSize; i < len(in); i++ {
|
||||
in[i] = byte(i)
|
||||
}
|
||||
return in
|
||||
}
|
||||
|
||||
func try(t *testing.T, in []byte) ([]byte, error) {
|
||||
t.Helper()
|
||||
out, _, err := HQCPrecompile.Run(nil, common.Address{}, ContractAddress,
|
||||
in, HQCPrecompile.RequiredGas(in), true)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// Every mode byte reaches its own parameter set, and the length check that
|
||||
// follows uses that set's key size -- so the same calldata length is
|
||||
// valid for exactly one mode.
|
||||
func TestRefuse_EachModeFixesItsOwnKeySize(t *testing.T) {
|
||||
sizes := map[int]uint8{}
|
||||
for mode := range modes {
|
||||
size := hqc.MustParamsFor(modes[mode]).PublicKeySize
|
||||
prev, dup := sizes[size]
|
||||
require.Falsef(t, dup, "modes 0x%02x and 0x%02x share a key size", prev, mode)
|
||||
sizes[size] = mode
|
||||
}
|
||||
|
||||
for mode := range modes {
|
||||
in := frame(mode, 0x11)
|
||||
|
||||
// The mode's own length gets past the frame checks -- it fails, if
|
||||
// at all, inside the backend, never on shape.
|
||||
_, err := try(t, in)
|
||||
require.NotErrorIsf(t, err, ErrInvalidInputLength,
|
||||
"mode 0x%02x refused its own key length", mode)
|
||||
require.NotErrorIsf(t, err, ErrInvalidMode, "mode 0x%02x is not routed", mode)
|
||||
|
||||
// Relabelled to any other mode, the same bytes are the wrong length.
|
||||
for other := range modes {
|
||||
if other == mode {
|
||||
continue
|
||||
}
|
||||
relabelled := append([]byte{}, in...)
|
||||
relabelled[1] = other
|
||||
_, err := try(t, relabelled)
|
||||
require.ErrorIsf(t, err, ErrInvalidInputLength,
|
||||
"a 0x%02x key relabelled 0x%02x passed the length check", mode, other)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The frame is exact: one byte short and one byte long are both refused.
|
||||
func TestRefuse_FrameLengthIsExact(t *testing.T) {
|
||||
for mode := range modes {
|
||||
in := frame(mode, 0x22)
|
||||
_, err := try(t, in[:len(in)-1])
|
||||
require.ErrorIsf(t, err, ErrInvalidInputLength, "mode 0x%02x: one byte short", mode)
|
||||
|
||||
_, err = try(t, append(append([]byte{}, in...), 0x00))
|
||||
require.ErrorIsf(t, err, ErrInvalidInputLength, "mode 0x%02x: one byte long", mode)
|
||||
}
|
||||
}
|
||||
|
||||
// Calldata below the header minimum never reaches a mode lookup.
|
||||
func TestRefuse_TooShortToParse(t *testing.T) {
|
||||
for name, in := range map[string][]byte{
|
||||
"nil": nil,
|
||||
"empty": {},
|
||||
"op only": {OpEncapsulate},
|
||||
"no seed": {OpEncapsulate, ModeHQC128},
|
||||
"short seed": append([]byte{OpEncapsulate, ModeHQC128}, make([]byte, SeedSize-1)...),
|
||||
"seed only": append([]byte{OpEncapsulate, ModeHQC128}, make([]byte, SeedSize)...),
|
||||
} {
|
||||
_, err := try(t, in)
|
||||
require.ErrorIsf(t, err, ErrInvalidInputLength, "%s parsed", name)
|
||||
}
|
||||
}
|
||||
|
||||
// Encapsulate is the only operation; every other op byte is refused.
|
||||
// Decapsulate is absent on purpose -- it would need the private key in
|
||||
// calldata, which every validator can read.
|
||||
func TestRefuse_OnlyEncapsulateExists(t *testing.T) {
|
||||
body := frame(ModeHQC128, 0x33)
|
||||
for op := range 256 {
|
||||
if uint8(op) == OpEncapsulate {
|
||||
continue
|
||||
}
|
||||
in := append([]byte{}, body...)
|
||||
in[0] = byte(op)
|
||||
_, err := try(t, in)
|
||||
require.ErrorIsf(t, err, ErrInvalidOperation, "op 0x%02x was dispatched", op)
|
||||
}
|
||||
}
|
||||
|
||||
// Exactly three mode bytes exist; every other value is refused.
|
||||
func TestRefuse_ExactlyThreeModes(t *testing.T) {
|
||||
body := frame(ModeHQC128, 0x44)
|
||||
for m := range 256 {
|
||||
if _, ok := modes[uint8(m)]; ok {
|
||||
continue
|
||||
}
|
||||
in := append([]byte{}, body...)
|
||||
in[1] = byte(m)
|
||||
_, err := try(t, in)
|
||||
require.ErrorIsf(t, err, ErrInvalidMode, "mode 0x%02x was accepted", m)
|
||||
}
|
||||
}
|
||||
|
||||
// The op byte is checked before the mode byte, so calldata that is wrong
|
||||
// in both ways reports the op. Order matters only in that it is fixed:
|
||||
// a caller can rely on which complaint it gets.
|
||||
func TestRefuse_OperationIsCheckedBeforeMode(t *testing.T) {
|
||||
in := frame(ModeHQC128, 0x55)
|
||||
in[0] = 0xEE
|
||||
in[1] = 0xEE
|
||||
_, err := try(t, in)
|
||||
require.ErrorIs(t, err, ErrInvalidOperation)
|
||||
}
|
||||
|
||||
// The reader is a SHA-256 chain seeded by the caller's 32 bytes: the first
|
||||
// block is SHA-256(seed) and each block after is the hash of the one
|
||||
// before. Pinning it here is what makes the encapsulation reproducible on
|
||||
// every validator -- and it is the only place the chain is checked, since
|
||||
// no backend consumes it in the default build.
|
||||
func TestBinding_ReaderIsASHA256Chain(t *testing.T) {
|
||||
seed := make([]byte, SeedSize)
|
||||
for i := range seed {
|
||||
seed[i] = byte(0xC0 + i)
|
||||
}
|
||||
|
||||
want := []byte{}
|
||||
block := sha256.Sum256(seed)
|
||||
for range 4 {
|
||||
want = append(want, block[:]...)
|
||||
block = sha256.Sum256(block[:])
|
||||
}
|
||||
|
||||
got := make([]byte, len(want))
|
||||
n, err := newDeterministicReader(seed).Read(got)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(want), n)
|
||||
require.Equal(t, want, got)
|
||||
require.NotEqual(t, seed, got[:SeedSize], "the raw seed must not pass through unhashed")
|
||||
|
||||
// The stream does not depend on how the consumer asks for it.
|
||||
for _, chunk := range []int{1, 7, 16, 32, 33} {
|
||||
r := newDeterministicReader(seed)
|
||||
out := make([]byte, 0, len(want))
|
||||
for len(out) < len(want) {
|
||||
buf := make([]byte, min(chunk, len(want)-len(out)))
|
||||
n, err := r.Read(buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(buf), n)
|
||||
out = append(out, buf...)
|
||||
}
|
||||
require.Equalf(t, want, out, "stream differs when read %d bytes at a time", chunk)
|
||||
}
|
||||
|
||||
// Different seeds give different streams, and a short seed is
|
||||
// zero-extended rather than aliasing a longer one.
|
||||
other := append([]byte{}, seed...)
|
||||
other[0] ^= 0x01
|
||||
alt := make([]byte, 32)
|
||||
_, err = newDeterministicReader(other).Read(alt)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, got[:32], alt)
|
||||
}
|
||||
|
||||
// Two callers submitting the same calldata get the same answer: unlike the
|
||||
// ML-KEM and X-Wing precompiles, HQC derives its stream from the seed
|
||||
// alone. That is a real difference in the block and it is asserted here so
|
||||
// it is a decision rather than a drift.
|
||||
func TestBinding_SeedAloneDeterminesTheStream(t *testing.T) {
|
||||
seed := make([]byte, SeedSize)
|
||||
a := make([]byte, 64)
|
||||
b := make([]byte, 64)
|
||||
_, err := newDeterministicReader(seed).Read(a)
|
||||
require.NoError(t, err)
|
||||
_, err = newDeterministicReader(seed).Read(b)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, a, b)
|
||||
}
|
||||
|
||||
// --- module wiring ---
|
||||
|
||||
func TestConfig_Roundtrip(t *testing.T) {
|
||||
require.Equal(t, ConfigKey, (&Config{}).Key())
|
||||
require.Nil(t, (&Config{}).Timestamp())
|
||||
|
||||
ts := uint64(1234)
|
||||
c := &Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts}}
|
||||
require.Equal(t, &ts, c.Timestamp())
|
||||
|
||||
require.False(t, (&Config{}).IsDisabled())
|
||||
require.True(t, (&Config{Upgrade: precompileconfig.Upgrade{Disable: true}}).IsDisabled())
|
||||
|
||||
require.True(t, c.Equal(&Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts}}))
|
||||
other := uint64(5678)
|
||||
require.False(t, c.Equal(&Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &other}}))
|
||||
require.False(t, c.Equal(nil))
|
||||
require.NoError(t, c.Verify(nil))
|
||||
}
|
||||
|
||||
func TestConfigurator_MakesTheRightConfig(t *testing.T) {
|
||||
cfg := (&configurator{}).MakeConfig()
|
||||
_, ok := cfg.(*Config)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, ConfigKey, cfg.Key())
|
||||
require.NoError(t, (&configurator{}).Configure(nil, nil, nil, nil))
|
||||
}
|
||||
+10
-1
@@ -146,13 +146,22 @@ func decodePoint(curveID byte, data []byte) (point, error) {
|
||||
if len(data) < PointLen {
|
||||
return point{}, ErrInvalidInput
|
||||
}
|
||||
p := modulus(curveID)
|
||||
x := new(big.Int).SetBytes(data[:32])
|
||||
y := new(big.Int).SetBytes(data[32:64])
|
||||
// A field element has exactly one 32-byte encoding. Admitting x >= p lets
|
||||
// one point arrive under several encodings; addPoints compares raw integers,
|
||||
// so it then misses the doubling case and takes the chord branch, where
|
||||
// ModInverse of a multiple of p returns nil and leaves the divisor
|
||||
// untouched -- the discarded nil turns into a bogus slope and an off-curve
|
||||
// result. Refuse the extra encodings instead of reducing them.
|
||||
if x.Cmp(p) >= 0 || y.Cmp(p) >= 0 {
|
||||
return point{}, ErrInvalidInput
|
||||
}
|
||||
if x.Sign() == 0 && y.Sign() == 0 {
|
||||
return point{inf: true}, nil
|
||||
}
|
||||
// Verify on curve: y^2 = x^3 + 5 mod p
|
||||
p := modulus(curveID)
|
||||
lhs := new(big.Int).Mul(y, y)
|
||||
lhs.Mod(lhs, p)
|
||||
x3 := new(big.Int).Mul(x, x)
|
||||
|
||||
@@ -0,0 +1,578 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package pasta
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/precompile/contract"
|
||||
"github.com/luxfi/precompile/modules"
|
||||
"github.com/luxfi/precompile/precompileconfig"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func dec(tb testing.TB, s string) *big.Int {
|
||||
tb.Helper()
|
||||
v, ok := new(big.Int).SetString(s, 10)
|
||||
require.True(tb, ok, "bad decimal literal %q", s)
|
||||
return v
|
||||
}
|
||||
|
||||
func word(v *big.Int) []byte {
|
||||
b := make([]byte, 32)
|
||||
v.FillBytes(b)
|
||||
return b
|
||||
}
|
||||
|
||||
func words(x, y *big.Int) []byte { return append(word(x), word(y)...) }
|
||||
|
||||
func exec(tb testing.TB, curveID, op byte, args ...[]byte) ([]byte, uint64, error) {
|
||||
tb.Helper()
|
||||
in := []byte{curveID, op}
|
||||
for _, a := range args {
|
||||
in = append(in, a...)
|
||||
}
|
||||
return PastaPrecompile.Run(nil, common.Address{}, ContractAddress, in, 1<<24, true)
|
||||
}
|
||||
|
||||
// generator returns the Pasta generator (-1, 2), which satisfies
|
||||
// y^2 = x^3 + 5 as 4 = -1 + 5 on both curves.
|
||||
func generator(curveID byte) (*big.Int, *big.Int) {
|
||||
return new(big.Int).Sub(modulus(curveID), big.NewInt(1)), big.NewInt(2)
|
||||
}
|
||||
|
||||
// groupOrder returns the order of the curve's group. Pallas and Vesta form a
|
||||
// 2-cycle: each one's group order is the other's base field modulus.
|
||||
func groupOrder(curveID byte) *big.Int { return modulus(curveID ^ 0x03) }
|
||||
|
||||
// multiples holds k*G for both curves, computed with an independent
|
||||
// implementation of the short-Weierstrass chord-and-tangent law. Each was
|
||||
// checked to satisfy y^2 = x^3 + 5 and to have the published group order before
|
||||
// being written here.
|
||||
var multiples = map[byte]map[int][2]string{
|
||||
CurvePallas: {
|
||||
2: {"12664759760331458874453076485325239921471337210849432813230171084403110838275",
|
||||
"19449452489080454700052938888178047022259553573804486106032048451047634501628"},
|
||||
3: {"4027241023027617754036171531542546502751647131375064771810253584944963179107",
|
||||
"21762326383673887073830845720227757791980770399450032709429395080608314263493"},
|
||||
7: {"11597971188910290580510217765257817734852330887059848818989398919746936474521",
|
||||
"16072930402498746743190202080148926785915484279186972392222802455510868042413"},
|
||||
13: {"18071519724993305994823418956020412618942156734267125192164762840965296331733",
|
||||
"19818191382159177435510133618996785660947641416707759797575257840979978328136"},
|
||||
12345: {"18979344285946257891976342328653028961358577761991777536576238509026147226792",
|
||||
"16682595558766154259970161743373292552444720227662848748247144435751974152804"},
|
||||
},
|
||||
CurveVesta: {
|
||||
2: {"12664759760331458874453076485325239921471337210849470728609887452422096289795",
|
||||
"19449452489080454700052938888178047022259553573804544333222327159076790730748"},
|
||||
3: {"25090067966472946007446590780583652548116456464496053869245354133418193309279",
|
||||
"14485812765332067710838382555935059365898177416503303828814702067459945738374"},
|
||||
7: {"25239266693512118316897230459878239393039601456471569695166164382595970610905",
|
||||
"11285412150649616874720335020355225107778890555574340278102540384742252572629"},
|
||||
13: {"10156475246948922191667186682453517613403044314391612076993990862302619077531",
|
||||
"18230172829350593030385658896184831891431135274899809641065219710746269229566"},
|
||||
12345: {"20982986658818477740166501316994920625962009651848408555069289392931482296445",
|
||||
"492782312291387666733785462487308572527117237245988757616569697279745631012"},
|
||||
},
|
||||
}
|
||||
|
||||
func expected(tb testing.TB, curveID byte, k int) []byte {
|
||||
tb.Helper()
|
||||
v, ok := multiples[curveID][k]
|
||||
require.True(tb, ok, "no reference for %d", k)
|
||||
return words(dec(tb, v[0]), dec(tb, v[1]))
|
||||
}
|
||||
|
||||
func bothCurves(t *testing.T, f func(t *testing.T, curveID byte)) {
|
||||
t.Helper()
|
||||
for name, id := range map[string]byte{"pallas": CurvePallas, "vesta": CurveVesta} {
|
||||
t.Run(name, func(t *testing.T) { f(t, id) })
|
||||
}
|
||||
}
|
||||
|
||||
// TestKAT_GeneratorIsOnCurve pins the generator itself: (-1, 2) satisfies the
|
||||
// curve equation, and its order is the published group order.
|
||||
func TestKAT_GeneratorIsOnCurve(t *testing.T) {
|
||||
bothCurves(t, func(t *testing.T, id byte) {
|
||||
gx, gy := generator(id)
|
||||
p := modulus(id)
|
||||
|
||||
lhs := new(big.Int).Mod(new(big.Int).Mul(gy, gy), p)
|
||||
rhs := new(big.Int).Mod(new(big.Int).Add(new(big.Int).Exp(gx, big.NewInt(3), p), curveB), p)
|
||||
require.Zero(t, lhs.Cmp(rhs), "generator must satisfy y^2 = x^3 + 5")
|
||||
|
||||
// n*G is the point at infinity, encoded as 64 zero bytes.
|
||||
out, _, err := exec(t, id, OpScalarMul, words(gx, gy), word(groupOrder(id)))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, make([]byte, PointLen), out, "n*G must be the point at infinity")
|
||||
|
||||
// (n-1)*G is -G.
|
||||
out, _, err = exec(t, id, OpScalarMul, words(gx, gy),
|
||||
word(new(big.Int).Sub(groupOrder(id), big.NewInt(1))))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, words(gx, new(big.Int).Sub(p, gy)), out, "(n-1)*G must be -G")
|
||||
})
|
||||
}
|
||||
|
||||
// TestKAT_ScalarMul pins k*G against independently computed reference points.
|
||||
func TestKAT_ScalarMul(t *testing.T) {
|
||||
bothCurves(t, func(t *testing.T, id byte) {
|
||||
gx, gy := generator(id)
|
||||
G := words(gx, gy)
|
||||
for _, k := range []int{2, 3, 7, 13, 12345} {
|
||||
out, _, err := exec(t, id, OpScalarMul, G, word(big.NewInt(int64(k))))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected(t, id, k), out, "%d*G", k)
|
||||
}
|
||||
|
||||
// 1*G is G, and 0*G is infinity.
|
||||
out, _, err := exec(t, id, OpScalarMul, G, word(big.NewInt(1)))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, G, out)
|
||||
|
||||
out, _, err = exec(t, id, OpScalarMul, G, word(big.NewInt(0)))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, make([]byte, PointLen), out)
|
||||
})
|
||||
}
|
||||
|
||||
// TestKAT_PointAdd pins the addition law against the same reference points, and
|
||||
// ties it back to scalar multiplication.
|
||||
func TestKAT_PointAdd(t *testing.T) {
|
||||
bothCurves(t, func(t *testing.T, id byte) {
|
||||
gx, gy := generator(id)
|
||||
G := words(gx, gy)
|
||||
inf := make([]byte, PointLen)
|
||||
|
||||
out, _, err := exec(t, id, OpPointAdd, G, G)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected(t, id, 2), out, "G + G == 2G (the doubling branch)")
|
||||
|
||||
out, _, err = exec(t, id, OpPointAdd, expected(t, id, 2), G)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected(t, id, 3), out, "2G + G == 3G (the chord branch)")
|
||||
|
||||
// Commutative.
|
||||
ab, _, err := exec(t, id, OpPointAdd, expected(t, id, 3), expected(t, id, 7))
|
||||
require.NoError(t, err)
|
||||
ba, _, err := exec(t, id, OpPointAdd, expected(t, id, 7), expected(t, id, 3))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ab, ba)
|
||||
|
||||
// Infinity is neutral in both positions.
|
||||
for _, args := range [][2][]byte{{G, inf}, {inf, G}} {
|
||||
out, _, err := exec(t, id, OpPointAdd, args[0], args[1])
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, G, out)
|
||||
}
|
||||
out, _, err = exec(t, id, OpPointAdd, inf, inf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, inf, out)
|
||||
|
||||
// G + (-G) is infinity: the vertical-line branch.
|
||||
negG := words(gx, new(big.Int).Sub(modulus(id), gy))
|
||||
out, _, err = exec(t, id, OpPointAdd, G, negG)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, inf, out)
|
||||
|
||||
// Doubling a point whose y is zero would be a vertical tangent. No such
|
||||
// point exists on y^2 = x^3 + 5 over these fields (5 is not a cube), so
|
||||
// the branch is driven through infinity instead.
|
||||
out, _, err = exec(t, id, OpScalarMul, negG, word(groupOrder(id)))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, inf, out)
|
||||
})
|
||||
}
|
||||
|
||||
// TestKAT_MSM pins the multiscalar sum: 3*G + 5*(2G) is 13G.
|
||||
func TestKAT_MSM(t *testing.T) {
|
||||
bothCurves(t, func(t *testing.T, id byte) {
|
||||
gx, gy := generator(id)
|
||||
G := words(gx, gy)
|
||||
|
||||
out, _, err := exec(t, id, OpMSM,
|
||||
G, word(big.NewInt(3)),
|
||||
expected(t, id, 2), word(big.NewInt(5)))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected(t, id, 13), out, "3G + 5(2G) == 13G")
|
||||
|
||||
// Order of the pairs does not matter.
|
||||
swapped, _, err := exec(t, id, OpMSM,
|
||||
expected(t, id, 2), word(big.NewInt(5)),
|
||||
G, word(big.NewInt(3)))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, out, swapped)
|
||||
|
||||
// One pair degenerates to scalar multiplication.
|
||||
single, _, err := exec(t, id, OpMSM, G, word(big.NewInt(7)))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected(t, id, 7), single)
|
||||
|
||||
// All-zero scalars sum to infinity.
|
||||
zeros, _, err := exec(t, id, OpMSM,
|
||||
G, make([]byte, 32), expected(t, id, 2), make([]byte, 32))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, make([]byte, PointLen), zeros)
|
||||
})
|
||||
}
|
||||
|
||||
// TestKAT_CurvesAreDistinct guards the curve selector. Pallas and Vesta differ
|
||||
// only in their modulus, so a selector that was ignored -- or inverted -- would
|
||||
// otherwise go unnoticed on the generator, which is (-1, 2) for both.
|
||||
func TestKAT_CurvesAreDistinct(t *testing.T) {
|
||||
require.NotEqual(t, pallasP, vestaP)
|
||||
require.Equal(t, pallasP, modulus(CurvePallas))
|
||||
require.Equal(t, vestaP, modulus(CurveVesta))
|
||||
require.Equal(t, vestaP, groupOrder(CurvePallas), "Pallas's group order is Vesta's field")
|
||||
require.Equal(t, pallasP, groupOrder(CurveVesta), "Vesta's group order is Pallas's field")
|
||||
|
||||
for _, k := range []int{2, 3, 7, 13, 12345} {
|
||||
require.NotEqual(t, expected(t, CurvePallas, k), expected(t, CurveVesta, k),
|
||||
"%d*G must differ between the curves", k)
|
||||
}
|
||||
|
||||
// A Pallas point at high x is not a Vesta point and must be refused there.
|
||||
// pallasP < vestaP, so pallasP itself is a canonical Vesta coordinate that
|
||||
// is NOT canonical on Pallas.
|
||||
require.Negative(t, pallasP.Cmp(vestaP))
|
||||
}
|
||||
|
||||
// TestNonCanonicalCoordinateRejected is the regression for the defect this file
|
||||
// was written around.
|
||||
//
|
||||
// decodePoint used to accept any 32-byte word as a coordinate. Two encodings of
|
||||
// one point (x and x+p) therefore reached addPoints, which compares raw
|
||||
// integers: it missed the doubling case, took the chord branch, and there
|
||||
// ModInverse of a multiple of p returns nil while leaving its receiver
|
||||
// untouched. The discarded nil became a bogus slope and the precompile returned
|
||||
// a point that is NOT ON THE CURVE -- (2, p-2) where 2G was expected.
|
||||
func TestNonCanonicalCoordinateRejected(t *testing.T) {
|
||||
bothCurves(t, func(t *testing.T, id byte) {
|
||||
p := modulus(id)
|
||||
gx, gy := generator(id)
|
||||
G := words(gx, gy)
|
||||
|
||||
ncX := new(big.Int).Add(gx, p)
|
||||
require.LessOrEqual(t, ncX.BitLen(), 256, "the second encoding must fit a word")
|
||||
ghost := words(ncX, gy)
|
||||
require.NotEqual(t, G, ghost)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
args [][]byte
|
||||
op byte
|
||||
}{
|
||||
// The exact shape that produced an off-curve answer.
|
||||
{"add ghost + real", [][]byte{ghost, G}, OpPointAdd},
|
||||
{"add real + ghost", [][]byte{G, ghost}, OpPointAdd},
|
||||
{"scalarMul ghost", [][]byte{ghost, word(big.NewInt(2))}, OpScalarMul},
|
||||
{"msm ghost", [][]byte{ghost, word(big.NewInt(2))}, OpMSM},
|
||||
// y is a field element too.
|
||||
{"y + p", [][]byte{words(gx, new(big.Int).Add(gy, p)), G}, OpPointAdd},
|
||||
// x = p reduces to 0, y = p reduces to 0: the infinity spelling.
|
||||
{"x = p", [][]byte{words(p, gy), G}, OpPointAdd},
|
||||
{"y = p", [][]byte{words(gx, p), G}, OpPointAdd},
|
||||
{"both = p", [][]byte{words(p, p), G}, OpPointAdd},
|
||||
{"x = 2^256-1", [][]byte{
|
||||
words(new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1)), gy), G},
|
||||
OpPointAdd},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
out, _, err := exec(t, id, tc.op, tc.args...)
|
||||
require.ErrorIs(t, err, ErrInvalidInput,
|
||||
"a coordinate at or above p must be refused, not reduced")
|
||||
require.Nil(t, out)
|
||||
})
|
||||
}
|
||||
|
||||
// The canonical pair still works, so the refusals are attributable to
|
||||
// canonicality rather than to a broken fixture.
|
||||
out, _, err := exec(t, id, OpPointAdd, G, G)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected(t, id, 2), out)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEveryOutputIsOnCurve is the invariant the off-curve defect violated. It
|
||||
// sweeps the operations over reference and derived points and re-checks the
|
||||
// curve equation on every answer, independently of how it was produced.
|
||||
func TestEveryOutputIsOnCurve(t *testing.T) {
|
||||
bothCurves(t, func(t *testing.T, id byte) {
|
||||
p := modulus(id)
|
||||
gx, gy := generator(id)
|
||||
G := words(gx, gy)
|
||||
|
||||
onCurve := func(out []byte) {
|
||||
t.Helper()
|
||||
require.Len(t, out, PointLen)
|
||||
x := new(big.Int).SetBytes(out[:32])
|
||||
y := new(big.Int).SetBytes(out[32:])
|
||||
require.Negative(t, x.Cmp(p), "output x must be canonical")
|
||||
require.Negative(t, y.Cmp(p), "output y must be canonical")
|
||||
if x.Sign() == 0 && y.Sign() == 0 {
|
||||
return // point at infinity
|
||||
}
|
||||
lhs := new(big.Int).Mod(new(big.Int).Mul(y, y), p)
|
||||
rhs := new(big.Int).Mod(new(big.Int).Add(new(big.Int).Exp(x, big.NewInt(3), p), curveB), p)
|
||||
require.Zero(t, lhs.Cmp(rhs), "output (%s, %s) is not on the curve", x, y)
|
||||
}
|
||||
|
||||
acc := G
|
||||
for k := 1; k <= 24; k++ {
|
||||
out, _, err := exec(t, id, OpScalarMul, G, word(big.NewInt(int64(k))))
|
||||
require.NoError(t, err)
|
||||
onCurve(out)
|
||||
|
||||
sum, _, err := exec(t, id, OpPointAdd, acc, G)
|
||||
require.NoError(t, err)
|
||||
onCurve(sum)
|
||||
acc = sum
|
||||
|
||||
msm, _, err := exec(t, id, OpMSM, G, word(big.NewInt(int64(k))), acc, word(big.NewInt(3)))
|
||||
require.NoError(t, err)
|
||||
onCurve(msm)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestRefusal_PointNotOnCurve is the invalid-curve refusal: canonical
|
||||
// coordinates that do not satisfy the curve equation.
|
||||
func TestRefusal_PointNotOnCurve(t *testing.T) {
|
||||
bothCurves(t, func(t *testing.T, id byte) {
|
||||
gx, gy := generator(id)
|
||||
G := words(gx, gy)
|
||||
bad := words(gx, new(big.Int).Add(gy, big.NewInt(1)))
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
args [][]byte
|
||||
op byte
|
||||
}{
|
||||
{"add first", [][]byte{bad, G}, OpPointAdd},
|
||||
{"add second", [][]byte{G, bad}, OpPointAdd},
|
||||
{"scalarMul", [][]byte{bad, word(big.NewInt(3))}, OpScalarMul},
|
||||
{"msm", [][]byte{bad, word(big.NewInt(3))}, OpMSM},
|
||||
{"msm second pair", [][]byte{G, word(big.NewInt(1)), bad, word(big.NewInt(3))}, OpMSM},
|
||||
{"x only", [][]byte{words(gx, big.NewInt(0)), G}, OpPointAdd},
|
||||
{"y only", [][]byte{words(big.NewInt(0), gy), G}, OpPointAdd},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
out, _, err := exec(t, id, tc.op, tc.args...)
|
||||
require.ErrorIs(t, err, ErrNotOnCurve)
|
||||
require.Nil(t, out)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestRefusal_CurveSelector: only 0x01 and 0x02 name a curve.
|
||||
func TestRefusal_CurveSelector(t *testing.T) {
|
||||
gx, gy := generator(CurvePallas)
|
||||
G := words(gx, gy)
|
||||
for _, id := range []byte{0x00, 0x03, 0x04, 0x7F, 0x80, 0xFF} {
|
||||
out, _, err := exec(t, id, OpPointAdd, G, G)
|
||||
require.ErrorIs(t, err, ErrInvalidCurve, "selector %#x", id)
|
||||
require.Nil(t, out)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRefusal_Length walks every truncation and the unknown-opcode arm.
|
||||
func TestRefusal_Length(t *testing.T) {
|
||||
gx, gy := generator(CurvePallas)
|
||||
G := words(gx, gy)
|
||||
s := word(big.NewInt(3))
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
in []byte
|
||||
err error
|
||||
}{
|
||||
{"nil", nil, ErrInvalidInput},
|
||||
{"empty", []byte{}, ErrInvalidInput},
|
||||
{"curve byte only", []byte{CurvePallas}, ErrInvalidInput},
|
||||
{"header only", []byte{CurvePallas, OpPointAdd}, ErrInvalidInput},
|
||||
{"add one point", append([]byte{CurvePallas, OpPointAdd}, G...), ErrInvalidInput},
|
||||
{"add one byte short", append([]byte{CurvePallas, OpPointAdd}, append(append([]byte{}, G...), G[:PointLen-1]...)...), ErrInvalidInput},
|
||||
{"scalarMul no scalar", append([]byte{CurvePallas, OpScalarMul}, G...), ErrInvalidInput},
|
||||
{"scalarMul one byte short", append([]byte{CurvePallas, OpScalarMul}, append(append([]byte{}, G...), s[:31]...)...), ErrInvalidInput},
|
||||
{"msm empty", []byte{CurvePallas, OpMSM}, ErrInvalidInput},
|
||||
{"msm half pair", append([]byte{CurvePallas, OpMSM}, G...), ErrInvalidInput},
|
||||
{"msm one byte long", append([]byte{CurvePallas, OpMSM}, append(append(append([]byte{}, G...), s...), 0)...), ErrInvalidInput},
|
||||
{"msm one byte short", append([]byte{CurvePallas, OpMSM}, append(append([]byte{}, G...), s[:31]...)...), ErrInvalidInput},
|
||||
{"unknown op 0x00", []byte{CurvePallas, 0x00}, ErrInvalidOp},
|
||||
{"unknown op 0x04", []byte{CurvePallas, 0x04}, ErrInvalidOp},
|
||||
{"unknown op 0xff", append([]byte{CurvePallas, 0xFF}, G...), ErrInvalidOp},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
out, _, err := PastaPrecompile.Run(nil, common.Address{}, ContractAddress, tc.in, 1<<24, true)
|
||||
require.ErrorIs(t, err, tc.err)
|
||||
require.Nil(t, out)
|
||||
})
|
||||
}
|
||||
|
||||
// Trailing data past a fixed-width operand is ignored, not refused.
|
||||
short, _, err := exec(t, CurvePallas, OpPointAdd, G, G)
|
||||
require.NoError(t, err)
|
||||
long, _, err := exec(t, CurvePallas, OpPointAdd, G, G, []byte{0xAA})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, short, long)
|
||||
}
|
||||
|
||||
// TestGas_Schedule covers every arm, and proves MSM is priced per pair.
|
||||
func TestGas_Schedule(t *testing.T) {
|
||||
pair := PointLen + 32
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
in []byte
|
||||
want uint64
|
||||
}{
|
||||
{"nil", nil, 0},
|
||||
{"empty", []byte{}, 0},
|
||||
{"curve byte only", []byte{CurvePallas}, 0},
|
||||
{"pointAdd", []byte{CurvePallas, OpPointAdd}, GasPointAdd},
|
||||
{"scalarMul", []byte{CurveVesta, OpScalarMul}, GasScalarMul},
|
||||
{"unknown op 0x00", []byte{CurvePallas, 0x00}, 0},
|
||||
{"unknown op 0x04", []byte{CurvePallas, 0x04}, 0},
|
||||
{"unknown op 0xff", []byte{CurvePallas, 0xFF}, 0},
|
||||
{"msm no data", []byte{CurvePallas, OpMSM}, 0},
|
||||
{"msm short of a pair", append([]byte{CurvePallas, OpMSM}, make([]byte, pair-1)...), 0},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Equal(t, tc.want, PastaPrecompile.RequiredGas(tc.in))
|
||||
})
|
||||
}
|
||||
|
||||
// The price is set by the operation byte, not the curve byte.
|
||||
require.Equal(t,
|
||||
PastaPrecompile.RequiredGas([]byte{CurvePallas, OpScalarMul}),
|
||||
PastaPrecompile.RequiredGas([]byte{CurveVesta, OpScalarMul}))
|
||||
require.Equal(t, uint64(GasPointAdd),
|
||||
PastaPrecompile.RequiredGas([]byte{0xFF, OpPointAdd}),
|
||||
"an invalid curve still costs its operation: the work is decided later")
|
||||
|
||||
// Each additional pair costs exactly one per-pair unit.
|
||||
prev := uint64(0)
|
||||
for n := 1; n <= 32; n++ {
|
||||
in := append([]byte{CurvePallas, OpMSM}, make([]byte, n*pair)...)
|
||||
got := PastaPrecompile.RequiredGas(in)
|
||||
require.Equal(t, uint64(GasMSMBase+n*GasMSMPerPt), got, "n=%d", n)
|
||||
if n > 1 {
|
||||
require.Equal(t, uint64(GasMSMPerPt), got-prev)
|
||||
}
|
||||
prev = got
|
||||
}
|
||||
|
||||
// A large pair count neither wraps nor overflows: the count is derived from
|
||||
// the actual byte length, so it cannot be declared independently of the work.
|
||||
huge := append([]byte{CurvePallas, OpMSM}, make([]byte, 4096*pair)...)
|
||||
require.Equal(t, uint64(GasMSMBase+4096*GasMSMPerPt), PastaPrecompile.RequiredGas(huge))
|
||||
require.Greater(t, PastaPrecompile.RequiredGas(huge), uint64(GasMSMBase))
|
||||
|
||||
// Every zero-priced input must refuse.
|
||||
for _, in := range [][]byte{
|
||||
nil, {}, {CurvePallas}, {CurvePallas, 0x00}, {CurvePallas, 0x04}, {CurvePallas, 0xFF},
|
||||
{CurvePallas, OpMSM}, append([]byte{CurvePallas, OpMSM}, make([]byte, pair-1)...),
|
||||
} {
|
||||
require.Zero(t, PastaPrecompile.RequiredGas(in))
|
||||
out, _, err := PastaPrecompile.Run(nil, common.Address{}, ContractAddress, in, 0, true)
|
||||
require.Error(t, err, "a zero-gas input must not execute")
|
||||
require.Nil(t, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGas_Deduction(t *testing.T) {
|
||||
gx, gy := generator(CurvePallas)
|
||||
G := words(gx, gy)
|
||||
in := append([]byte{CurvePallas, OpPointAdd}, append(append([]byte{}, G...), G...)...)
|
||||
|
||||
_, left, err := PastaPrecompile.Run(nil, common.Address{}, ContractAddress, in, GasPointAdd+31, true)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(31), left)
|
||||
|
||||
_, left, err = PastaPrecompile.Run(nil, common.Address{}, ContractAddress, in, GasPointAdd, true)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, left)
|
||||
|
||||
_, left, err = PastaPrecompile.Run(nil, common.Address{}, ContractAddress, in, GasPointAdd-1, true)
|
||||
require.ErrorIs(t, err, contract.ErrOutOfGas)
|
||||
require.Zero(t, left)
|
||||
|
||||
// Two MSM pairs must be unaffordable at the one-pair price.
|
||||
pair := PointLen + 32
|
||||
two := append([]byte{CurvePallas, OpMSM}, make([]byte, 2*pair)...)
|
||||
_, _, err = PastaPrecompile.Run(nil, common.Address{}, ContractAddress, two,
|
||||
GasMSMBase+GasMSMPerPt, true)
|
||||
require.ErrorIs(t, err, contract.ErrOutOfGas)
|
||||
}
|
||||
|
||||
// --- module + config ----------------------------------------------------
|
||||
|
||||
func TestModule_Wiring(t *testing.T) {
|
||||
require.Equal(t, ConfigKey, Module.ConfigKey)
|
||||
require.Equal(t, ContractAddress, Module.Address)
|
||||
require.Same(t, PastaPrecompile, Module.Contract)
|
||||
require.NotNil(t, Module.Configurator)
|
||||
require.Equal(t,
|
||||
common.HexToAddress("0x0500000000000000000000000000000000000008"),
|
||||
ContractAddress)
|
||||
}
|
||||
|
||||
func TestConfigurator_Surface(t *testing.T) {
|
||||
c := &configurator{}
|
||||
require.IsType(t, &Config{}, c.MakeConfig())
|
||||
require.NoError(t, c.Configure(nil, nil, nil, nil))
|
||||
}
|
||||
|
||||
func TestConfig_Surface(t *testing.T) {
|
||||
c := &Config{}
|
||||
require.Equal(t, ConfigKey, c.Key())
|
||||
require.Nil(t, c.Timestamp())
|
||||
require.False(t, c.IsDisabled())
|
||||
require.NoError(t, c.Verify(nil))
|
||||
|
||||
ts := uint64(31)
|
||||
a := &Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts}}
|
||||
require.Equal(t, &ts, a.Timestamp())
|
||||
require.True(t, a.Equal(&Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts}}))
|
||||
|
||||
other := uint64(32)
|
||||
require.False(t, a.Equal(&Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &other}}))
|
||||
require.False(t, a.Equal(nil))
|
||||
require.True(t, (&Config{Upgrade: precompileconfig.Upgrade{Disable: true}}).IsDisabled())
|
||||
}
|
||||
|
||||
func TestRegisterModuleIsIdempotentlyRefused(t *testing.T) {
|
||||
require.Error(t, modules.RegisterModule(Module))
|
||||
}
|
||||
|
||||
// TestHelperGuards exercises the two defensive branches in the arithmetic
|
||||
// helpers directly. Both are unreachable through Run today -- every caller
|
||||
// slices exactly PointLen bytes before calling decodePoint, and the Pasta
|
||||
// groups have prime order so no point of order two (y = 0) exists, and the
|
||||
// ladder never hands doublePoint the identity. They stay because deleting them
|
||||
// turns a future mis-slice into a panic and a nil dereference rather than an
|
||||
// error, and they are asserted here so they cannot rot unnoticed.
|
||||
func TestHelperGuards(t *testing.T) {
|
||||
bothCurves(t, func(t *testing.T, id byte) {
|
||||
// decodePoint refuses a short slice instead of panicking on it.
|
||||
for _, n := range []int{0, 1, 31, 32, 63} {
|
||||
_, err := decodePoint(id, make([]byte, n))
|
||||
require.ErrorIs(t, err, ErrInvalidInput, "len=%d", n)
|
||||
}
|
||||
|
||||
// doublePoint of the identity is the identity.
|
||||
require.True(t, doublePoint(id, point{inf: true}).inf)
|
||||
|
||||
// A point with y = 0 has order two. No such point is on this curve, so
|
||||
// the guard is what stops the tangent slope from dividing by zero.
|
||||
require.True(t, doublePoint(id, point{x: big.NewInt(1), y: big.NewInt(0)}).inf)
|
||||
|
||||
// The prime group order is why no such point exists: it is odd, so the
|
||||
// group has no element of order two.
|
||||
require.Equal(t, uint(1), groupOrder(id).Bit(0), "group order must be odd")
|
||||
})
|
||||
}
|
||||
+17
-14
@@ -137,11 +137,12 @@ func (p *pedersenPrecompile) Run(
|
||||
}
|
||||
}
|
||||
|
||||
// commit: C = v*G + r*H
|
||||
func (p *pedersenPrecompile) commit(data []byte, gas uint64) ([]byte, uint64, error) {
|
||||
if len(data) < 64 {
|
||||
return nil, gas, ErrInvalidInput
|
||||
}
|
||||
// commitTo computes sha256(v*G + r*H) from 64 bytes of value || blinding.
|
||||
// It is the one place the commitment is defined; commit and verify both go
|
||||
// through it, so an opening can never be checked against a different formula
|
||||
// than the one that produced it. The multipliers are reduced modulo r, which is
|
||||
// exactly the order of G and H, so every 32-byte word names one scalar.
|
||||
func commitTo(data []byte) []byte {
|
||||
var v, r fr.Element
|
||||
v.SetBytes(data[:32])
|
||||
r.SetBytes(data[32:64])
|
||||
@@ -153,9 +154,16 @@ func (p *pedersenPrecompile) commit(data []byte, gas uint64) ([]byte, uint64, er
|
||||
var c bn254.G1Affine
|
||||
c.Add(&vG, &rH)
|
||||
|
||||
cBytes := c.Marshal()
|
||||
h := sha256.Sum256(cBytes)
|
||||
return h[:], gas, nil
|
||||
h := sha256.Sum256(c.Marshal())
|
||||
return h[:]
|
||||
}
|
||||
|
||||
// commit: C = v*G + r*H
|
||||
func (p *pedersenPrecompile) commit(data []byte, gas uint64) ([]byte, uint64, error) {
|
||||
if len(data) < 64 {
|
||||
return nil, gas, ErrInvalidInput
|
||||
}
|
||||
return commitTo(data[:64]), gas, nil
|
||||
}
|
||||
|
||||
// verify: check C == v*G + r*H
|
||||
@@ -163,13 +171,8 @@ func (p *pedersenPrecompile) verify(data []byte, gas uint64) ([]byte, uint64, er
|
||||
if len(data) < 96 {
|
||||
return nil, gas, ErrInvalidInput
|
||||
}
|
||||
commitHash := data[:32]
|
||||
recomputed, _, err := p.commit(data[32:96], gas)
|
||||
if err != nil {
|
||||
return nil, gas, err
|
||||
}
|
||||
result := make([]byte, 32)
|
||||
if equal(commitHash, recomputed) {
|
||||
if equal(data[:32], commitTo(data[32:96])) {
|
||||
result[31] = 1
|
||||
}
|
||||
return result, gas, nil
|
||||
|
||||
@@ -0,0 +1,570 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package pedersen
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/consensys/gnark-crypto/ecc/bn254"
|
||||
"github.com/consensys/gnark-crypto/ecc/bn254/fp"
|
||||
"github.com/consensys/gnark-crypto/ecc/bn254/fr"
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/precompile/contract"
|
||||
"github.com/luxfi/precompile/modules"
|
||||
"github.com/luxfi/precompile/precompileconfig"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func hexBytes(tb testing.TB, s string) []byte {
|
||||
tb.Helper()
|
||||
b, err := hex.DecodeString(s)
|
||||
require.NoError(tb, err)
|
||||
return b
|
||||
}
|
||||
|
||||
func scalar32(v *big.Int) []byte {
|
||||
b := make([]byte, 32)
|
||||
v.FillBytes(b)
|
||||
return b
|
||||
}
|
||||
|
||||
func fire(tb testing.TB, op byte, data ...[]byte) ([]byte, uint64, error) {
|
||||
tb.Helper()
|
||||
in := []byte{op}
|
||||
for _, d := range data {
|
||||
in = append(in, d...)
|
||||
}
|
||||
return PedersenPrecompile.Run(nil, common.Address{}, ContractAddress, in, 1<<24, true)
|
||||
}
|
||||
|
||||
// TestKAT_Generators pins the derived generators. They are the public
|
||||
// parameters of every commitment this precompile has ever produced: changing
|
||||
// one silently invalidates every stored commitment, so they belong in the test
|
||||
// suite as constants rather than as whatever the derivation happens to emit.
|
||||
func TestKAT_Generators(t *testing.T) {
|
||||
_, _, g1, _ := bn254.Generators()
|
||||
require.Equal(t, g1.Marshal(), genG.Marshal(), "G must be the standard BN254 G1 generator")
|
||||
require.Equal(t, hexBytes(t,
|
||||
"0000000000000000000000000000000000000000000000000000000000000001"+
|
||||
"0000000000000000000000000000000000000000000000000000000000000002"),
|
||||
genG.Marshal())
|
||||
|
||||
require.Equal(t, hexBytes(t,
|
||||
"167ee3e50591aab77ddfeb4fc1df8720a14033eedbe0209e7fc1c70c67399a52"+
|
||||
"248e4bb6192c1b605f29c4974d64e10a87bcabaa5df6e629bbdbc2695cc87a95"),
|
||||
genH.Marshal(), "H is a fixed public parameter")
|
||||
|
||||
require.Equal(t, hexBytes(t,
|
||||
"115cc397834e042c864f20e181a6f7706576fcd5dbdbd278de8811d454deeca2"+
|
||||
"2532e5ab8a42a4d941aa328297869daefc19f4101eac0223870080bde5df9baa"),
|
||||
genVi[0].Marshal())
|
||||
require.Equal(t, hexBytes(t,
|
||||
"2c68086f0f67176b240069897b5654c78f68157d2da4f478a5871ca248107f48"+
|
||||
"1b847c62886d1b9dc2ee6ef97e32622d5ab489f8f2650f7d5155e04e53560252"),
|
||||
genVi[31].Marshal())
|
||||
}
|
||||
|
||||
// TestGeneratorsAreValidAndIndependent is the binding precondition. A generator
|
||||
// off the curve, at infinity, or equal to another one destroys the binding
|
||||
// property: a caller could open one commitment to two values.
|
||||
func TestGeneratorsAreValidAndIndependent(t *testing.T) {
|
||||
all := append([]bn254.G1Affine{genG, genH}, genVi[:]...)
|
||||
require.Len(t, all, 34)
|
||||
|
||||
for i, g := range all {
|
||||
require.True(t, g.IsOnCurve(), "generator %d must be on the curve", i)
|
||||
require.False(t, g.IsInfinity(), "generator %d must not be the identity", i)
|
||||
// BN254's G1 has cofactor one, so on-curve already implies prime order;
|
||||
// asserting it makes that dependency explicit rather than assumed.
|
||||
require.True(t, g.IsInSubGroup(), "generator %d must have prime order", i)
|
||||
}
|
||||
|
||||
for i := range all {
|
||||
for j := i + 1; j < len(all); j++ {
|
||||
require.False(t, all[i].Equal(&all[j]),
|
||||
"generators %d and %d are equal: the commitment would not bind", i, j)
|
||||
neg := new(bn254.G1Affine).Neg(&all[j])
|
||||
require.False(t, all[i].Equal(neg),
|
||||
"generator %d is the negation of %d: their sum vanishes", i, j)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestHashToG1IsDeterministicAndOnCurve exercises the derivation itself,
|
||||
// including the rejection loop: about half of all candidate x values have no
|
||||
// square root, so most seeds take more than one try.
|
||||
func TestHashToG1IsDeterministicAndOnCurve(t *testing.T) {
|
||||
seeds := []string{
|
||||
"Lux_Pedersen_H_Generator",
|
||||
"", "a", "\x00", "0123456789abcdef0123456789abcdef",
|
||||
"Lux_Pedersen_Gen_A", "Lux_Pedersen_Gen_Z",
|
||||
}
|
||||
seen := map[string]string{}
|
||||
for _, s := range seeds {
|
||||
got := hashToG1(s)
|
||||
again := hashToG1(s)
|
||||
require.True(t, got.IsOnCurve(), "seed %q must land on the curve", s)
|
||||
require.False(t, got.IsInfinity())
|
||||
require.Equal(t, got.Marshal(), again.Marshal(), "derivation must be deterministic")
|
||||
|
||||
key := string(got.Marshal())
|
||||
prev, dup := seen[key]
|
||||
require.False(t, dup, "seeds %q and %q derive the same generator", prev, s)
|
||||
seen[key] = s
|
||||
}
|
||||
|
||||
// Reproduce the loop here to pin how many candidates each live seed needs.
|
||||
// The panic at 256 tries is unreachable for these fixed seeds: the worst of
|
||||
// the 34 finds a point on its eighth candidate, and failing 256 in a row has
|
||||
// probability about 2^-256 for any seed at all.
|
||||
tries := func(seed string) int {
|
||||
b := []byte(seed)
|
||||
for c := 0; c < 256; c++ {
|
||||
h := sha256.Sum256(append(b, byte(c)))
|
||||
var x, x2, x3, rhs, y, three fp.Element
|
||||
x.SetBytes(h[:])
|
||||
x2.Square(&x)
|
||||
x3.Mul(&x2, &x)
|
||||
three.SetInt64(3)
|
||||
rhs.Add(&x3, &three)
|
||||
if y.Sqrt(&rhs) != nil {
|
||||
var pt bn254.G1Affine
|
||||
pt.X, pt.Y = x, y
|
||||
if pt.IsOnCurve() && !pt.IsInfinity() {
|
||||
return c
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
worst := 0
|
||||
for i := range 32 {
|
||||
n := tries("Lux_Pedersen_Gen_" + string(rune('A'+i)))
|
||||
require.GreaterOrEqual(t, n, 0, "generator %d must be derivable", i)
|
||||
if n > worst {
|
||||
worst = n
|
||||
}
|
||||
}
|
||||
require.GreaterOrEqual(t, tries("Lux_Pedersen_H_Generator"), 0)
|
||||
require.Less(t, worst, 32,
|
||||
"the live seeds all resolve far below the 256-try limit the panic guards")
|
||||
}
|
||||
|
||||
// TestKAT_CommitIsTheHashOfTheGroupElement pins the commitment against the
|
||||
// group arithmetic it claims to perform, computed here rather than taken from
|
||||
// the precompile: C = sha256(marshal(v*G + r*H)).
|
||||
func TestKAT_CommitIsTheHashOfTheGroupElement(t *testing.T) {
|
||||
for _, tc := range []struct{ v, r int64 }{
|
||||
{0, 0}, {1, 0}, {0, 1}, {1, 1}, {7, 9}, {12345, 67890},
|
||||
} {
|
||||
v, r := big.NewInt(tc.v), big.NewInt(tc.r)
|
||||
|
||||
var vG, rH, c bn254.G1Affine
|
||||
vG.ScalarMultiplication(&genG, v)
|
||||
rH.ScalarMultiplication(&genH, r)
|
||||
c.Add(&vG, &rH)
|
||||
want := sha256.Sum256(c.Marshal())
|
||||
|
||||
got, _, err := fire(t, OpCommit, scalar32(v), scalar32(r))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, want[:], got, "v=%d r=%d", tc.v, tc.r)
|
||||
|
||||
// And Verify accepts exactly that opening.
|
||||
ok, _, err := fire(t, OpVerify, got, scalar32(v), scalar32(r))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, byte(1), ok[31])
|
||||
}
|
||||
}
|
||||
|
||||
// TestCommitIsHomomorphicInTheGroup states what the scheme actually gives and
|
||||
// what it does not. v*G + r*H is additively homomorphic, so committing to
|
||||
// v1+v2 with blinding r1+r2 lands on the sum of the two group elements -- but
|
||||
// Commit returns sha256 of that element, and Add operates on raw 64-byte
|
||||
// points, so the two operations do NOT compose through this API. A caller who
|
||||
// needs the homomorphism must keep the points, not the digests.
|
||||
func TestCommitIsHomomorphicInTheGroup(t *testing.T) {
|
||||
v1, r1 := big.NewInt(11), big.NewInt(22)
|
||||
v2, r2 := big.NewInt(33), big.NewInt(44)
|
||||
|
||||
point := func(v, r *big.Int) bn254.G1Affine {
|
||||
var vG, rH, c bn254.G1Affine
|
||||
vG.ScalarMultiplication(&genG, v)
|
||||
rH.ScalarMultiplication(&genH, r)
|
||||
c.Add(&vG, &rH)
|
||||
return c
|
||||
}
|
||||
c1, c2 := point(v1, r1), point(v2, r2)
|
||||
|
||||
var sum bn254.G1Affine
|
||||
sum.Add(&c1, &c2)
|
||||
combined := point(new(big.Int).Add(v1, v2), new(big.Int).Add(r1, r2))
|
||||
require.Equal(t, combined.Marshal(), sum.Marshal(),
|
||||
"C(v1,r1) + C(v2,r2) == C(v1+v2, r1+r2)")
|
||||
|
||||
// Add reproduces that sum on the wire.
|
||||
added, _, err := fire(t, OpAdd, c1.Marshal(), c2.Marshal())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, sum.Marshal(), added)
|
||||
|
||||
// The digests do not add: Commit's output is a hash, by construction.
|
||||
d1, _, err := fire(t, OpCommit, scalar32(v1), scalar32(r1))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, d1, 32)
|
||||
_, _, err = fire(t, OpAdd, d1, d1)
|
||||
require.ErrorIs(t, err, ErrInvalidInput,
|
||||
"a commitment digest is not a point and Add must refuse it")
|
||||
|
||||
// Verify only accepts the exact opening: any other one fails.
|
||||
digest, _, err := fire(t, OpCommit, scalar32(v1), scalar32(r1))
|
||||
require.NoError(t, err)
|
||||
for _, bad := range [][2]*big.Int{
|
||||
{new(big.Int).Add(v1, big.NewInt(1)), r1},
|
||||
{v1, new(big.Int).Add(r1, big.NewInt(1))},
|
||||
{r1, v1},
|
||||
{big.NewInt(0), big.NewInt(0)},
|
||||
} {
|
||||
out, _, err := fire(t, OpVerify, digest, scalar32(bad[0]), scalar32(bad[1]))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, byte(0), out[31], "a wrong opening must not verify")
|
||||
}
|
||||
}
|
||||
|
||||
// TestScalarsReduceModTheGroupOrder documents why reduction is correct here,
|
||||
// unlike for a coordinate. G and H have order r, so (v+r)*G == v*G as group
|
||||
// elements: the multiplier's domain IS the scalar field, and two words that
|
||||
// differ by r name the same scalar. Nothing is lost by reducing.
|
||||
func TestScalarsReduceModTheGroupOrder(t *testing.T) {
|
||||
r := fr.Modulus()
|
||||
require.Equal(t, r.String(), bn254.ID.ScalarField().String(),
|
||||
"the reduction modulus must be the G1 group order")
|
||||
|
||||
base, _, err := fire(t, OpCommit, scalar32(big.NewInt(7)), scalar32(big.NewInt(9)))
|
||||
require.NoError(t, err)
|
||||
|
||||
shifted, _, err := fire(t, OpCommit,
|
||||
scalar32(new(big.Int).Add(r, big.NewInt(7))), scalar32(big.NewInt(9)))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, base, shifted, "(7+r)*G == 7*G: same scalar, same commitment")
|
||||
|
||||
// A different scalar still gives a different commitment.
|
||||
other, _, err := fire(t, OpCommit,
|
||||
scalar32(new(big.Int).Add(r, big.NewInt(8))), scalar32(big.NewInt(9)))
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, base, other)
|
||||
}
|
||||
|
||||
// TestKAT_VectorCommit pins the vector commitment against the same arithmetic
|
||||
// computed here: C = sum(v_i * V_i) + r*H, hashed.
|
||||
func TestKAT_VectorCommit(t *testing.T) {
|
||||
for _, n := range []int{0, 1, 2, 5, 31, 32} {
|
||||
vals := make([]*big.Int, n)
|
||||
payload := []byte{byte(n)}
|
||||
for i := range n {
|
||||
vals[i] = big.NewInt(int64(i*7 + 1))
|
||||
payload = append(payload, scalar32(vals[i])...)
|
||||
}
|
||||
blind := big.NewInt(int64(n) + 100)
|
||||
payload = append(payload, scalar32(blind)...)
|
||||
|
||||
var sum bn254.G1Jac
|
||||
for i := range n {
|
||||
var viG bn254.G1Affine
|
||||
viG.ScalarMultiplication(&genVi[i], vals[i])
|
||||
var j bn254.G1Jac
|
||||
j.FromAffine(&viG)
|
||||
sum.AddAssign(&j)
|
||||
}
|
||||
var rH bn254.G1Affine
|
||||
rH.ScalarMultiplication(&genH, blind)
|
||||
var rHJac bn254.G1Jac
|
||||
rHJac.FromAffine(&rH)
|
||||
sum.AddAssign(&rHJac)
|
||||
var affine bn254.G1Affine
|
||||
affine.FromJacobian(&sum)
|
||||
want := sha256.Sum256(affine.Marshal())
|
||||
|
||||
got, _, err := fire(t, OpVectorCommit, payload)
|
||||
require.NoError(t, err, "n=%d", n)
|
||||
require.Equal(t, want[:], got, "n=%d", n)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVectorCommitBindsPositionAndValue: swapping two values must change the
|
||||
// commitment, which is only true if each slot uses its own generator.
|
||||
func TestVectorCommitBindsPosition(t *testing.T) {
|
||||
build := func(vals ...int64) []byte {
|
||||
p := []byte{byte(len(vals))}
|
||||
for _, v := range vals {
|
||||
p = append(p, scalar32(big.NewInt(v))...)
|
||||
}
|
||||
return append(p, scalar32(big.NewInt(5))...)
|
||||
}
|
||||
a, _, err := fire(t, OpVectorCommit, build(1, 2, 3))
|
||||
require.NoError(t, err)
|
||||
b, _, err := fire(t, OpVectorCommit, build(3, 2, 1))
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, a, b, "a permutation of the vector must change the commitment")
|
||||
|
||||
c, _, err := fire(t, OpVectorCommit, build(1, 2))
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, a, c, "the length must be bound too")
|
||||
}
|
||||
|
||||
// TestRefusal_AddPointValidation is the invalid-curve refusal. gnark's decoder
|
||||
// checks canonicality, curve membership and subgroup membership, and Add must
|
||||
// surface every one of those as a refusal rather than compute on the result.
|
||||
func TestRefusal_AddPointValidation(t *testing.T) {
|
||||
_, _, g1, _ := bn254.Generators()
|
||||
good := g1.Marshal()
|
||||
require.Len(t, good, 128/2)
|
||||
|
||||
offCurve := append([]byte{}, good...)
|
||||
offCurve[63] ^= 0x01
|
||||
var probe bn254.G1Affine
|
||||
require.Error(t, probe.Unmarshal(offCurve), "fixture must really be invalid")
|
||||
|
||||
xPlusP := make([]byte, 64)
|
||||
new(big.Int).Add(new(big.Int).SetBytes(good[:32]), fp.Modulus()).FillBytes(xPlusP[:32])
|
||||
copy(xPlusP[32:], good[32:])
|
||||
|
||||
xEqualsP := make([]byte, 64)
|
||||
fp.Modulus().FillBytes(xEqualsP[:32])
|
||||
copy(xEqualsP[32:], good[32:])
|
||||
|
||||
allOnes := make([]byte, 64)
|
||||
for i := range allOnes {
|
||||
allOnes[i] = 0xFF
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
bad []byte
|
||||
}{
|
||||
{"off curve", offCurve},
|
||||
{"x + p is not canonical", xPlusP},
|
||||
{"x = p is not canonical", xEqualsP},
|
||||
{"all ones", allOnes},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
for _, args := range [][2][]byte{{tc.bad, good}, {good, tc.bad}, {tc.bad, tc.bad}} {
|
||||
out, _, err := fire(t, OpAdd, args[0], args[1])
|
||||
require.ErrorIs(t, err, ErrInvalidInput)
|
||||
require.Nil(t, out)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// The point at infinity is a legal encoding and is the additive identity.
|
||||
inf := make([]byte, 64)
|
||||
out, _, err := fire(t, OpAdd, good, inf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, good, out)
|
||||
out, _, err = fire(t, OpAdd, inf, inf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, inf, out)
|
||||
|
||||
// G + (-G) is infinity.
|
||||
neg := new(bn254.G1Affine).Neg(&g1)
|
||||
out, _, err = fire(t, OpAdd, good, neg.Marshal())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, inf, out)
|
||||
}
|
||||
|
||||
// TestRefusal_Shape walks every truncation, oversize and selector refusal.
|
||||
func TestRefusal_Shape(t *testing.T) {
|
||||
_, _, g1, _ := bn254.Generators()
|
||||
good := g1.Marshal()
|
||||
v := scalar32(big.NewInt(1))
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
in []byte
|
||||
err error
|
||||
}{
|
||||
{"nil", nil, ErrInvalidInput},
|
||||
{"empty", []byte{}, ErrInvalidInput},
|
||||
{"commit op only", []byte{OpCommit}, ErrInvalidInput},
|
||||
{"commit one scalar", append([]byte{OpCommit}, v...), ErrInvalidInput},
|
||||
{"commit one byte short", append([]byte{OpCommit}, make([]byte, 63)...), ErrInvalidInput},
|
||||
{"verify op only", []byte{OpVerify}, ErrInvalidInput},
|
||||
{"verify one byte short", append([]byte{OpVerify}, make([]byte, 95)...), ErrInvalidInput},
|
||||
{"add op only", []byte{OpAdd}, ErrInvalidInput},
|
||||
{"add one point", append([]byte{OpAdd}, good...), ErrInvalidInput},
|
||||
{"add one byte short", append([]byte{OpAdd}, make([]byte, 127)...), ErrInvalidInput},
|
||||
{"vector op only", []byte{OpVectorCommit}, ErrInvalidInput},
|
||||
{"vector n=1 no data", []byte{OpVectorCommit, 1}, ErrInvalidInput},
|
||||
{"vector n=1 short", append([]byte{OpVectorCommit, 1}, make([]byte, 63)...), ErrInvalidInput},
|
||||
{"vector n=0 no blinding", []byte{OpVectorCommit, 0}, ErrInvalidInput},
|
||||
{"vector n=33", append([]byte{OpVectorCommit, 33}, make([]byte, 33*32+32)...), ErrTooManyVals},
|
||||
{"vector n=255", append([]byte{OpVectorCommit, 255}, make([]byte, 255*32+32)...), ErrTooManyVals},
|
||||
{"unknown op 0x00", []byte{0x00}, ErrInvalidOp},
|
||||
{"unknown op 0x05", []byte{0x05}, ErrInvalidOp},
|
||||
{"unknown op 0xff", []byte{0xFF}, ErrInvalidOp},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
out, _, err := PedersenPrecompile.Run(nil, common.Address{}, ContractAddress, tc.in, 1<<24, true)
|
||||
require.ErrorIs(t, err, tc.err)
|
||||
require.Nil(t, out)
|
||||
})
|
||||
}
|
||||
|
||||
// The boundaries either side of each refusal must work.
|
||||
out, _, err := fire(t, OpVectorCommit, append([]byte{32}, make([]byte, 32*32+32)...))
|
||||
require.NoError(t, err, "32 values is the maximum width")
|
||||
require.Len(t, out, 32)
|
||||
|
||||
out, _, err = fire(t, OpVectorCommit, append([]byte{0}, make([]byte, 32)...))
|
||||
require.NoError(t, err, "a zero-length vector is just the blinding term")
|
||||
require.Len(t, out, 32)
|
||||
|
||||
// Trailing data past a fixed operand is ignored, not refused.
|
||||
short, _, err := fire(t, OpCommit, v, v)
|
||||
require.NoError(t, err)
|
||||
long, _, err := fire(t, OpCommit, v, v, []byte{0xAA})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, short, long)
|
||||
}
|
||||
|
||||
// TestEqualIsLengthCheckedAndConstantTime covers the comparison used by Verify.
|
||||
func TestEqualIsLengthChecked(t *testing.T) {
|
||||
require.True(t, equal(nil, nil))
|
||||
require.True(t, equal([]byte{}, []byte{}))
|
||||
require.True(t, equal([]byte{1, 2, 3}, []byte{1, 2, 3}))
|
||||
|
||||
require.False(t, equal([]byte{1, 2, 3}, []byte{1, 2}), "length mismatch must not compare equal")
|
||||
require.False(t, equal([]byte{1, 2}, []byte{1, 2, 3}))
|
||||
require.False(t, equal(nil, []byte{0}))
|
||||
require.False(t, equal([]byte{0}, nil))
|
||||
require.False(t, equal([]byte{1, 2, 3}, []byte{1, 2, 4}), "a difference in the last byte counts")
|
||||
require.False(t, equal([]byte{1, 2, 3}, []byte{9, 2, 3}), "a difference in the first byte counts")
|
||||
|
||||
// It accumulates rather than returning early, so every byte is read.
|
||||
a := make([]byte, 32)
|
||||
b := make([]byte, 32)
|
||||
b[0] = 1
|
||||
require.False(t, equal(a, b))
|
||||
b[0], b[31] = 0, 1
|
||||
require.False(t, equal(a, b))
|
||||
}
|
||||
|
||||
// TestGas_Schedule covers every arm and proves nothing is bought for free.
|
||||
func TestGas_Schedule(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
in []byte
|
||||
want uint64
|
||||
}{
|
||||
{"nil", nil, 0},
|
||||
{"empty", []byte{}, 0},
|
||||
{"commit", []byte{OpCommit}, GasCommit},
|
||||
{"verify", []byte{OpVerify}, GasVerify},
|
||||
{"add", []byte{OpAdd}, GasAdd},
|
||||
{"vector op only", []byte{OpVectorCommit}, 0},
|
||||
{"vector n=0", []byte{OpVectorCommit, 0}, GasVectorBase},
|
||||
{"vector n=1", []byte{OpVectorCommit, 1}, GasVectorBase + GasVectorPerVal},
|
||||
{"vector n=32", []byte{OpVectorCommit, 32}, GasVectorBase + 32*GasVectorPerVal},
|
||||
{"vector n=255", []byte{OpVectorCommit, 255}, GasVectorBase + 255*GasVectorPerVal},
|
||||
{"unknown 0x00", []byte{0x00}, 0},
|
||||
{"unknown 0x05", []byte{0x05}, 0},
|
||||
{"unknown 0xff", []byte{0xFF}, 0},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Equal(t, tc.want, PedersenPrecompile.RequiredGas(tc.in))
|
||||
})
|
||||
}
|
||||
|
||||
// The vector price rises by one unit per declared value, and the declared
|
||||
// count is a single byte, so it can never overflow the accumulator.
|
||||
prev := uint64(GasVectorBase)
|
||||
for n := 1; n <= 255; n++ {
|
||||
got := PedersenPrecompile.RequiredGas([]byte{OpVectorCommit, byte(n)})
|
||||
require.Equal(t, uint64(GasVectorBase+n*GasVectorPerVal), got, "n=%d", n)
|
||||
require.Equal(t, uint64(GasVectorPerVal), got-prev)
|
||||
prev = got
|
||||
}
|
||||
require.Greater(t, PedersenPrecompile.RequiredGas([]byte{OpVectorCommit, 255}),
|
||||
uint64(GasVectorBase), "the maximum declared count must not wrap")
|
||||
|
||||
// A count above the 32-value limit is charged and then refused: the caller
|
||||
// pays for the width it declared, so over-declaring buys nothing.
|
||||
_, _, err := PedersenPrecompile.Run(nil, common.Address{}, ContractAddress,
|
||||
append([]byte{OpVectorCommit, 255}, make([]byte, 255*32+32)...),
|
||||
uint64(GasVectorBase+255*GasVectorPerVal), true)
|
||||
require.ErrorIs(t, err, ErrTooManyVals)
|
||||
|
||||
// The fixed-width arms do not move with input size.
|
||||
require.Equal(t, uint64(GasCommit),
|
||||
PedersenPrecompile.RequiredGas(append([]byte{OpCommit}, make([]byte, 1<<16)...)))
|
||||
|
||||
// Every zero-priced input must refuse.
|
||||
for _, in := range [][]byte{nil, {}, {OpVectorCommit}, {0x00}, {0x05}, {0xFF}} {
|
||||
require.Zero(t, PedersenPrecompile.RequiredGas(in))
|
||||
out, _, err := PedersenPrecompile.Run(nil, common.Address{}, ContractAddress, in, 0, true)
|
||||
require.Error(t, err, "a zero-gas input must not execute")
|
||||
require.Nil(t, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGas_Deduction(t *testing.T) {
|
||||
v := scalar32(big.NewInt(3))
|
||||
in := append([]byte{OpCommit}, append(append([]byte{}, v...), v...)...)
|
||||
|
||||
_, left, err := PedersenPrecompile.Run(nil, common.Address{}, ContractAddress, in, GasCommit+23, true)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(23), left)
|
||||
|
||||
_, left, err = PedersenPrecompile.Run(nil, common.Address{}, ContractAddress, in, GasCommit, true)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, left)
|
||||
|
||||
_, left, err = PedersenPrecompile.Run(nil, common.Address{}, ContractAddress, in, GasCommit-1, true)
|
||||
require.ErrorIs(t, err, contract.ErrOutOfGas)
|
||||
require.Zero(t, left)
|
||||
|
||||
// Verify costs more than Commit because it commits and then compares.
|
||||
require.Greater(t, uint64(GasVerify), uint64(GasCommit))
|
||||
}
|
||||
|
||||
// --- module + config ----------------------------------------------------
|
||||
|
||||
func TestModule_Wiring(t *testing.T) {
|
||||
require.Equal(t, ConfigKey, Module.ConfigKey)
|
||||
require.Equal(t, ContractAddress, Module.Address)
|
||||
require.Same(t, PedersenPrecompile, Module.Contract)
|
||||
require.NotNil(t, Module.Configurator)
|
||||
require.Equal(t,
|
||||
common.HexToAddress("0x0500000000000000000000000000000000000006"),
|
||||
ContractAddress)
|
||||
}
|
||||
|
||||
func TestConfigurator_Surface(t *testing.T) {
|
||||
c := &configurator{}
|
||||
require.IsType(t, &Config{}, c.MakeConfig())
|
||||
require.NoError(t, c.Configure(nil, nil, nil, nil))
|
||||
}
|
||||
|
||||
func TestConfig_Surface(t *testing.T) {
|
||||
c := &Config{}
|
||||
require.Equal(t, ConfigKey, c.Key())
|
||||
require.Nil(t, c.Timestamp())
|
||||
require.False(t, c.IsDisabled())
|
||||
require.NoError(t, c.Verify(nil))
|
||||
|
||||
ts := uint64(51)
|
||||
a := &Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts}}
|
||||
require.Equal(t, &ts, a.Timestamp())
|
||||
require.True(t, a.Equal(&Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts}}))
|
||||
|
||||
other := uint64(52)
|
||||
require.False(t, a.Equal(&Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &other}}))
|
||||
require.False(t, a.Equal(nil))
|
||||
require.True(t, (&Config{Upgrade: precompileconfig.Upgrade{Disable: true}}).IsDisabled())
|
||||
}
|
||||
|
||||
func TestRegisterModuleIsIdempotentlyRefused(t *testing.T) {
|
||||
require.Error(t, modules.RegisterModule(Module))
|
||||
}
|
||||
+44
-10
@@ -47,8 +47,25 @@ const (
|
||||
GasHashPair = 700
|
||||
GasSpongeBase = 600
|
||||
GasSpongePerIn = 100
|
||||
|
||||
// MaxSpongeOutput caps the squeeze in bytes.
|
||||
MaxSpongeOutput = 1024
|
||||
)
|
||||
|
||||
// readElement decodes one field element.
|
||||
//
|
||||
// The input domain is F_r, so each element has exactly one 32-byte encoding.
|
||||
// A word >= r is refused rather than reduced: reduction made x and x+r hash
|
||||
// identically, i.e. a second preimage anyone could compute by addition, which
|
||||
// is fatal for the Merkle and nullifier uses this hash exists for.
|
||||
func readElement(b []byte) (fr.Element, error) {
|
||||
var e fr.Element
|
||||
if err := e.SetBytesCanonical(b); err != nil {
|
||||
return e, ErrInvalidInput
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
type poseidonPrecompile struct{}
|
||||
|
||||
func (p *poseidonPrecompile) RequiredGas(input []byte) uint64 {
|
||||
@@ -68,8 +85,16 @@ func (p *poseidonPrecompile) RequiredGas(input []byte) uint64 {
|
||||
if len(input) < 5 {
|
||||
return 0
|
||||
}
|
||||
n := (len(input) - 5) / 32
|
||||
return GasSpongeBase + uint64(n)*GasSpongePerIn
|
||||
outLen := binary.BigEndian.Uint32(input[1:5])
|
||||
if outLen == 0 || outLen > MaxSpongeOutput {
|
||||
return 0
|
||||
}
|
||||
// Squeezing permutes once per output word, exactly as absorbing does
|
||||
// per input word. Pricing only the input left those permutations free:
|
||||
// a 5-byte call bought 31 of them for the flat base fee.
|
||||
words := uint64(outLen+31) / 32
|
||||
n := uint64(len(input)-5) / 32
|
||||
return GasSpongeBase + (n+words)*GasSpongePerIn
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
@@ -115,8 +140,10 @@ func (p *poseidonPrecompile) hash(data []byte, gas uint64) ([]byte, uint64, erro
|
||||
|
||||
hasher := poseidon2.NewMerkleDamgardHasher()
|
||||
for i := range n {
|
||||
var elem fr.Element
|
||||
elem.SetBytes(data[i*32 : (i+1)*32])
|
||||
elem, err := readElement(data[i*32 : (i+1)*32])
|
||||
if err != nil {
|
||||
return nil, gas, err
|
||||
}
|
||||
b := elem.Bytes()
|
||||
hasher.Write(b[:])
|
||||
}
|
||||
@@ -130,9 +157,14 @@ func (p *poseidonPrecompile) hashPair(data []byte, gas uint64) ([]byte, uint64,
|
||||
if len(data) < 64 {
|
||||
return nil, gas, ErrInvalidInput
|
||||
}
|
||||
var left, right fr.Element
|
||||
left.SetBytes(data[:32])
|
||||
right.SetBytes(data[32:64])
|
||||
left, err := readElement(data[:32])
|
||||
if err != nil {
|
||||
return nil, gas, err
|
||||
}
|
||||
right, err := readElement(data[32:64])
|
||||
if err != nil {
|
||||
return nil, gas, err
|
||||
}
|
||||
|
||||
hasher := poseidon2.NewMerkleDamgardHasher()
|
||||
lb := left.Bytes()
|
||||
@@ -150,7 +182,7 @@ func (p *poseidonPrecompile) sponge(data []byte, gas uint64) ([]byte, uint64, er
|
||||
return nil, gas, ErrInvalidInput
|
||||
}
|
||||
outLen := binary.BigEndian.Uint32(data[:4])
|
||||
if outLen > 1024 || outLen == 0 {
|
||||
if outLen > MaxSpongeOutput || outLen == 0 {
|
||||
return nil, gas, ErrInvalidInput
|
||||
}
|
||||
payload := data[4:]
|
||||
@@ -181,8 +213,10 @@ func (p *poseidonPrecompile) sponge(data []byte, gas uint64) ([]byte, uint64, er
|
||||
|
||||
// Absorb phase: XOR each input element into state[0], then permute.
|
||||
for i := range n {
|
||||
var elem fr.Element
|
||||
elem.SetBytes(payload[i*32 : (i+1)*32])
|
||||
elem, err := readElement(payload[i*32 : (i+1)*32])
|
||||
if err != nil {
|
||||
return nil, gas, err
|
||||
}
|
||||
state[0].Add(&state[0], &elem)
|
||||
perm.Permutation(state)
|
||||
}
|
||||
|
||||
@@ -255,11 +255,32 @@ func TestGasCost(t *testing.T) {
|
||||
return b
|
||||
}(), GasHashBase + 4*GasPerElement},
|
||||
{"hashpair", []byte{OpHashPair}, GasHashPair},
|
||||
{"sponge_1elem", func() []byte {
|
||||
// One absorbed element, one squeezed word: one permutation each.
|
||||
{"sponge_1elem_1word", func() []byte {
|
||||
b := make([]byte, 1+4+32)
|
||||
b[0] = OpSponge
|
||||
binary.BigEndian.PutUint32(b[1:5], 32)
|
||||
return b
|
||||
}(), GasSpongeBase + 2*GasSpongePerIn},
|
||||
// Squeezing 1024 bytes is 32 words whether or not anything was absorbed.
|
||||
{"sponge_0elem_32words", func() []byte {
|
||||
b := make([]byte, 1+4)
|
||||
b[0] = OpSponge
|
||||
binary.BigEndian.PutUint32(b[1:5], MaxSpongeOutput)
|
||||
return b
|
||||
}(), GasSpongeBase + 32*GasSpongePerIn},
|
||||
// Lengths Run refuses cost nothing.
|
||||
{"sponge_outlen_zero", func() []byte {
|
||||
b := make([]byte, 1+4+32)
|
||||
b[0] = OpSponge
|
||||
return b
|
||||
}(), GasSpongeBase + 1*GasSpongePerIn},
|
||||
}(), 0},
|
||||
{"sponge_outlen_over_max", func() []byte {
|
||||
b := make([]byte, 1+4+32)
|
||||
b[0] = OpSponge
|
||||
binary.BigEndian.PutUint32(b[1:5], MaxSpongeOutput+1)
|
||||
return b
|
||||
}(), 0},
|
||||
{"invalid_op", []byte{0xFF}, 0},
|
||||
{"empty", []byte{}, 0},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,491 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package poseidon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/consensys/gnark-crypto/ecc/bn254/fr"
|
||||
"github.com/consensys/gnark-crypto/ecc/bn254/fr/poseidon2"
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/precompile/contract"
|
||||
"github.com/luxfi/precompile/modules"
|
||||
"github.com/luxfi/precompile/precompileconfig"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// fieldModulus is r, the BN254 scalar field order and the domain of every
|
||||
// element this precompile accepts.
|
||||
func fieldModulus() *big.Int { return fr.Modulus() }
|
||||
|
||||
func elem(v *big.Int) []byte {
|
||||
b := make([]byte, 32)
|
||||
v.FillBytes(b)
|
||||
return b
|
||||
}
|
||||
|
||||
func small(n int64) []byte { return elem(big.NewInt(n)) }
|
||||
|
||||
func run(tb testing.TB, op byte, data ...[]byte) ([]byte, uint64, error) {
|
||||
tb.Helper()
|
||||
in := []byte{op}
|
||||
for _, d := range data {
|
||||
in = append(in, d...)
|
||||
}
|
||||
return PoseidonPrecompile.Run(nil, common.Address{}, ContractAddress, in, 1<<24, true)
|
||||
}
|
||||
|
||||
func spongeInput(outLen uint32, elems ...[]byte) []byte {
|
||||
hdr := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(hdr, outLen)
|
||||
for _, e := range elems {
|
||||
hdr = append(hdr, e...)
|
||||
}
|
||||
return hdr
|
||||
}
|
||||
|
||||
// TestNonCanonicalElementRejected is the regression for the defect this file
|
||||
// was written around.
|
||||
//
|
||||
// Inputs used to go through fr.Element.SetBytes, which reduces modulo r. Two
|
||||
// distinct 32-byte words -- x and x+r -- therefore hashed to the same digest,
|
||||
// so a second preimage for any leaf was a single addition away. That is fatal
|
||||
// for the Merkle trees and nullifier sets this hash exists to serve, where the
|
||||
// leaf is a bytes32 the attacker chooses.
|
||||
func TestNonCanonicalElementRejected(t *testing.T) {
|
||||
r := fieldModulus()
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
v *big.Int
|
||||
}{
|
||||
{"r", r},
|
||||
{"r+1", new(big.Int).Add(r, big.NewInt(1))},
|
||||
{"r+7", new(big.Int).Add(r, big.NewInt(7))},
|
||||
{"2^256-1", new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1))},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
bad := elem(tc.v)
|
||||
require.Len(t, bad, 32)
|
||||
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
op byte
|
||||
data [][]byte
|
||||
}{
|
||||
{"hash single", OpHash, [][]byte{bad}},
|
||||
{"hash first of two", OpHash, [][]byte{bad, small(1)}},
|
||||
{"hash second of two", OpHash, [][]byte{small(1), bad}},
|
||||
{"hashPair left", OpHashPair, [][]byte{bad, small(1)}},
|
||||
{"hashPair right", OpHashPair, [][]byte{small(1), bad}},
|
||||
{"sponge", OpSponge, [][]byte{spongeInput(32, bad)}},
|
||||
{"sponge second", OpSponge, [][]byte{spongeInput(32, small(1), bad)}},
|
||||
} {
|
||||
out, _, err := run(t, c.op, c.data...)
|
||||
require.ErrorIs(t, err, ErrInvalidInput,
|
||||
"%s must refuse a non-canonical element, not reduce it", c.name)
|
||||
require.Nil(t, out)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// The reduced value of each rejected word is still a perfectly good input,
|
||||
// so the refusals above come from canonicality and not from a broken shape.
|
||||
for _, v := range []*big.Int{big.NewInt(0), big.NewInt(1), new(big.Int).Sub(r, big.NewInt(1))} {
|
||||
out, _, err := run(t, OpHash, elem(v))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out, 32)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDistinctInputsGiveDistinctDigests is the collision check over the whole
|
||||
// accepted domain, including its two endpoints. Every one of these words used
|
||||
// to have r-1 aliases; now each names itself.
|
||||
func TestDistinctInputsGiveDistinctDigests(t *testing.T) {
|
||||
r := fieldModulus()
|
||||
inputs := [][]byte{
|
||||
elem(big.NewInt(0)),
|
||||
elem(big.NewInt(1)),
|
||||
elem(big.NewInt(2)),
|
||||
elem(new(big.Int).Rsh(r, 1)),
|
||||
elem(new(big.Int).Sub(r, big.NewInt(2))),
|
||||
elem(new(big.Int).Sub(r, big.NewInt(1))),
|
||||
}
|
||||
|
||||
seen := map[string][]byte{}
|
||||
for _, in := range inputs {
|
||||
out, _, err := run(t, OpHash, in)
|
||||
require.NoError(t, err)
|
||||
prev, dup := seen[string(out)]
|
||||
require.False(t, dup, "collision: %x and %x share a digest", prev, in)
|
||||
seen[string(out)] = in
|
||||
}
|
||||
require.Len(t, seen, len(inputs))
|
||||
}
|
||||
|
||||
// TestHashIsOrderSensitive: a Merkle hash that ignored argument order would let
|
||||
// anyone swap siblings and keep the root.
|
||||
func TestHashIsOrderSensitive(t *testing.T) {
|
||||
a, b := small(1), small(2)
|
||||
|
||||
ab, _, err := run(t, OpHash, a, b)
|
||||
require.NoError(t, err)
|
||||
ba, _, err := run(t, OpHash, b, a)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, ab, ba, "Hash(a,b) must differ from Hash(b,a)")
|
||||
|
||||
pab, _, err := run(t, OpHashPair, a, b)
|
||||
require.NoError(t, err)
|
||||
pba, _, err := run(t, OpHashPair, b, a)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, pab, pba)
|
||||
|
||||
// HashPair is the two-element case of Hash: one function, two entry points.
|
||||
require.Equal(t, ab, pab, "HashPair(a,b) must equal Hash(a,b)")
|
||||
require.Equal(t, ba, pba)
|
||||
}
|
||||
|
||||
// TestHashLengthIsBound: appending an element must change the digest, so a
|
||||
// shorter input cannot be extended into a longer one with the same answer.
|
||||
func TestHashIsLengthBound(t *testing.T) {
|
||||
seen := map[string]int{}
|
||||
var elems [][]byte
|
||||
for n := 1; n <= 16; n++ {
|
||||
elems = append(elems, small(int64(n)))
|
||||
out, _, err := run(t, OpHash, elems...)
|
||||
require.NoError(t, err)
|
||||
prev, dup := seen[string(out)]
|
||||
require.False(t, dup, "width %d collides with width %d", n, prev)
|
||||
seen[string(out)] = n
|
||||
}
|
||||
require.Len(t, seen, 16)
|
||||
}
|
||||
|
||||
// TestAvalanche: flipping the low bit of one element must change essentially
|
||||
// the whole digest. A hash that leaked its input structure would fail this.
|
||||
func TestAvalanche(t *testing.T) {
|
||||
base, _, err := run(t, OpHash, small(0))
|
||||
require.NoError(t, err)
|
||||
next, _, err := run(t, OpHash, small(1))
|
||||
require.NoError(t, err)
|
||||
|
||||
differing := 0
|
||||
for i := range base {
|
||||
if base[i] != next[i] {
|
||||
differing++
|
||||
}
|
||||
}
|
||||
require.Greater(t, differing, 24, "a one-bit input change must disturb the whole digest")
|
||||
}
|
||||
|
||||
// TestSpongeMatchesAnIndependentlyDrivenPermutation reconstructs the sponge in
|
||||
// the test from the raw permutation and requires the same bytes. It cannot
|
||||
// catch a wrong permutation -- both sides use the same one -- but it does catch
|
||||
// every wiring error the sponge can have: absorb order, a missing or extra
|
||||
// permutation, squeezing the wrong lane, or zero-padding instead of squeezing.
|
||||
func TestSpongeMatchesAnIndependentlyDrivenPermutation(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
outLen uint32
|
||||
elems []int64
|
||||
}{
|
||||
{32, nil},
|
||||
{32, []int64{7}},
|
||||
{64, []int64{7}},
|
||||
{96, []int64{1, 2, 3}},
|
||||
{MaxSpongeOutput, nil},
|
||||
{MaxSpongeOutput, []int64{9, 9, 9, 9}},
|
||||
{1, []int64{5}},
|
||||
{31, []int64{5}},
|
||||
{33, []int64{5}},
|
||||
} {
|
||||
var in [][]byte
|
||||
for _, v := range tc.elems {
|
||||
in = append(in, small(v))
|
||||
}
|
||||
got, _, err := run(t, OpSponge, spongeInput(tc.outLen, in...))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, int(tc.outLen))
|
||||
|
||||
// Rebuild it here.
|
||||
perm := poseidon2.NewPermutation(3, 6, 50)
|
||||
state := make([]fr.Element, 3)
|
||||
for _, v := range tc.elems {
|
||||
var e fr.Element
|
||||
e.SetInt64(v)
|
||||
state[0].Add(&state[0], &e)
|
||||
perm.Permutation(state)
|
||||
}
|
||||
want := make([]byte, 0, tc.outLen)
|
||||
for uint32(len(want)) < tc.outLen {
|
||||
b := state[0].Bytes()
|
||||
want = append(want, b[:]...)
|
||||
if uint32(len(want)) < tc.outLen {
|
||||
perm.Permutation(state)
|
||||
}
|
||||
}
|
||||
require.Equal(t, want[:tc.outLen], got,
|
||||
"outLen=%d elems=%v", tc.outLen, tc.elems)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSpongeSqueezesRatherThanPads: every 32-byte word of a long output must be
|
||||
// distinct, which is only true if the permutation runs between squeezes.
|
||||
func TestSpongeSqueezesRatherThanPads(t *testing.T) {
|
||||
out, _, err := run(t, OpSponge, spongeInput(MaxSpongeOutput, small(42)))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out, MaxSpongeOutput)
|
||||
|
||||
seen := map[string]int{}
|
||||
for i := 0; i+32 <= len(out); i += 32 {
|
||||
w := string(out[i : i+32])
|
||||
require.False(t, bytes.Equal(out[i:i+32], make([]byte, 32)), "word %d is zero padding", i/32)
|
||||
prev, dup := seen[w]
|
||||
require.False(t, dup, "word %d repeats word %d: the permutation did not run", i/32, prev)
|
||||
seen[w] = i / 32
|
||||
}
|
||||
require.Len(t, seen, MaxSpongeOutput/32)
|
||||
|
||||
// A shorter request is a prefix of a longer one: the same sponge, truncated.
|
||||
short, _, err := run(t, OpSponge, spongeInput(64, small(42)))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, out[:64], short)
|
||||
}
|
||||
|
||||
// TestSpongeGasScalesWithOutput is the regression for the pricing defect: the
|
||||
// squeeze permutes once per output word, but the schedule only counted input
|
||||
// words, so a five-byte call bought 31 permutations for the flat base fee.
|
||||
func TestSpongeGasScalesWithOutput(t *testing.T) {
|
||||
base := PoseidonPrecompile.RequiredGas(append([]byte{OpSponge}, spongeInput(32)...))
|
||||
require.Equal(t, uint64(GasSpongeBase+GasSpongePerIn), base,
|
||||
"one squeezed word costs one per-word unit")
|
||||
|
||||
// Requesting the maximum with no input must cost far more than the base fee.
|
||||
maxOut := PoseidonPrecompile.RequiredGas(append([]byte{OpSponge}, spongeInput(MaxSpongeOutput)...))
|
||||
require.Equal(t, uint64(GasSpongeBase+32*GasSpongePerIn), maxOut)
|
||||
require.Greater(t, maxOut, base, "a longer squeeze must cost more")
|
||||
|
||||
// Price rises by exactly one unit per output word.
|
||||
prev := uint64(0)
|
||||
for words := uint32(1); words <= 32; words++ {
|
||||
got := PoseidonPrecompile.RequiredGas(append([]byte{OpSponge}, spongeInput(words*32)...))
|
||||
require.Equal(t, uint64(GasSpongeBase)+uint64(words)*GasSpongePerIn, got, "words=%d", words)
|
||||
if words > 1 {
|
||||
require.Equal(t, uint64(GasSpongePerIn), got-prev)
|
||||
}
|
||||
prev = got
|
||||
}
|
||||
|
||||
// A partial word still costs a whole word: the squeeze permutes either way.
|
||||
require.Equal(t,
|
||||
PoseidonPrecompile.RequiredGas(append([]byte{OpSponge}, spongeInput(32)...)),
|
||||
PoseidonPrecompile.RequiredGas(append([]byte{OpSponge}, spongeInput(1)...)))
|
||||
require.Equal(t,
|
||||
PoseidonPrecompile.RequiredGas(append([]byte{OpSponge}, spongeInput(64)...)),
|
||||
PoseidonPrecompile.RequiredGas(append([]byte{OpSponge}, spongeInput(33)...)))
|
||||
|
||||
// Input and output are priced the same, because they do the same work.
|
||||
in1 := PoseidonPrecompile.RequiredGas(append([]byte{OpSponge}, spongeInput(32, small(1))...))
|
||||
require.Equal(t, uint64(GasSpongeBase+2*GasSpongePerIn), in1)
|
||||
|
||||
// And the price is actually charged: the old flat fee no longer buys a
|
||||
// maximum-length squeeze.
|
||||
_, _, err := PoseidonPrecompile.Run(nil, common.Address{}, ContractAddress,
|
||||
append([]byte{OpSponge}, spongeInput(MaxSpongeOutput)...), uint64(GasSpongeBase+GasSpongePerIn), true)
|
||||
require.ErrorIs(t, err, contract.ErrOutOfGas,
|
||||
"a full squeeze must not be affordable at the one-word price")
|
||||
}
|
||||
|
||||
// TestRefusal_Shape walks every length and selector refusal.
|
||||
func TestRefusal_Shape(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
in []byte
|
||||
err error
|
||||
}{
|
||||
{"nil", nil, ErrInvalidInput},
|
||||
{"empty", []byte{}, ErrInvalidInput},
|
||||
{"hash op only", []byte{OpHash}, ErrInvalidInput},
|
||||
{"hash one byte", []byte{OpHash, 0x01}, ErrInvalidInput},
|
||||
{"hash 31 bytes", append([]byte{OpHash}, make([]byte, 31)...), ErrInvalidInput},
|
||||
{"hash 33 bytes", append([]byte{OpHash}, make([]byte, 33)...), ErrInvalidInput},
|
||||
{"hash 17 elements", append([]byte{OpHash}, make([]byte, 17*32)...), ErrTooManyInputs},
|
||||
{"hash 32 elements", append([]byte{OpHash}, make([]byte, 32*32)...), ErrTooManyInputs},
|
||||
{"hashPair op only", []byte{OpHashPair}, ErrInvalidInput},
|
||||
{"hashPair one element", append([]byte{OpHashPair}, make([]byte, 32)...), ErrInvalidInput},
|
||||
{"hashPair 63 bytes", append([]byte{OpHashPair}, make([]byte, 63)...), ErrInvalidInput},
|
||||
{"sponge op only", []byte{OpSponge}, ErrInvalidInput},
|
||||
{"sponge 3-byte header", []byte{OpSponge, 0, 0, 0}, ErrInvalidInput},
|
||||
{"sponge outLen 0", append([]byte{OpSponge}, spongeInput(0)...), ErrInvalidInput},
|
||||
{"sponge outLen over max", append([]byte{OpSponge}, spongeInput(MaxSpongeOutput+1)...), ErrInvalidInput},
|
||||
{"sponge misaligned payload", append([]byte{OpSponge}, append(spongeInput(32), 0x01)...), ErrInvalidInput},
|
||||
{"sponge 17 elements", append([]byte{OpSponge}, spongeInput(32, make([]byte, 17*32))...), ErrTooManyInputs},
|
||||
{"unknown op 0x00", []byte{0x00}, ErrInvalidOp},
|
||||
{"unknown op 0x04", []byte{0x04}, ErrInvalidOp},
|
||||
{"unknown op 0xff", []byte{0xFF}, ErrInvalidOp},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
out, _, err := PoseidonPrecompile.Run(nil, common.Address{}, ContractAddress, tc.in, 1<<24, true)
|
||||
require.ErrorIs(t, err, tc.err)
|
||||
require.Nil(t, out)
|
||||
})
|
||||
}
|
||||
|
||||
// The boundaries either side of each refusal must work.
|
||||
out, _, err := run(t, OpHash, make([]byte, 16*32))
|
||||
require.NoError(t, err, "16 elements is the maximum width")
|
||||
require.Len(t, out, 32)
|
||||
|
||||
out, _, err = run(t, OpSponge, spongeInput(MaxSpongeOutput))
|
||||
require.NoError(t, err, "the maximum squeeze is allowed")
|
||||
require.Len(t, out, MaxSpongeOutput)
|
||||
|
||||
out, _, err = run(t, OpSponge, spongeInput(1))
|
||||
require.NoError(t, err, "a one-byte squeeze is allowed")
|
||||
require.Len(t, out, 1)
|
||||
|
||||
// HashPair ignores trailing data past its two elements.
|
||||
a, b := small(1), small(2)
|
||||
pair, _, err := run(t, OpHashPair, a, b)
|
||||
require.NoError(t, err)
|
||||
padded, _, err := run(t, OpHashPair, a, b, []byte{0xAA})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, pair, padded)
|
||||
}
|
||||
|
||||
// TestGas_Schedule covers every arm, and proves nothing is bought for free.
|
||||
func TestGas_Schedule(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
in []byte
|
||||
want uint64
|
||||
}{
|
||||
{"nil", nil, 0},
|
||||
{"empty", []byte{}, 0},
|
||||
{"hash op only", []byte{OpHash}, 0},
|
||||
{"hash sub-element", append([]byte{OpHash}, make([]byte, 31)...), 0},
|
||||
{"hash 1", append([]byte{OpHash}, make([]byte, 32)...), GasHashBase + GasPerElement},
|
||||
{"hash 4", append([]byte{OpHash}, make([]byte, 4*32)...), GasHashBase + 4*GasPerElement},
|
||||
{"hash 16", append([]byte{OpHash}, make([]byte, 16*32)...), GasHashBase + 16*GasPerElement},
|
||||
{"hashPair", []byte{OpHashPair}, GasHashPair},
|
||||
{"hashPair with data", append([]byte{OpHashPair}, make([]byte, 64)...), GasHashPair},
|
||||
{"sponge short header", []byte{OpSponge, 0, 0, 0}, 0},
|
||||
{"sponge outLen 0", append([]byte{OpSponge}, spongeInput(0)...), 0},
|
||||
{"sponge over max", append([]byte{OpSponge}, spongeInput(MaxSpongeOutput+1)...), 0},
|
||||
{"unknown 0x00", []byte{0x00}, 0},
|
||||
{"unknown 0x04", []byte{0x04}, 0},
|
||||
{"unknown 0xff", []byte{0xFF}, 0},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Equal(t, tc.want, PoseidonPrecompile.RequiredGas(tc.in))
|
||||
})
|
||||
}
|
||||
|
||||
// Hash is priced per element and rises monotonically.
|
||||
prev := uint64(0)
|
||||
for n := 1; n <= 16; n++ {
|
||||
got := PoseidonPrecompile.RequiredGas(append([]byte{OpHash}, make([]byte, n*32)...))
|
||||
require.Equal(t, uint64(GasHashBase+n*GasPerElement), got, "n=%d", n)
|
||||
if n > 1 {
|
||||
require.Equal(t, uint64(GasPerElement), got-prev)
|
||||
}
|
||||
prev = got
|
||||
}
|
||||
|
||||
// Every zero-priced input must refuse.
|
||||
for _, in := range [][]byte{
|
||||
nil, {}, {OpHash}, append([]byte{OpHash}, make([]byte, 31)...),
|
||||
{OpSponge, 0, 0, 0}, append([]byte{OpSponge}, spongeInput(0)...),
|
||||
append([]byte{OpSponge}, spongeInput(MaxSpongeOutput+1)...),
|
||||
{0x00}, {0x04}, {0xFF},
|
||||
} {
|
||||
require.Zero(t, PoseidonPrecompile.RequiredGas(in))
|
||||
out, _, err := PoseidonPrecompile.Run(nil, common.Address{}, ContractAddress, in, 0, true)
|
||||
require.Error(t, err, "a zero-gas input must not execute")
|
||||
require.Nil(t, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGas_Deduction(t *testing.T) {
|
||||
in := append([]byte{OpHash}, small(1)...)
|
||||
price := uint64(GasHashBase + GasPerElement)
|
||||
|
||||
_, left, err := PoseidonPrecompile.Run(nil, common.Address{}, ContractAddress, in, price+17, true)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(17), left)
|
||||
|
||||
_, left, err = PoseidonPrecompile.Run(nil, common.Address{}, ContractAddress, in, price, true)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, left)
|
||||
|
||||
_, left, err = PoseidonPrecompile.Run(nil, common.Address{}, ContractAddress, in, price-1, true)
|
||||
require.ErrorIs(t, err, contract.ErrOutOfGas)
|
||||
require.Zero(t, left)
|
||||
}
|
||||
|
||||
// TestDeterminism: no clock, no randomness, no map iteration. The same call
|
||||
// must give the same bytes, in any order, from any goroutine.
|
||||
func TestDeterminism(t *testing.T) {
|
||||
cases := [][]byte{
|
||||
append([]byte{OpHash}, small(1)...),
|
||||
append([]byte{OpHash}, append(small(1), small(2)...)...),
|
||||
append([]byte{OpHashPair}, append(small(3), small(4)...)...),
|
||||
append([]byte{OpSponge}, spongeInput(128, small(5))...),
|
||||
}
|
||||
first := make([][]byte, len(cases))
|
||||
for i, in := range cases {
|
||||
out, _, err := PoseidonPrecompile.Run(nil, common.Address{}, ContractAddress, in, 1<<24, true)
|
||||
require.NoError(t, err)
|
||||
first[i] = out
|
||||
}
|
||||
for range 8 {
|
||||
for i := len(cases) - 1; i >= 0; i-- {
|
||||
out, _, err := PoseidonPrecompile.Run(nil, common.Address{}, ContractAddress, cases[i], 1<<24, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, first[i], out, "case %d is not deterministic", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- module + config ----------------------------------------------------
|
||||
|
||||
func TestModule_Wiring(t *testing.T) {
|
||||
require.Equal(t, ConfigKey, Module.ConfigKey)
|
||||
require.Equal(t, ContractAddress, Module.Address)
|
||||
require.Same(t, PoseidonPrecompile, Module.Contract)
|
||||
require.NotNil(t, Module.Configurator)
|
||||
require.Equal(t,
|
||||
common.HexToAddress("0x0500000000000000000000000000000000000005"),
|
||||
ContractAddress)
|
||||
}
|
||||
|
||||
func TestConfigurator_Surface(t *testing.T) {
|
||||
c := &configurator{}
|
||||
require.IsType(t, &Config{}, c.MakeConfig())
|
||||
require.NoError(t, c.Configure(nil, nil, nil, nil))
|
||||
}
|
||||
|
||||
func TestConfig_Surface(t *testing.T) {
|
||||
c := &Config{}
|
||||
require.Equal(t, ConfigKey, c.Key())
|
||||
require.Nil(t, c.Timestamp())
|
||||
require.False(t, c.IsDisabled())
|
||||
require.NoError(t, c.Verify(nil))
|
||||
|
||||
ts := uint64(41)
|
||||
a := &Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts}}
|
||||
require.Equal(t, &ts, a.Timestamp())
|
||||
require.True(t, a.Equal(&Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts}}))
|
||||
|
||||
other := uint64(42)
|
||||
require.False(t, a.Equal(&Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &other}}))
|
||||
require.False(t, a.Equal(nil))
|
||||
require.True(t, (&Config{Upgrade: precompileconfig.Upgrade{Disable: true}}).IsDisabled())
|
||||
}
|
||||
|
||||
func TestRegisterModuleIsIdempotentlyRefused(t *testing.T) {
|
||||
require.Error(t, modules.RegisterModule(Module))
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
||||
|
||||
package secp256r1
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/precompile/contract"
|
||||
"github.com/luxfi/precompile/modules"
|
||||
"github.com/luxfi/precompile/precompileconfig"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func katInt(tb testing.TB, s string) *big.Int {
|
||||
tb.Helper()
|
||||
v, ok := new(big.Int).SetString(s, 16)
|
||||
require.True(tb, ok, "bad hex literal %q", s)
|
||||
return v
|
||||
}
|
||||
|
||||
func word(tb testing.TB, v *big.Int) []byte {
|
||||
tb.Helper()
|
||||
require.LessOrEqual(tb, v.BitLen(), 256, "value does not fit in a 32-byte word")
|
||||
b := make([]byte, 32)
|
||||
v.FillBytes(b)
|
||||
return b
|
||||
}
|
||||
|
||||
// calldata lays out the RIP-7212 input: hash(32) || r(32) || s(32) || x(32) || y(32).
|
||||
func calldata(tb testing.TB, hash []byte, r, s, x, y *big.Int) []byte {
|
||||
tb.Helper()
|
||||
out := make([]byte, 0, InputLength)
|
||||
out = append(out, hash...)
|
||||
out = append(out, word(tb, r)...)
|
||||
out = append(out, word(tb, s)...)
|
||||
out = append(out, word(tb, x)...)
|
||||
return append(out, word(tb, y)...)
|
||||
}
|
||||
|
||||
// rfc6979 holds the P-256 / SHA-256 deterministic-ECDSA vectors from RFC 6979
|
||||
// appendix A.2.5. They were re-checked against an independent implementation of
|
||||
// the verification equation, and the public key was re-derived from the private
|
||||
// scalar, before being written here.
|
||||
var rfc6979 = struct {
|
||||
d, x, y string
|
||||
cases []struct{ msg, r, s string }
|
||||
}{
|
||||
d: "C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721",
|
||||
x: "60FED4BA255A9D31C961EB74C6356D68C049B8923B61FA6CE669622E60F29FB6",
|
||||
y: "7903FE1008B8BC99A41AE9E95628BC64F2F1B20C2D7E9F5177A3C294D4462299",
|
||||
cases: []struct{ msg, r, s string }{
|
||||
{
|
||||
msg: "sample",
|
||||
r: "EFD48B2AACB6A8FD1140DD9CD45E81D69D2C877B56AAF991C34D0EA84EAF3716",
|
||||
s: "F7CB1C942D657C41D436C7A1B6E29F65F3E900DBB9AFF4064DC4AB2F843ACDA8",
|
||||
},
|
||||
{
|
||||
msg: "test",
|
||||
r: "F1ABB023518351CD71D881567B1EA663ED3EFCF6C5132B354F28D3B0B7D38367",
|
||||
s: "019F4113742A2B14BD25926B49C649155F267E60D3814B4C0CC84250E46F0083",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// TestKAT_RFC6979 pins the verifier against published vectors, and binds the
|
||||
// public key to the published private scalar so key handling is covered too.
|
||||
func TestKAT_RFC6979(t *testing.T) {
|
||||
curve := elliptic.P256()
|
||||
x, y := katInt(t, rfc6979.x), katInt(t, rfc6979.y)
|
||||
|
||||
dx, dy := curve.ScalarBaseMult(word(t, katInt(t, rfc6979.d)))
|
||||
require.Zero(t, dx.Cmp(x), "published private scalar must produce the published X")
|
||||
require.Zero(t, dy.Cmp(y), "published private scalar must produce the published Y")
|
||||
|
||||
c := &Contract{}
|
||||
for _, tc := range rfc6979.cases {
|
||||
t.Run(tc.msg, func(t *testing.T) {
|
||||
h := sha256.Sum256([]byte(tc.msg))
|
||||
r, s := katInt(t, tc.r), katInt(t, tc.s)
|
||||
|
||||
out, err := c.Run(calldata(t, h[:], r, s, x, y))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, successResult, out, "published vector must verify")
|
||||
require.True(t, Verify(h[:], r, s, x, y))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestKAT_RFC6979_MutationsRejected flips one bit in each field of each
|
||||
// published vector. Without this a verifier that answers true unconditionally
|
||||
// still passes TestKAT_RFC6979.
|
||||
func TestKAT_RFC6979_MutationsRejected(t *testing.T) {
|
||||
x, y := katInt(t, rfc6979.x), katInt(t, rfc6979.y)
|
||||
c := &Contract{}
|
||||
|
||||
for _, tc := range rfc6979.cases {
|
||||
h := sha256.Sum256([]byte(tc.msg))
|
||||
full := calldata(t, h[:], katInt(t, tc.r), katInt(t, tc.s), x, y)
|
||||
|
||||
for at := range full {
|
||||
bad := append([]byte{}, full...)
|
||||
bad[at] ^= 0x01
|
||||
out, err := c.Run(bad)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, out, "%s: mutating byte %d must not verify", tc.msg, at)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// verifyIndependently is a textbook ECDSA check over math/big, deliberately not
|
||||
// sharing a line of code with crypto/ecdsa's nistec path. It exists so the
|
||||
// property tests below compare two implementations rather than one against
|
||||
// itself.
|
||||
func verifyIndependently(hash []byte, r, s, px, py *big.Int) bool {
|
||||
c := elliptic.P256().Params()
|
||||
if r.Sign() <= 0 || r.Cmp(c.N) >= 0 || s.Sign() <= 0 || s.Cmp(c.N) >= 0 {
|
||||
return false
|
||||
}
|
||||
if !c.IsOnCurve(px, py) {
|
||||
return false
|
||||
}
|
||||
// e is the leftmost min(bitlen(N), 8*len(hash)) bits of the hash.
|
||||
e := new(big.Int).SetBytes(hash)
|
||||
if excess := 8*len(hash) - c.N.BitLen(); excess > 0 {
|
||||
e.Rsh(e, uint(excess))
|
||||
}
|
||||
w := new(big.Int).ModInverse(s, c.N)
|
||||
if w == nil {
|
||||
return false
|
||||
}
|
||||
u1 := new(big.Int).Mod(new(big.Int).Mul(e, w), c.N)
|
||||
u2 := new(big.Int).Mod(new(big.Int).Mul(r, w), c.N)
|
||||
|
||||
ax, ay := c.ScalarBaseMult(u1.Bytes())
|
||||
bx, by := c.ScalarMult(px, py, u2.Bytes())
|
||||
qx, qy := c.Add(ax, ay, bx, by)
|
||||
if qx.Sign() == 0 && qy.Sign() == 0 {
|
||||
return false
|
||||
}
|
||||
return new(big.Int).Mod(qx, c.N).Cmp(r) == 0
|
||||
}
|
||||
|
||||
// TestAgreesWithIndependentVerifier runs fresh keys and signatures, plus
|
||||
// deliberately broken ones, through both implementations and requires the same
|
||||
// verdict every time. Fixed vectors cannot catch an error that only shows on
|
||||
// inputs nobody published.
|
||||
func TestAgreesWithIndependentVerifier(t *testing.T) {
|
||||
curve := elliptic.P256()
|
||||
n := curve.Params().N
|
||||
c := &Contract{}
|
||||
|
||||
for i := range 24 {
|
||||
priv, err := ecdsa.GenerateKey(curve, rand.Reader)
|
||||
require.NoError(t, err)
|
||||
h := sha256.Sum256([]byte{byte(i)})
|
||||
r, s, err := ecdsa.Sign(rand.Reader, priv, h[:])
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
r, s *big.Int
|
||||
x, y *big.Int
|
||||
hash []byte
|
||||
}{
|
||||
{"genuine", r, s, priv.X, priv.Y, h[:]},
|
||||
{"malleated s", r, new(big.Int).Sub(n, s), priv.X, priv.Y, h[:]},
|
||||
{"swapped r,s", s, r, priv.X, priv.Y, h[:]},
|
||||
{"other hash", r, s, priv.X, priv.Y, sha256.New().Sum(nil)},
|
||||
{"y negated", r, s, priv.X, new(big.Int).Sub(curve.Params().P, priv.Y), h[:]},
|
||||
{"r incremented", new(big.Int).Add(r, big.NewInt(1)), s, priv.X, priv.Y, h[:]},
|
||||
} {
|
||||
want := verifyIndependently(tc.hash, tc.r, tc.s, tc.x, tc.y)
|
||||
out, err := c.Run(calldata(t, tc.hash, tc.r, tc.s, tc.x, tc.y))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, want, out != nil,
|
||||
"iteration %d %q: precompile and independent verifier disagree", i, tc.name)
|
||||
require.Equal(t, want, Verify(tc.hash, tc.r, tc.s, tc.x, tc.y))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestMalleability_HighSIsAcceptedBySpec records a real property of this
|
||||
// precompile: RIP-7212 requires only 1 <= s <= n-1, so both s and n-s verify.
|
||||
// Every P-256 signature therefore has two accepted encodings. That is correct
|
||||
// here -- unlike secp256k1's ecrecover, no low-s rule exists for P-256, and
|
||||
// enforcing one would reject signatures WebAuthn authenticators legitimately
|
||||
// produce. Callers that need a unique signature identity must canonicalise
|
||||
// themselves. This test fails the day someone adds a low-s check, which would
|
||||
// be a consensus change.
|
||||
func TestMalleability_HighSIsAcceptedBySpec(t *testing.T) {
|
||||
curve := elliptic.P256()
|
||||
n := curve.Params().N
|
||||
halfN := new(big.Int).Rsh(n, 1)
|
||||
c := &Contract{}
|
||||
|
||||
priv, err := ecdsa.GenerateKey(curve, rand.Reader)
|
||||
require.NoError(t, err)
|
||||
h := sha256.Sum256([]byte("malleability"))
|
||||
|
||||
var lowS, highS *big.Int
|
||||
var r *big.Int
|
||||
for range 64 {
|
||||
sr, ss, err := ecdsa.Sign(rand.Reader, priv, h[:])
|
||||
require.NoError(t, err)
|
||||
r = sr
|
||||
if ss.Cmp(halfN) > 0 {
|
||||
highS, lowS = ss, new(big.Int).Sub(n, ss)
|
||||
} else {
|
||||
lowS, highS = ss, new(big.Int).Sub(n, ss)
|
||||
}
|
||||
break
|
||||
}
|
||||
require.NotNil(t, r)
|
||||
require.LessOrEqual(t, lowS.Cmp(halfN), 0)
|
||||
require.Positive(t, highS.Cmp(halfN))
|
||||
|
||||
for name, s := range map[string]*big.Int{"low-s": lowS, "high-s": highS} {
|
||||
out, err := c.Run(calldata(t, h[:], r, s, priv.X, priv.Y))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, successResult, out, "%s must verify: RIP-7212 has no low-s rule", name)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRefusals_PointAndScalarRanges is the input-validation surface. Every one
|
||||
// of these is a well-formed 160-byte call that must not verify.
|
||||
func TestRefusals_PointAndScalarRanges(t *testing.T) {
|
||||
curve := elliptic.P256()
|
||||
params := curve.Params()
|
||||
n, p := params.N, params.P
|
||||
|
||||
priv, err := ecdsa.GenerateKey(curve, rand.Reader)
|
||||
require.NoError(t, err)
|
||||
h := sha256.Sum256([]byte("refusals"))
|
||||
r, s, err := ecdsa.Sign(rand.Reader, priv, h[:])
|
||||
require.NoError(t, err)
|
||||
c := &Contract{}
|
||||
|
||||
// Sanity: the unmutated call really does verify, so a rejection below is
|
||||
// attributable to the mutation and not to a broken fixture.
|
||||
out, err := c.Run(calldata(t, h[:], r, s, priv.X, priv.Y))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, successResult, out)
|
||||
|
||||
offCurveY := new(big.Int).Add(priv.Y, big.NewInt(1))
|
||||
require.False(t, curve.IsOnCurve(priv.X, offCurveY), "fixture must really be off-curve")
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
r, s, x, y *big.Int
|
||||
}{
|
||||
// Invalid-curve attack: well-formed coordinates off the curve.
|
||||
{"point off curve", r, s, priv.X, offCurveY},
|
||||
{"point off curve (x+1)", r, s, new(big.Int).Add(priv.X, big.NewInt(1)), priv.Y},
|
||||
// Point at infinity, spelled (0, 0).
|
||||
{"point at infinity", r, s, big.NewInt(0), big.NewInt(0)},
|
||||
{"x zero", r, s, big.NewInt(0), priv.Y},
|
||||
{"y zero", r, s, priv.X, big.NewInt(0)},
|
||||
// Non-canonical field elements: x = p reduces to 0, which is not a
|
||||
// coordinate. It must be refused rather than reduced.
|
||||
{"x = p", r, s, p, priv.Y},
|
||||
{"y = p", r, s, priv.X, p},
|
||||
// Scalars outside [1, n-1].
|
||||
{"r = 0", big.NewInt(0), s, priv.X, priv.Y},
|
||||
{"s = 0", r, big.NewInt(0), priv.X, priv.Y},
|
||||
{"r = n", n, s, priv.X, priv.Y},
|
||||
{"s = n", r, n, priv.X, priv.Y},
|
||||
{"r = n+1", new(big.Int).Add(n, big.NewInt(1)), s, priv.X, priv.Y},
|
||||
{"s = n+1", r, new(big.Int).Add(n, big.NewInt(1)), priv.X, priv.Y},
|
||||
{"r = s = 0", big.NewInt(0), big.NewInt(0), priv.X, priv.Y},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
in := calldata(t, h[:], tc.r, tc.s, tc.x, tc.y)
|
||||
require.Len(t, in, InputLength, "refusal must come from validation, not length")
|
||||
out, err := c.Run(in)
|
||||
require.NoError(t, err, "a refusal is an empty return, not an error")
|
||||
require.Nil(t, out)
|
||||
require.False(t, Verify(h[:], tc.r, tc.s, tc.x, tc.y))
|
||||
require.False(t, verifyIndependently(h[:], tc.r, tc.s, tc.x, tc.y),
|
||||
"independent verifier must agree on the refusal")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRefusals_Length covers empty, one short, one long and the all-zero call.
|
||||
func TestRefusals_Length(t *testing.T) {
|
||||
curve := elliptic.P256()
|
||||
priv, err := ecdsa.GenerateKey(curve, rand.Reader)
|
||||
require.NoError(t, err)
|
||||
h := sha256.Sum256([]byte("length"))
|
||||
r, s, err := ecdsa.Sign(rand.Reader, priv, h[:])
|
||||
require.NoError(t, err)
|
||||
full := calldata(t, h[:], r, s, priv.X, priv.Y)
|
||||
c := &Contract{}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
in []byte
|
||||
}{
|
||||
{"nil", nil},
|
||||
{"empty", []byte{}},
|
||||
{"one byte", []byte{0x01}},
|
||||
{"one short", full[:InputLength-1]},
|
||||
{"one long", append(append([]byte{}, full...), 0)},
|
||||
{"double", append(append([]byte{}, full...), full...)},
|
||||
{"all zero", make([]byte, InputLength)},
|
||||
{"all ones", func() []byte {
|
||||
b := make([]byte, InputLength)
|
||||
for i := range b {
|
||||
b[i] = 0xFF
|
||||
}
|
||||
return b
|
||||
}()},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
out, err := c.Run(tc.in)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, out)
|
||||
})
|
||||
}
|
||||
|
||||
out, err := c.Run(full)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, successResult, out, "the exact length still verifies")
|
||||
}
|
||||
|
||||
// --- stateful wrapper ---------------------------------------------------
|
||||
|
||||
// TestStateful_DeductsGasThenDelegates covers the modules.Module entry point,
|
||||
// which is what the EVM actually calls.
|
||||
func TestStateful_DeductsGasThenDelegates(t *testing.T) {
|
||||
curve := elliptic.P256()
|
||||
priv, err := ecdsa.GenerateKey(curve, rand.Reader)
|
||||
require.NoError(t, err)
|
||||
h := sha256.Sum256([]byte("stateful"))
|
||||
r, s, err := ecdsa.Sign(rand.Reader, priv, h[:])
|
||||
require.NoError(t, err)
|
||||
in := calldata(t, h[:], r, s, priv.X, priv.Y)
|
||||
|
||||
// Production reaches the wrapper's gas price through an optional interface
|
||||
// (see bindings/cabi), not through StatefulPrecompiledContract, so assert it
|
||||
// exactly that way.
|
||||
estimator, ok := StatefulPrecompile.(interface{ RequiredGas([]byte) uint64 })
|
||||
require.True(t, ok, "the registered precompile must expose RequiredGas")
|
||||
require.Equal(t, uint64(GasP256Verify), estimator.RequiredGas(in))
|
||||
|
||||
out, left, err := StatefulPrecompile.Run(nil, common.Address{}, Address, in, GasP256Verify+5, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, successResult, out)
|
||||
require.Equal(t, uint64(5), left, "exactly RequiredGas is deducted")
|
||||
|
||||
// Exactly enough gas.
|
||||
_, left, err = StatefulPrecompile.Run(nil, common.Address{}, Address, in, GasP256Verify, true)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, left)
|
||||
|
||||
// One short.
|
||||
out, left, err = StatefulPrecompile.Run(nil, common.Address{}, Address, in, GasP256Verify-1, true)
|
||||
require.ErrorIs(t, err, contract.ErrOutOfGas)
|
||||
require.Nil(t, out)
|
||||
require.Zero(t, left)
|
||||
|
||||
// Zero gas.
|
||||
_, _, err = StatefulPrecompile.Run(nil, common.Address{}, Address, in, 0, true)
|
||||
require.ErrorIs(t, err, contract.ErrOutOfGas)
|
||||
|
||||
// A failed verify still costs full price and still returns no error.
|
||||
bad := append([]byte{}, in...)
|
||||
bad[0] ^= 0xFF
|
||||
out, left, err = StatefulPrecompile.Run(nil, common.Address{}, Address, bad, GasP256Verify+2, true)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, out)
|
||||
require.Equal(t, uint64(2), left)
|
||||
}
|
||||
|
||||
// TestRequiredGas_IsFlat: the input is a fixed 160 bytes, so a size-dependent
|
||||
// price would be wrong. Longer input is refused, not metered.
|
||||
func TestRequiredGas_IsFlat(t *testing.T) {
|
||||
c := &Contract{}
|
||||
for _, n := range []int{0, 1, 159, 160, 161, 1 << 16} {
|
||||
require.Equal(t, uint64(GasP256Verify), c.RequiredGas(make([]byte, n)), "len=%d", n)
|
||||
require.Equal(t, uint64(GasP256Verify), (&stateful{}).RequiredGas(make([]byte, n)))
|
||||
}
|
||||
require.Equal(t, uint64(GasP256Verify), c.RequiredGas(nil))
|
||||
}
|
||||
|
||||
// --- module + config ----------------------------------------------------
|
||||
|
||||
func TestRegistryModule_Wiring(t *testing.T) {
|
||||
require.Equal(t, ConfigKey, RegistryModule.ConfigKey)
|
||||
require.Equal(t, Address, RegistryModule.Address)
|
||||
require.Same(t, StatefulPrecompile, RegistryModule.Contract)
|
||||
require.NotNil(t, RegistryModule.Configurator)
|
||||
|
||||
// RIP-7212 fixes the address at 0x100; moving it forks the chain.
|
||||
require.Equal(t, common.HexToAddress(P256VerifyAddress), Address)
|
||||
want, err := hex.DecodeString("0000000000000000000000000000000000000100")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, want, Address.Bytes())
|
||||
}
|
||||
|
||||
func TestConfigurator_Surface(t *testing.T) {
|
||||
c := &configurator{}
|
||||
require.IsType(t, &Config{}, c.MakeConfig())
|
||||
require.NoError(t, c.Configure(nil, nil, nil, nil))
|
||||
}
|
||||
|
||||
func TestConfig_Surface(t *testing.T) {
|
||||
c := &Config{}
|
||||
require.Equal(t, ConfigKey, c.Key())
|
||||
require.Nil(t, c.Timestamp())
|
||||
require.False(t, c.IsDisabled())
|
||||
require.NoError(t, c.Verify(nil))
|
||||
|
||||
ts := uint64(99)
|
||||
a := &Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts}}
|
||||
require.Equal(t, &ts, a.Timestamp())
|
||||
require.True(t, a.Equal(&Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts}}))
|
||||
|
||||
other := uint64(100)
|
||||
require.False(t, a.Equal(&Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &other}}))
|
||||
require.False(t, a.Equal(nil))
|
||||
|
||||
d := &Config{Upgrade: precompileconfig.Upgrade{Disable: true}}
|
||||
require.True(t, d.IsDisabled())
|
||||
}
|
||||
|
||||
// TestRegisterModuleIsIdempotentlyRefused exercises the condition init() panics
|
||||
// on. The panic itself runs before any test binary can observe it.
|
||||
func TestRegisterModuleIsIdempotentlyRefused(t *testing.T) {
|
||||
require.Error(t, modules.RegisterModule(RegistryModule))
|
||||
}
|
||||
+5
-8
@@ -50,10 +50,9 @@ const (
|
||||
MinInputSize = PublicKeySize + SignatureSize + 1
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInputTooShort = errors.New("sr25519: input too short")
|
||||
ErrEmptyMessage = errors.New("sr25519: empty message")
|
||||
)
|
||||
// ErrInputTooShort is the one refusal Run can report: anything shorter than
|
||||
// MinInputSize cannot carry a key, a signature and a message.
|
||||
var ErrInputTooShort = errors.New("sr25519: input too short")
|
||||
|
||||
type sr25519VerifyPrecompile struct{}
|
||||
|
||||
@@ -101,14 +100,12 @@ func (p *sr25519VerifyPrecompile) Run(
|
||||
"%w: expected >= %d bytes, got %d", ErrInputTooShort, MinInputSize, len(input))
|
||||
}
|
||||
|
||||
// MinInputSize already reserves a byte for the message, so the slice below
|
||||
// is never empty and no second emptiness test is possible here.
|
||||
publicKey := input[0:PublicKeySize]
|
||||
signature := input[PublicKeySize : PublicKeySize+SignatureSize]
|
||||
message := input[PublicKeySize+SignatureSize:]
|
||||
|
||||
if len(message) == 0 {
|
||||
return failResult, remainingGas, ErrEmptyMessage
|
||||
}
|
||||
|
||||
// sr25519 is Schnorrkel over Ristretto255 with a Merlin ("substrate")
|
||||
// transcript. There is NO GPU/accel kernel for it: luxfi/accel only
|
||||
// exposes an Ed25519 (Edwards) verify, which is a DIFFERENT scheme on a
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package sr25519
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/precompile/contract"
|
||||
"github.com/luxfi/precompile/modules"
|
||||
"github.com/luxfi/precompile/precompileconfig"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// This package ships TWO verifiers behind one name: sr25519-donna under cgo and
|
||||
// ChainSafe/go-schnorrkel without it. Whichever the node links decides whether a
|
||||
// block is valid, so a single input on which they disagree is a chain split.
|
||||
//
|
||||
// The build tags make it impossible to link both into one binary, so parity
|
||||
// cannot be checked by calling them side by side. What this file does instead:
|
||||
// every expectation below is a CONSTANT -- either a published vector or a
|
||||
// property that must hold of any correct Schnorrkel verifier -- never a value
|
||||
// read back from the verifier under test. Both builds are held to the same
|
||||
// constants, so agreement follows from both passing:
|
||||
//
|
||||
// go test ./sr25519/...
|
||||
// CGO_ENABLED=0 go test ./sr25519/...
|
||||
//
|
||||
// TestParity_CorpusDigest closes the gap the individual cases leave, by pinning
|
||||
// a digest over the verdicts for a large deterministic corpus. Any divergence
|
||||
// anywhere in that corpus changes the digest and fails one of the two builds.
|
||||
|
||||
func hexed(tb testing.TB, s string) []byte {
|
||||
tb.Helper()
|
||||
b, err := hex.DecodeString(s)
|
||||
require.NoError(tb, err)
|
||||
return b
|
||||
}
|
||||
|
||||
func verdict(tb testing.TB, in []byte) bool {
|
||||
tb.Helper()
|
||||
out, _, err := SR25519VerifyPrecompile.Run(
|
||||
nil, common.Address{}, ContractAddress, in, SR25519VerifyPrecompile.RequiredGas(in), true)
|
||||
require.NoError(tb, err)
|
||||
require.Len(tb, out, 32)
|
||||
return out[31] == 1
|
||||
}
|
||||
|
||||
// TestParity_BackendIsNamed makes the run self-describing: a reader of CI output
|
||||
// can tell which verifier was exercised, and a build that linked neither would
|
||||
// not compile.
|
||||
func TestParity_BackendIsNamed(t *testing.T) {
|
||||
require.NotEmpty(t, backend)
|
||||
t.Logf("sr25519 verifier under test: %s", backend)
|
||||
}
|
||||
|
||||
// TestParity_AcceptsTheKnownGoodVector is the one input both backends must
|
||||
// accept. Alice's key and signature are the well-known Substrate development
|
||||
// vector, produced with the "substrate" signing context.
|
||||
func TestParity_AcceptsTheKnownGoodVector(t *testing.T) {
|
||||
require.True(t, verdict(t, buildInput(alicePubKey, aliceSignature, aliceMessage)),
|
||||
"%s must accept the published Alice vector", backend)
|
||||
|
||||
// verifySR25519 and Run must agree; Run adds only gas and framing.
|
||||
require.True(t, verifySR25519(alicePubKey, aliceSignature, aliceMessage))
|
||||
}
|
||||
|
||||
// TestParity_StructuredRefusals holds both backends to the same verdict on the
|
||||
// inputs where two Ristretto/Schnorrkel implementations most plausibly differ:
|
||||
// canonicality of the compressed public key, the identity encoding, the
|
||||
// signature's scalar half, and the sign bit Schnorrkel packs into it.
|
||||
func TestParity_StructuredRefusals(t *testing.T) {
|
||||
// Ristretto255 encodings that no correct decoder accepts: field elements at
|
||||
// or above p, and the all-ones word.
|
||||
nonCanonical := []string{
|
||||
"edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", // p
|
||||
"eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", // p+1
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", // all ones
|
||||
"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", // p-1, s = -1
|
||||
"00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"0100000000000000000000000000000000000000000000000000000000000000", // negative field element
|
||||
}
|
||||
for _, enc := range nonCanonical {
|
||||
t.Run("pubkey/"+enc[:12], func(t *testing.T) {
|
||||
require.False(t, verdict(t, buildInput(hexed(t, enc), aliceSignature, aliceMessage)),
|
||||
"%s must not accept a non-canonical Ristretto public key", backend)
|
||||
})
|
||||
t.Run("sigR/"+enc[:12], func(t *testing.T) {
|
||||
sig := append(append([]byte{}, hexed(t, enc)...), aliceSignature[32:]...)
|
||||
require.False(t, verdict(t, buildInput(alicePubKey, sig, aliceMessage)))
|
||||
})
|
||||
}
|
||||
|
||||
// The Ristretto identity is a canonical encoding but carries no key.
|
||||
identity := make([]byte, PublicKeySize)
|
||||
require.False(t, verdict(t, buildInput(identity, aliceSignature, aliceMessage)),
|
||||
"the identity element must not verify as a public key")
|
||||
|
||||
// Schnorrkel sets the high bit of the last signature byte; clearing it, and
|
||||
// every other single-bit mutation, must break the signature.
|
||||
for _, at := range []int{0, 1, 31, 32, 33, 62, 63} {
|
||||
for _, bit := range []byte{0x01, 0x80} {
|
||||
sig := append([]byte{}, aliceSignature...)
|
||||
sig[at] ^= bit
|
||||
require.False(t, verdict(t, buildInput(alicePubKey, sig, aliceMessage)),
|
||||
"signature byte %d bit %#x must matter", at, bit)
|
||||
}
|
||||
}
|
||||
|
||||
// The scalar half must be reduced. Adding the Ristretto group order L to s
|
||||
// gives a second encoding of the same scalar; a verifier that reduces rather
|
||||
// than refuses would accept it, which is signature malleability.
|
||||
//
|
||||
// L = 2^252 + 27742317777372353535851937790883648493, little-endian.
|
||||
l := hexed(t, "edd3f55c1a631258d69cf7a2def9de1400000000000000000000000000000010")
|
||||
malleated := append([]byte{}, aliceSignature...)
|
||||
carry := 0
|
||||
for i := range 32 {
|
||||
sum := int(malleated[32+i]&0x7F) + int(l[i]) + carry
|
||||
if i == 31 {
|
||||
sum = int(malleated[63]&0x7F) + int(l[31]) + carry
|
||||
}
|
||||
malleated[32+i] = byte(sum)
|
||||
carry = sum >> 8
|
||||
}
|
||||
malleated[63] |= 0x80 // restore Schnorrkel's marker bit
|
||||
require.NotEqual(t, aliceSignature, malleated, "the mutation must change the encoding")
|
||||
require.False(t, verdict(t, buildInput(alicePubKey, malleated, aliceMessage)),
|
||||
"s must be canonical: s + L must not verify")
|
||||
|
||||
// Every field is bound: key, signature and message each break it alone.
|
||||
require.False(t, verdict(t, buildInput(
|
||||
hexed(t, "8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48"),
|
||||
aliceSignature, aliceMessage)), "a different public key must not verify")
|
||||
require.False(t, verdict(t, buildInput(alicePubKey, aliceSignature,
|
||||
append(append([]byte{}, aliceMessage...), '!'))), "an extended message must not verify")
|
||||
require.False(t, verdict(t, buildInput(alicePubKey, aliceSignature,
|
||||
aliceMessage[:len(aliceMessage)-1])), "a truncated message must not verify")
|
||||
require.False(t, verdict(t, buildInput(alicePubKey, make([]byte, SignatureSize), aliceMessage)),
|
||||
"the zero signature must not verify")
|
||||
require.False(t, verdict(t, make([]byte, MinInputSize)), "an all-zero call must not verify")
|
||||
|
||||
ones := make([]byte, MinInputSize)
|
||||
for i := range ones {
|
||||
ones[i] = 0xFF
|
||||
}
|
||||
require.False(t, verdict(t, ones), "an all-ones call must not verify")
|
||||
}
|
||||
|
||||
// parityCorpus builds a deterministic set of calls from a SHA-256 chain: no
|
||||
// clock, no randomness, no map order. Both builds see byte-identical inputs.
|
||||
func parityCorpus() [][]byte {
|
||||
const n = 512
|
||||
corpus := make([][]byte, 0, n)
|
||||
seed := sha256.Sum256([]byte("lux/precompile/sr25519 parity corpus v1"))
|
||||
block := seed
|
||||
|
||||
next := func() [32]byte {
|
||||
block = sha256.Sum256(block[:])
|
||||
return block
|
||||
}
|
||||
|
||||
for i := range n {
|
||||
// Message length walks 1..17 bytes so the per-byte gas arm varies too.
|
||||
msgLen := 1 + i%17
|
||||
in := make([]byte, 0, PublicKeySize+SignatureSize+msgLen)
|
||||
|
||||
switch i % 4 {
|
||||
case 0:
|
||||
// Fully pseudorandom.
|
||||
a, b := next(), next()
|
||||
in = append(in, a[:]...)
|
||||
in = append(in, b[:]...)
|
||||
c := next()
|
||||
in = append(in, c[:]...)
|
||||
case 1:
|
||||
// Alice's real key with a pseudorandom signature.
|
||||
a, b := next(), next()
|
||||
in = append(in, alicePubKey...)
|
||||
in = append(in, a[:]...)
|
||||
in = append(in, b[:]...)
|
||||
case 2:
|
||||
// Alice's real signature under a pseudorandom key.
|
||||
a := next()
|
||||
in = append(in, a[:]...)
|
||||
in = append(in, aliceSignature...)
|
||||
case 3:
|
||||
// Alice's real key and signature over a different message.
|
||||
in = append(in, alicePubKey...)
|
||||
in = append(in, aliceSignature...)
|
||||
}
|
||||
|
||||
// Pad or trim to exactly pubkey+sig+msgLen with a deterministic tail.
|
||||
want := PublicKeySize + SignatureSize + msgLen
|
||||
for len(in) < want {
|
||||
t := next()
|
||||
in = append(in, t[:]...)
|
||||
}
|
||||
corpus = append(corpus, in[:want])
|
||||
}
|
||||
return corpus
|
||||
}
|
||||
|
||||
// TestParity_CorpusDigest is the cross-backend check the per-case tests cannot
|
||||
// give on their own. It hashes the verdict for all 512 deterministic calls into
|
||||
// one digest and compares it to a pinned constant. The two builds cannot be
|
||||
// linked together, so this digest is how their agreement is stated: if
|
||||
// sr25519-donna and go-schnorrkel differ on ANY input in the corpus, the digest
|
||||
// differs and one of the two runs fails.
|
||||
//
|
||||
// The pinned value was produced by running this test under both builds and
|
||||
// confirming they matched. Regenerating it because it "went red" defeats the
|
||||
// entire purpose -- a mismatch means the backends disagree, which is a
|
||||
// consensus split, not a stale constant.
|
||||
func TestParity_CorpusDigest(t *testing.T) {
|
||||
corpus := parityCorpus()
|
||||
require.Len(t, corpus, 512)
|
||||
|
||||
h := sha256.New()
|
||||
accepted := 0
|
||||
for i, in := range corpus {
|
||||
// The corpus itself is hashed in, so a change to the generator shows up
|
||||
// as a mismatch rather than silently comparing different inputs.
|
||||
var hdr [8]byte
|
||||
binary.BigEndian.PutUint32(hdr[:4], uint32(i))
|
||||
binary.BigEndian.PutUint32(hdr[4:], uint32(len(in)))
|
||||
h.Write(hdr[:])
|
||||
h.Write(in)
|
||||
|
||||
v := verdict(t, in)
|
||||
if v {
|
||||
accepted++
|
||||
h.Write([]byte{1})
|
||||
} else {
|
||||
h.Write([]byte{0})
|
||||
}
|
||||
}
|
||||
|
||||
require.Zero(t, accepted,
|
||||
"no pseudorandom call may verify; %s accepted %d of %d", backend, accepted, len(corpus))
|
||||
require.Equal(t,
|
||||
"cbe358fc9189609bf42d61e5baa0472f3483f37b0324848d68720c3d22c80412",
|
||||
hex.EncodeToString(h.Sum(nil)),
|
||||
"%s disagrees with the pinned cross-backend verdict corpus", backend)
|
||||
}
|
||||
|
||||
// TestParity_GasIsBackendIndependent: the price is computed from the input
|
||||
// length alone, so both builds must charge identically for identical calldata.
|
||||
func TestParity_GasIsBackendIndependent(t *testing.T) {
|
||||
for _, n := range []int{0, 1, 96, 97, 128, 1024, 65536} {
|
||||
in := make([]byte, n)
|
||||
want := SR25519VerifyBaseGas
|
||||
if n > PublicKeySize+SignatureSize {
|
||||
want += uint64(n-PublicKeySize-SignatureSize) * SR25519VerifyPerByteGas
|
||||
}
|
||||
require.Equal(t, want, SR25519VerifyPrecompile.RequiredGas(in), "len=%d", n)
|
||||
}
|
||||
require.Equal(t, SR25519VerifyBaseGas, SR25519VerifyPrecompile.RequiredGas(nil))
|
||||
|
||||
// Price rises by exactly the per-byte rate for each extra message byte.
|
||||
prev := SR25519VerifyPrecompile.RequiredGas(make([]byte, MinInputSize))
|
||||
for n := MinInputSize + 1; n <= MinInputSize+64; n++ {
|
||||
got := SR25519VerifyPrecompile.RequiredGas(make([]byte, n))
|
||||
require.Equal(t, SR25519VerifyPerByteGas, got-prev, "len=%d", n)
|
||||
prev = got
|
||||
}
|
||||
|
||||
// The base fee is charged even for calls too short to verify, so a truncated
|
||||
// call is not a free probe.
|
||||
require.Equal(t, SR25519VerifyBaseGas, SR25519VerifyPrecompile.RequiredGas(make([]byte, 96)))
|
||||
}
|
||||
|
||||
// TestParity_GasDeduction covers exact, surplus and insufficient supply.
|
||||
func TestParity_GasDeduction(t *testing.T) {
|
||||
in := buildInput(alicePubKey, aliceSignature, aliceMessage)
|
||||
price := SR25519VerifyPrecompile.RequiredGas(in)
|
||||
|
||||
out, left, err := SR25519VerifyPrecompile.Run(nil, common.Address{}, ContractAddress, in, price+37, true)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, byte(1), out[31])
|
||||
require.Equal(t, uint64(37), left)
|
||||
|
||||
_, left, err = SR25519VerifyPrecompile.Run(nil, common.Address{}, ContractAddress, in, price, true)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, left)
|
||||
|
||||
out, left, err = SR25519VerifyPrecompile.Run(nil, common.Address{}, ContractAddress, in, price-1, true)
|
||||
require.ErrorIs(t, err, contract.ErrOutOfGas)
|
||||
require.Nil(t, out)
|
||||
require.Zero(t, left)
|
||||
|
||||
_, _, err = SR25519VerifyPrecompile.Run(nil, common.Address{}, ContractAddress, in, 0, true)
|
||||
require.ErrorIs(t, err, contract.ErrOutOfGas)
|
||||
|
||||
// A short call is refused with an error, and the base fee is still deducted.
|
||||
out, left, err = SR25519VerifyPrecompile.Run(nil, common.Address{}, ContractAddress,
|
||||
make([]byte, MinInputSize-1), SR25519VerifyBaseGas+5, true)
|
||||
require.ErrorIs(t, err, ErrInputTooShort)
|
||||
require.Equal(t, failResult, out)
|
||||
require.Equal(t, uint64(5), left)
|
||||
}
|
||||
|
||||
// TestParity_LengthBoundary: one byte below MinInputSize is refused, exactly
|
||||
// MinInputSize is answered.
|
||||
func TestParity_LengthBoundary(t *testing.T) {
|
||||
for n := 0; n < MinInputSize; n++ {
|
||||
in := make([]byte, n)
|
||||
out, _, err := SR25519VerifyPrecompile.Run(nil, common.Address{}, ContractAddress,
|
||||
in, SR25519VerifyPrecompile.RequiredGas(in)+1000, true)
|
||||
require.ErrorIs(t, err, ErrInputTooShort, "len=%d", n)
|
||||
require.Equal(t, failResult, out)
|
||||
}
|
||||
|
||||
in := make([]byte, MinInputSize)
|
||||
out, _, err := SR25519VerifyPrecompile.Run(nil, common.Address{}, ContractAddress,
|
||||
in, SR25519VerifyPrecompile.RequiredGas(in), true)
|
||||
require.NoError(t, err, "the minimum length is answered, not refused")
|
||||
require.Equal(t, byte(0), out[31])
|
||||
}
|
||||
|
||||
// TestParity_HelperGuards covers verifySR25519's own preconditions. Run cannot
|
||||
// reach them -- it slices fixed widths from an already length-checked input --
|
||||
// but the helper is package surface and the guards are what keep a future
|
||||
// caller from indexing past the end.
|
||||
func TestParity_HelperGuards(t *testing.T) {
|
||||
good := make([]byte, PublicKeySize)
|
||||
sig := make([]byte, SignatureSize)
|
||||
|
||||
for _, n := range []int{0, 1, 31, 33, 64} {
|
||||
require.False(t, verifySR25519(make([]byte, n), sig, []byte("m")), "pubkey len=%d", n)
|
||||
}
|
||||
for _, n := range []int{0, 1, 63, 65, 128} {
|
||||
require.False(t, verifySR25519(good, make([]byte, n), []byte("m")), "sig len=%d", n)
|
||||
}
|
||||
require.False(t, verifySR25519(good, sig, nil))
|
||||
require.False(t, verifySR25519(good, sig, []byte{}))
|
||||
require.False(t, verifySR25519(nil, nil, nil))
|
||||
}
|
||||
|
||||
// TestParity_Deterministic: no clock, no randomness, no map iteration. Repeated
|
||||
// calls in any order return the same bytes.
|
||||
func TestParity_Deterministic(t *testing.T) {
|
||||
cases := [][]byte{
|
||||
buildInput(alicePubKey, aliceSignature, aliceMessage),
|
||||
buildInput(alicePubKey, aliceSignature, []byte("other")),
|
||||
make([]byte, MinInputSize),
|
||||
}
|
||||
first := make([]bool, len(cases))
|
||||
for i, in := range cases {
|
||||
first[i] = verdict(t, in)
|
||||
}
|
||||
for range 16 {
|
||||
for i := len(cases) - 1; i >= 0; i-- {
|
||||
require.Equal(t, first[i], verdict(t, cases[i]), "case %d is not deterministic", i)
|
||||
}
|
||||
}
|
||||
require.True(t, first[0])
|
||||
require.False(t, first[1])
|
||||
require.False(t, first[2])
|
||||
}
|
||||
|
||||
// TestParity_ReadOnlyAndCallerAreIrrelevant: the precompile touches no state.
|
||||
func TestParity_ReadOnlyAndCallerAreIrrelevant(t *testing.T) {
|
||||
in := buildInput(alicePubKey, aliceSignature, aliceMessage)
|
||||
gas := SR25519VerifyPrecompile.RequiredGas(in)
|
||||
|
||||
ro, _, err := SR25519VerifyPrecompile.Run(nil, common.Address{}, ContractAddress, in, gas, true)
|
||||
require.NoError(t, err)
|
||||
rw, _, err := SR25519VerifyPrecompile.Run(nil, common.HexToAddress("0xbeef"), common.Address{}, in, gas, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ro, rw)
|
||||
}
|
||||
|
||||
// --- module wiring ------------------------------------------------------
|
||||
|
||||
func TestParity_ModuleWiring(t *testing.T) {
|
||||
// This package builds its registry entry inline in init() rather than
|
||||
// exporting it, so the registration is restated here.
|
||||
entry := modules.Module{
|
||||
ConfigKey: ConfigKey,
|
||||
Address: ContractAddress,
|
||||
Contract: SR25519VerifyPrecompile,
|
||||
Configurator: &configurator{},
|
||||
}
|
||||
require.Equal(t, "sr25519Verify", entry.ConfigKey)
|
||||
require.Equal(t,
|
||||
common.HexToAddress("0x0A00000000000000000000000000000000000001"),
|
||||
ContractAddress)
|
||||
require.Equal(t, ContractAddress, SR25519VerifyPrecompile.Address())
|
||||
|
||||
require.IsType(t, &Config{}, (&configurator{}).MakeConfig())
|
||||
require.NoError(t, (&configurator{}).Configure(nil, nil, nil, nil))
|
||||
require.False(t, (&Config{}).Equal(&struct{ precompileconfig.Config }{}))
|
||||
|
||||
// Registering the same address twice is refused, which is the condition
|
||||
// init() panics on. The panic itself runs before any test can observe it.
|
||||
require.Error(t, modules.RegisterModule(entry))
|
||||
}
|
||||
@@ -12,6 +12,10 @@ package sr25519
|
||||
import "C"
|
||||
import "unsafe"
|
||||
|
||||
// backend names the verifier this build linked. The two builds must return the
|
||||
// same verdict for every input; see the parity corpus in the tests.
|
||||
const backend = "sr25519-donna (cgo)"
|
||||
|
||||
// verifySR25519 verifies an sr25519 Schnorrkel signature using the
|
||||
// sr25519-donna C library. Signing context is hardcoded to "substrate".
|
||||
func verifySR25519(publicKey, signature, message []byte) bool {
|
||||
|
||||
@@ -9,6 +9,10 @@ import (
|
||||
schnorrkel "github.com/ChainSafe/go-schnorrkel"
|
||||
)
|
||||
|
||||
// backend names the verifier this build linked. The two builds must return the
|
||||
// same verdict for every input; see the parity corpus in the tests.
|
||||
const backend = "go-schnorrkel (pure Go)"
|
||||
|
||||
// verifySR25519 is the pure-Go fallback when CGO is disabled.
|
||||
// Uses ChainSafe/go-schnorrkel for Substrate-compatible sr25519 verification.
|
||||
func verifySR25519(publicKey, signature, message []byte) bool {
|
||||
|
||||
+13
-11
@@ -100,25 +100,27 @@ func (p *x25519Precompile) scalarMult(data []byte, gas uint64) ([]byte, uint64,
|
||||
if len(data) < 64 {
|
||||
return nil, gas, ErrInvalidInput
|
||||
}
|
||||
scalar := data[:32]
|
||||
point := data[32:64]
|
||||
|
||||
shared, err := curve25519.X25519(scalar, point)
|
||||
if err != nil {
|
||||
return nil, gas, ErrDHFailed
|
||||
}
|
||||
return shared, gas, nil
|
||||
return mult(data[:32], data[32:64], gas)
|
||||
}
|
||||
|
||||
// basepoint is scalarMult against u = 9, so it goes through the same code.
|
||||
// Splitting them gave the basepoint arm its own unreachable error branch:
|
||||
// X25519 fails only on an all-zero result, and the base point generates the
|
||||
// prime-order subgroup, so a clamped scalar can never land on the identity.
|
||||
func (p *x25519Precompile) basepoint(data []byte, gas uint64) ([]byte, uint64, error) {
|
||||
if len(data) < 32 {
|
||||
return nil, gas, ErrInvalidInput
|
||||
}
|
||||
scalar := data[:32]
|
||||
return mult(data[:32], curve25519.Basepoint, gas)
|
||||
}
|
||||
|
||||
pub, err := curve25519.X25519(scalar, curve25519.Basepoint)
|
||||
// mult is the one place the ladder is called and the one place its refusal --
|
||||
// a low-order point, which drives the shared secret to zero for every scalar --
|
||||
// turns into an error.
|
||||
func mult(scalar, point []byte, gas uint64) ([]byte, uint64, error) {
|
||||
out, err := curve25519.X25519(scalar, point)
|
||||
if err != nil {
|
||||
return nil, gas, ErrDHFailed
|
||||
}
|
||||
return pub, gas, nil
|
||||
return out, gas, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package x25519
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/precompile/contract"
|
||||
"github.com/luxfi/precompile/modules"
|
||||
"github.com/luxfi/precompile/precompileconfig"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func kat(tb testing.TB, s string) []byte {
|
||||
tb.Helper()
|
||||
b, err := hex.DecodeString(s)
|
||||
require.NoError(tb, err)
|
||||
require.Len(tb, b, 32)
|
||||
return b
|
||||
}
|
||||
|
||||
func call(tb testing.TB, op byte, args ...[]byte) ([]byte, uint64, error) {
|
||||
tb.Helper()
|
||||
in := []byte{op}
|
||||
for _, a := range args {
|
||||
in = append(in, a...)
|
||||
}
|
||||
return X25519Precompile.Run(nil, common.Address{}, ContractAddress, in, 1<<20, true)
|
||||
}
|
||||
|
||||
// basepointU is the Curve25519 base point u = 9 (RFC 7748 section 4.1).
|
||||
const basepointU = "0900000000000000000000000000000000000000000000000000000000000000"
|
||||
|
||||
// TestKAT_RFC7748_ScalarMult pins the two scalar-multiplication vectors from
|
||||
// RFC 7748 section 5.2. Both were reproduced with an independent Montgomery
|
||||
// ladder before being written here.
|
||||
func TestKAT_RFC7748_ScalarMult(t *testing.T) {
|
||||
for _, v := range []struct{ name, scalar, u, want string }{
|
||||
{
|
||||
name: "vector 1",
|
||||
scalar: "a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4",
|
||||
u: "e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c",
|
||||
want: "c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552",
|
||||
},
|
||||
{
|
||||
name: "vector 2",
|
||||
scalar: "4b66e9d4d1b4673c5ad22691957d6af5c11b6421e0ea01d42ca4169e7918ba0d",
|
||||
u: "e5210f12786811d3f4b7959d0538ae2c31dbe7106fc03c3efc4cd549c715a493",
|
||||
want: "95cbde9476e8907d7aade45cb4b873f88b595a68799fa152e6f8f7647aac7957",
|
||||
},
|
||||
} {
|
||||
t.Run(v.name, func(t *testing.T) {
|
||||
out, _, err := call(t, OpScalarMult, kat(t, v.scalar), kat(t, v.u))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, kat(t, v.want), out)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestKAT_RFC7748_DiffieHellman pins section 6.1: both public keys and the
|
||||
// shared secret are published constants, so both directions are covered.
|
||||
func TestKAT_RFC7748_DiffieHellman(t *testing.T) {
|
||||
const (
|
||||
alicePriv = "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a"
|
||||
alicePub = "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a"
|
||||
bobPriv = "5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb"
|
||||
bobPub = "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f"
|
||||
shared = "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742"
|
||||
)
|
||||
|
||||
ap, _, err := call(t, OpBasepoint, kat(t, alicePriv))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, kat(t, alicePub), ap, "Alice's published public key")
|
||||
|
||||
bp, _, err := call(t, OpBasepoint, kat(t, bobPriv))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, kat(t, bobPub), bp, "Bob's published public key")
|
||||
|
||||
// Basepoint must be exactly ScalarMult against u = 9: two code paths, one
|
||||
// answer. A divergence here is a silent key-agreement failure.
|
||||
viaMult, _, err := call(t, OpScalarMult, kat(t, alicePriv), kat(t, basepointU))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ap, viaMult)
|
||||
|
||||
sa, _, err := call(t, OpScalarMult, kat(t, alicePriv), kat(t, bobPub))
|
||||
require.NoError(t, err)
|
||||
sb, _, err := call(t, OpScalarMult, kat(t, bobPriv), kat(t, alicePub))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, kat(t, shared), sa, "published shared secret")
|
||||
require.Equal(t, sa, sb, "Diffie-Hellman must commute")
|
||||
}
|
||||
|
||||
// TestKAT_RFC7748_Iterated runs the section 5.2 iteration. The value after one
|
||||
// round is published; running a thousand rounds and matching the published
|
||||
// thousand-round value exercises the ladder across a thousand distinct scalars
|
||||
// and points, which no single vector can.
|
||||
func TestKAT_RFC7748_Iterated(t *testing.T) {
|
||||
k := kat(t, basepointU)
|
||||
u := kat(t, basepointU)
|
||||
|
||||
for i := 1; i <= 1000; i++ {
|
||||
out, _, err := call(t, OpScalarMult, k, u)
|
||||
require.NoError(t, err, "iteration %d", i)
|
||||
u, k = k, out
|
||||
switch i {
|
||||
case 1:
|
||||
require.Equal(t,
|
||||
kat(t, "422c8e7a6227d7bca1350b3e2bb7279f7897b87bb6854b783c60e80311ae3079"), k,
|
||||
"RFC 7748 iteration 1")
|
||||
case 1000:
|
||||
require.Equal(t,
|
||||
kat(t, "684cf59ba83309552800ef566f2f4d3c1c3887c49360e3875f2eb94d99532c51"), k,
|
||||
"RFC 7748 iteration 1000")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lowOrderU lists every u-coordinate whose X25519 output is all zeros for any
|
||||
// clamped scalar: the seven canonical ones from RFC 7748 section 6.1's security
|
||||
// note plus the five that differ only in the ignored high bit. Each was checked
|
||||
// against an independent ladder.
|
||||
var lowOrderU = []string{
|
||||
"0000000000000000000000000000000000000000000000000000000000000000", // 0
|
||||
"0100000000000000000000000000000000000000000000000000000000000000", // 1
|
||||
"e0eb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b800", // order 8
|
||||
"5f9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f1157", // order 4
|
||||
"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", // p-1
|
||||
"edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", // p
|
||||
"eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", // p+1
|
||||
"e0eb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b880", // high bit set
|
||||
"5f9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f11d7",
|
||||
"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
}
|
||||
|
||||
// TestRefusal_LowOrderPoint is the contributory-behaviour refusal. A peer who
|
||||
// sends one of these forces the shared secret to zero no matter what scalar the
|
||||
// caller holds, so accepting it would let anyone fix both sides of a key
|
||||
// agreement. Returning zero bytes and no error would be worse than useless:
|
||||
// the caller would key a cipher with a constant.
|
||||
func TestRefusal_LowOrderPoint(t *testing.T) {
|
||||
scalar := kat(t, "a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4")
|
||||
for _, u := range lowOrderU {
|
||||
t.Run(u[:12], func(t *testing.T) {
|
||||
out, gas, err := call(t, OpScalarMult, scalar, kat(t, u))
|
||||
require.ErrorIs(t, err, ErrDHFailed, "low-order point must be refused")
|
||||
require.Nil(t, out)
|
||||
require.Equal(t, uint64(1<<20-GasScalarMult), gas, "gas is charged before the check")
|
||||
})
|
||||
}
|
||||
|
||||
// Every one of them is also refused as a basepoint scalar? No: the scalar is
|
||||
// clamped, so any 32 bytes are a legal scalar. Basepoint must succeed.
|
||||
for _, u := range lowOrderU {
|
||||
out, _, err := call(t, OpBasepoint, kat(t, u))
|
||||
require.NoError(t, err, "any 32 bytes is a valid scalar once clamped")
|
||||
require.Len(t, out, 32)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRefusal_Length covers short, empty and nil for both operations, and pins
|
||||
// that trailing bytes past the fixed operand width are ignored rather than
|
||||
// refused -- so calldata is NOT a unique identifier for a call. A caller
|
||||
// deduplicating by calldata hash must trim first.
|
||||
func TestRefusal_Length(t *testing.T) {
|
||||
scalar := kat(t, "a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4")
|
||||
u := kat(t, basepointU)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
in []byte
|
||||
err error
|
||||
}{
|
||||
{"nil", nil, ErrInvalidInput},
|
||||
{"empty", []byte{}, ErrInvalidInput},
|
||||
{"scalarmult op only", []byte{OpScalarMult}, ErrInvalidInput},
|
||||
{"basepoint op only", []byte{OpBasepoint}, ErrInvalidInput},
|
||||
{"scalarmult one short", append([]byte{OpScalarMult}, append(append([]byte{}, scalar...), u[:31]...)...), ErrInvalidInput},
|
||||
{"basepoint one short", append([]byte{OpBasepoint}, scalar[:31]...), ErrInvalidInput},
|
||||
{"unknown op 0x00", []byte{0x00, 1, 2, 3}, ErrInvalidOp},
|
||||
{"unknown op 0x03", []byte{0x03, 1, 2, 3}, ErrInvalidOp},
|
||||
{"unknown op 0xff", []byte{0xFF}, ErrInvalidOp},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
out, _, err := X25519Precompile.Run(nil, common.Address{}, ContractAddress, tc.in, 1<<20, true)
|
||||
require.ErrorIs(t, err, tc.err)
|
||||
require.Nil(t, out)
|
||||
})
|
||||
}
|
||||
|
||||
// Trailing data is ignored, not refused: two encodings, one result.
|
||||
short, _, err := call(t, OpBasepoint, scalar)
|
||||
require.NoError(t, err)
|
||||
long, _, err := call(t, OpBasepoint, scalar, make([]byte, 97))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, short, long, "trailing bytes are ignored by design")
|
||||
|
||||
shortM, _, err := call(t, OpScalarMult, scalar, u)
|
||||
require.NoError(t, err)
|
||||
longM, _, err := call(t, OpScalarMult, scalar, u, []byte{0xAA})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, shortM, longM)
|
||||
}
|
||||
|
||||
// TestGas_Schedule covers every arm of RequiredGas and the deduction in Run.
|
||||
// The zero-gas arm is only safe because the matching Run arm refuses: a call
|
||||
// that costs nothing must also do nothing.
|
||||
func TestGas_Schedule(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
in []byte
|
||||
want uint64
|
||||
}{
|
||||
{"nil", nil, 0},
|
||||
{"empty", []byte{}, 0},
|
||||
{"scalarmult", []byte{OpScalarMult}, GasScalarMult},
|
||||
{"basepoint", []byte{OpBasepoint}, GasBasepoint},
|
||||
{"unknown 0x00", []byte{0x00}, 0},
|
||||
{"unknown 0x03", []byte{0x03}, 0},
|
||||
{"unknown 0xff", []byte{0xFF}, 0},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require.Equal(t, tc.want, X25519Precompile.RequiredGas(tc.in))
|
||||
})
|
||||
}
|
||||
|
||||
// Work is a fixed-size ladder, so the price must not move with input size.
|
||||
long := append([]byte{OpScalarMult}, make([]byte, 1<<16)...)
|
||||
require.Equal(t, uint64(GasScalarMult), X25519Precompile.RequiredGas(long))
|
||||
|
||||
// Every zero-priced input must refuse in Run: no work is bought for free.
|
||||
for _, in := range [][]byte{nil, {}, {0x00}, {0x03}, {0xFF}, {0x00, 1, 2}} {
|
||||
require.Zero(t, X25519Precompile.RequiredGas(in))
|
||||
out, _, err := X25519Precompile.Run(nil, common.Address{}, ContractAddress, in, 0, true)
|
||||
require.Error(t, err, "a zero-gas input must not execute")
|
||||
require.Nil(t, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGas_Deduction(t *testing.T) {
|
||||
scalar := kat(t, "a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4")
|
||||
in := append([]byte{OpBasepoint}, scalar...)
|
||||
|
||||
_, left, err := X25519Precompile.Run(nil, common.Address{}, ContractAddress, in, GasBasepoint+13, true)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(13), left)
|
||||
|
||||
_, left, err = X25519Precompile.Run(nil, common.Address{}, ContractAddress, in, GasBasepoint, true)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, left)
|
||||
|
||||
_, left, err = X25519Precompile.Run(nil, common.Address{}, ContractAddress, in, GasBasepoint-1, true)
|
||||
require.ErrorIs(t, err, contract.ErrOutOfGas)
|
||||
require.Zero(t, left)
|
||||
|
||||
_, _, err = X25519Precompile.Run(nil, common.Address{}, ContractAddress, in, 0, true)
|
||||
require.ErrorIs(t, err, contract.ErrOutOfGas)
|
||||
}
|
||||
|
||||
// TestReadOnlyIsIrrelevant: the precompile touches no state, so the readOnly
|
||||
// flag and the caller address must not change the answer.
|
||||
func TestReadOnlyIsIrrelevant(t *testing.T) {
|
||||
scalar := kat(t, "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a")
|
||||
in := append([]byte{OpBasepoint}, scalar...)
|
||||
|
||||
ro, _, err := X25519Precompile.Run(nil, common.Address{}, ContractAddress, in, 1<<20, true)
|
||||
require.NoError(t, err)
|
||||
rw, _, err := X25519Precompile.Run(nil, common.HexToAddress("0xdead"), common.Address{}, in, 1<<20, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, ro, rw)
|
||||
}
|
||||
|
||||
// --- module + config ----------------------------------------------------
|
||||
|
||||
func TestModule_Wiring(t *testing.T) {
|
||||
require.Equal(t, ConfigKey, Module.ConfigKey)
|
||||
require.Equal(t, ContractAddress, Module.Address)
|
||||
require.Same(t, X25519Precompile, Module.Contract)
|
||||
require.NotNil(t, Module.Configurator)
|
||||
require.Equal(t,
|
||||
common.HexToAddress("0x0000000000000000000000000000000000009203"),
|
||||
ContractAddress)
|
||||
}
|
||||
|
||||
func TestConfigurator_Surface(t *testing.T) {
|
||||
c := &configurator{}
|
||||
require.IsType(t, &Config{}, c.MakeConfig())
|
||||
require.NoError(t, c.Configure(nil, nil, nil, nil))
|
||||
}
|
||||
|
||||
func TestConfig_Surface(t *testing.T) {
|
||||
c := &Config{}
|
||||
require.Equal(t, ConfigKey, c.Key())
|
||||
require.Nil(t, c.Timestamp())
|
||||
require.False(t, c.IsDisabled())
|
||||
require.NoError(t, c.Verify(nil))
|
||||
|
||||
ts := uint64(7)
|
||||
a := &Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts}}
|
||||
require.Equal(t, &ts, a.Timestamp())
|
||||
require.True(t, a.Equal(&Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &ts}}))
|
||||
|
||||
other := uint64(8)
|
||||
require.False(t, a.Equal(&Config{Upgrade: precompileconfig.Upgrade{BlockTimestamp: &other}}))
|
||||
require.False(t, a.Equal(nil))
|
||||
require.True(t, (&Config{Upgrade: precompileconfig.Upgrade{Disable: true}}).IsDisabled())
|
||||
}
|
||||
|
||||
func TestRegisterModuleIsIdempotentlyRefused(t *testing.T) {
|
||||
require.Error(t, modules.RegisterModule(Module),
|
||||
"a second registration must be refused, which is what init() panics on")
|
||||
}
|
||||
Reference in New Issue
Block a user