mirror of
https://github.com/luxfi/precompile.git
synced 2026-07-27 03:33:45 +00:00
Public permissionless launch policy: enable basically every precompile for builder convenience, ESPECIALLY wallet-curve VERIFY so users sign natively on Lux from other chains (ed25519=Solana, sr25519=Polkadot, secp256r1=WebAuthn, secp256k1/ecrecover=Ethereum). Disable ONLY actual security risks. Lux's own consensus and identity stay PQ (quasar/p3q) — enforced in the consensus layer, never by refusing an EVM verifier a dapp asked for. Two layers, decomplected: - builder EVM precompile surface : enable-all-verify (this change) - chain consensus / finality : PQ-strict (consensus module, untouched) Removed the RefuseUnderStrictPQ gate from 17 verify-only / key-safe custom precompiles: ed25519, sr25519, secp256r1, bls12381 (EIP-2537, x7 ops), kzg4844 (EIP-4844), blake3, poseidon, pedersen, babyjubjub, pasta, ring, vrf, hpke, curve25519, x25519, cggmp21, frost, and the classical SNARK verifiers in zk (Groth16/PLONK/Halo2/KZG/IPA/range/batch/commitment). zk fflonk (0x03) stays DISABLED — but on its OWN forge-bug mechanism (ErrFflonkDisabled, returned at dispatch), NOT strict-PQ: verifyFflonk has a nil-vk soundness hole that forges any statement. Security disable, fully PQ-independent. The RefuseUnderStrictPQ helper + ErrClassicalForbiddenInPQ + StrictPQReporter had zero remaining code callers (evm uses a local structural interface) — deleted contract/strict_pq.go and its test. Rewrote zk's gate test to assert the new policy (classical ops enabled, fflonk disabled). Removed the now- orphaned isPedersenCommitment; fixed stale comments referencing deleted symbols. Build: full module green. Tests: contract + zk + all 17 edited packages pass. NOTE: the STANDARD eth precompiles (ecrecover, p256Verify, sha256, ripemd160, blake2f, bls12381, kzg) are still refused by LuxStrictPQ() in the evm plugin — a follow-up commit flips that to Permissive so ecrecover (every Ethereum dapp) works at launch.
344 lines
9.0 KiB
Go
344 lines
9.0 KiB
Go
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Package pasta implements Pallas + Vesta curve operations precompile.
|
|
// Address: 0x0500000000000000000000000000000000000008 (Hashing range)
|
|
//
|
|
// The "Pasta" curves form a 2-cycle: Pallas's base field is Vesta's scalar field
|
|
// and vice versa. This enables efficient recursive proof composition (IVC).
|
|
//
|
|
// Operations (first byte selects curve: 0x01=Pallas, 0x02=Vesta):
|
|
// - 0x01: PointAdd — P1(64) + P2(64) -> P3(64)
|
|
// - 0x02: ScalarMul — P(64) + scalar(32) -> P*s(64)
|
|
// - 0x03: MSM — n pairs of (point, scalar)
|
|
//
|
|
// Used by: Halo2, Zcash Orchard, recursive SNARKs, Nova/SuperNova.
|
|
package pasta
|
|
|
|
import (
|
|
"errors"
|
|
"math/big"
|
|
|
|
"github.com/consensys/gnark-crypto/ecc/stark-curve/fp"
|
|
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/luxfi/precompile/contract"
|
|
)
|
|
|
|
var (
|
|
ContractAddress = common.HexToAddress("0x0500000000000000000000000000000000000008")
|
|
|
|
PastaPrecompile = &pastaPrecompile{}
|
|
|
|
_ contract.StatefulPrecompiledContract = &pastaPrecompile{}
|
|
|
|
ErrInvalidInput = contract.ErrInvalidInput
|
|
ErrInvalidOp = errors.New("invalid pasta operation")
|
|
ErrInvalidCurve = errors.New("invalid curve selector (0x01=Pallas, 0x02=Vesta)")
|
|
ErrNotOnCurve = errors.New("point not on curve")
|
|
)
|
|
|
|
const (
|
|
CurvePallas = 0x01
|
|
CurveVesta = 0x02
|
|
|
|
OpPointAdd = 0x01
|
|
OpScalarMul = 0x02
|
|
OpMSM = 0x03
|
|
|
|
PointLen = 64
|
|
|
|
GasPointAdd = 2500
|
|
GasScalarMul = 8000
|
|
GasMSMBase = 8000
|
|
GasMSMPerPt = 6000
|
|
)
|
|
|
|
type pastaPrecompile struct{}
|
|
|
|
func (p *pastaPrecompile) RequiredGas(input []byte) uint64 {
|
|
if len(input) < 2 {
|
|
return 0
|
|
}
|
|
switch input[1] {
|
|
case OpPointAdd:
|
|
return GasPointAdd
|
|
case OpScalarMul:
|
|
return GasScalarMul
|
|
case OpMSM:
|
|
pairSize := PointLen + 32
|
|
dataLen := len(input) - 2
|
|
if dataLen < pairSize {
|
|
return 0
|
|
}
|
|
n := dataLen / pairSize
|
|
return GasMSMBase + uint64(n)*GasMSMPerPt
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func (p *pastaPrecompile) Run(
|
|
accessibleState contract.AccessibleState,
|
|
caller common.Address,
|
|
addr common.Address,
|
|
input []byte,
|
|
suppliedGas uint64,
|
|
readOnly bool,
|
|
) ([]byte, uint64, error) {
|
|
gasCost := p.RequiredGas(input)
|
|
gas, err := contract.DeductGas(suppliedGas, gasCost)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if len(input) < 2 {
|
|
return nil, gas, ErrInvalidInput
|
|
}
|
|
|
|
curveID := input[0]
|
|
op := input[1]
|
|
data := input[2:]
|
|
|
|
if curveID != CurvePallas && curveID != CurveVesta {
|
|
return nil, gas, ErrInvalidCurve
|
|
}
|
|
|
|
// Both Pallas and Vesta are short Weierstrass curves y^2 = x^3 + 5
|
|
// with different moduli. We use field arithmetic from gnark-crypto's
|
|
// Stark curve field (255-bit) as a compatible implementation.
|
|
// In production, use dedicated Pasta field implementations.
|
|
|
|
switch op {
|
|
case OpPointAdd:
|
|
return p.pointAdd(curveID, data, gas)
|
|
case OpScalarMul:
|
|
return p.scalarMul(curveID, data, gas)
|
|
case OpMSM:
|
|
return p.msm(curveID, data, gas)
|
|
default:
|
|
return nil, gas, ErrInvalidOp
|
|
}
|
|
}
|
|
|
|
// Simplified point representation using big.Int for the field
|
|
// Full production implementation would use dedicated Pallas/Vesta fields
|
|
type point struct {
|
|
x, y *big.Int
|
|
inf bool
|
|
}
|
|
|
|
// Pallas: y^2 = x^3 + 5 over Fp (p = 2^254 + 0x224698fc094cf91b992d30ed00000001)
|
|
// Vesta: y^2 = x^3 + 5 over Fq (q = 2^254 + 0x224698fc0994a8dd8c46eb2100000001)
|
|
var (
|
|
pallasP, _ = new(big.Int).SetString("28948022309329048855892746252171976963363056481941560715954676764349967630337", 10)
|
|
vestaP, _ = new(big.Int).SetString("28948022309329048855892746252171976963363056481941647379679742748393362948097", 10)
|
|
curveB = big.NewInt(5)
|
|
)
|
|
|
|
func modulus(curveID byte) *big.Int {
|
|
if curveID == CurvePallas {
|
|
return pallasP
|
|
}
|
|
return vestaP
|
|
}
|
|
|
|
func decodePoint(curveID byte, data []byte) (point, error) {
|
|
if len(data) < PointLen {
|
|
return point{}, ErrInvalidInput
|
|
}
|
|
x := new(big.Int).SetBytes(data[:32])
|
|
y := new(big.Int).SetBytes(data[32:64])
|
|
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)
|
|
x3.Mul(x3, x)
|
|
x3.Mod(x3, p)
|
|
rhs := new(big.Int).Add(x3, curveB)
|
|
rhs.Mod(rhs, p)
|
|
if lhs.Cmp(rhs) != 0 {
|
|
return point{}, ErrNotOnCurve
|
|
}
|
|
return point{x: x, y: y}, nil
|
|
}
|
|
|
|
func encodePoint(pt point) []byte {
|
|
result := make([]byte, PointLen)
|
|
if pt.inf {
|
|
return result
|
|
}
|
|
xBytes := pt.x.Bytes()
|
|
yBytes := pt.y.Bytes()
|
|
copy(result[32-len(xBytes):32], xBytes)
|
|
copy(result[64-len(yBytes):64], yBytes)
|
|
return result
|
|
}
|
|
|
|
// addPoints adds two points on the short Weierstrass curve
|
|
func addPoints(curveID byte, a, b point) point {
|
|
if a.inf {
|
|
return b
|
|
}
|
|
if b.inf {
|
|
return a
|
|
}
|
|
p := modulus(curveID)
|
|
|
|
if a.x.Cmp(b.x) == 0 {
|
|
if a.y.Cmp(b.y) == 0 {
|
|
return doublePoint(curveID, a)
|
|
}
|
|
return point{inf: true}
|
|
}
|
|
|
|
// slope = (y2-y1) / (x2-x1) mod p
|
|
dy := new(big.Int).Sub(b.y, a.y)
|
|
dx := new(big.Int).Sub(b.x, a.x)
|
|
dx.ModInverse(dx, p)
|
|
s := new(big.Int).Mul(dy, dx)
|
|
s.Mod(s, p)
|
|
|
|
// x3 = s^2 - x1 - x2
|
|
x3 := new(big.Int).Mul(s, s)
|
|
x3.Sub(x3, a.x)
|
|
x3.Sub(x3, b.x)
|
|
x3.Mod(x3, p)
|
|
|
|
// y3 = s*(x1-x3) - y1
|
|
y3 := new(big.Int).Sub(a.x, x3)
|
|
y3.Mul(y3, s)
|
|
y3.Sub(y3, a.y)
|
|
y3.Mod(y3, p)
|
|
|
|
return point{x: x3, y: y3}
|
|
}
|
|
|
|
func doublePoint(curveID byte, a point) point {
|
|
if a.inf || a.y.Sign() == 0 {
|
|
return point{inf: true}
|
|
}
|
|
p := modulus(curveID)
|
|
|
|
// slope = (3*x^2) / (2*y) mod p (a=0 for Pasta curves)
|
|
x2 := new(big.Int).Mul(a.x, a.x)
|
|
x2.Mod(x2, p)
|
|
num := new(big.Int).Mul(x2, big.NewInt(3))
|
|
den := new(big.Int).Mul(a.y, big.NewInt(2))
|
|
den.ModInverse(den, p)
|
|
s := new(big.Int).Mul(num, den)
|
|
s.Mod(s, p)
|
|
|
|
x3 := new(big.Int).Mul(s, s)
|
|
x3.Sub(x3, new(big.Int).Mul(a.x, big.NewInt(2)))
|
|
x3.Mod(x3, p)
|
|
|
|
y3 := new(big.Int).Sub(a.x, x3)
|
|
y3.Mul(y3, s)
|
|
y3.Sub(y3, a.y)
|
|
y3.Mod(y3, p)
|
|
|
|
return point{x: x3, y: y3}
|
|
}
|
|
|
|
// scalarMulPoint computes scalar * pt using a Montgomery ladder.
|
|
//
|
|
// The Montgomery ladder processes every bit of the scalar with the same
|
|
// sequence of operations (one add + one double per bit), regardless of the
|
|
// bit value. This eliminates the timing side-channel present in the naive
|
|
// double-and-add algorithm, where an attacker can distinguish 0-bits from
|
|
// 1-bits by measuring execution time or power consumption.
|
|
//
|
|
// Note: the underlying field operations (addPoints, doublePoint) still use
|
|
// math/big which is not fully constant-time in its internal limb operations.
|
|
// A production deployment should use gnark-crypto's native Pasta field
|
|
// arithmetic when available. The Montgomery ladder eliminates the dominant
|
|
// side-channel (branch on secret bits) regardless.
|
|
func scalarMulPoint(curveID byte, pt point, scalar *big.Int) point {
|
|
if scalar.Sign() == 0 || pt.inf {
|
|
return point{inf: true}
|
|
}
|
|
|
|
// Reduce scalar modulo curve order for canonical form.
|
|
// Pallas and Vesta have the same group order as each other's base field
|
|
// (they form a 2-cycle), so we use the other curve's modulus as the order.
|
|
order := modulus(curveID ^ 0x03) // Pallas(0x01)^0x03=Vesta(0x02), Vesta(0x02)^0x03=Pallas(0x01)
|
|
k := new(big.Int).Mod(scalar, order)
|
|
if k.Sign() == 0 {
|
|
return point{inf: true}
|
|
}
|
|
|
|
// Montgomery ladder: R0 starts at identity, R1 starts at pt.
|
|
// For each bit from MSB to LSB:
|
|
// if bit == 0: R1 = R0 + R1, R0 = 2*R0
|
|
// if bit == 1: R0 = R0 + R1, R1 = 2*R1
|
|
r0 := point{inf: true}
|
|
r1 := pt
|
|
|
|
for i := k.BitLen() - 1; i >= 0; i-- {
|
|
if k.Bit(i) == 0 {
|
|
r1 = addPoints(curveID, r0, r1)
|
|
r0 = doublePoint(curveID, r0)
|
|
} else {
|
|
r0 = addPoints(curveID, r0, r1)
|
|
r1 = doublePoint(curveID, r1)
|
|
}
|
|
}
|
|
return r0
|
|
}
|
|
|
|
func (p *pastaPrecompile) pointAdd(curveID byte, data []byte, gas uint64) ([]byte, uint64, error) {
|
|
if len(data) < 2*PointLen {
|
|
return nil, gas, ErrInvalidInput
|
|
}
|
|
a, err := decodePoint(curveID, data[:PointLen])
|
|
if err != nil {
|
|
return nil, gas, err
|
|
}
|
|
b, err := decodePoint(curveID, data[PointLen:2*PointLen])
|
|
if err != nil {
|
|
return nil, gas, err
|
|
}
|
|
result := addPoints(curveID, a, b)
|
|
return encodePoint(result), gas, nil
|
|
}
|
|
|
|
func (p *pastaPrecompile) scalarMul(curveID byte, data []byte, gas uint64) ([]byte, uint64, error) {
|
|
if len(data) < PointLen+32 {
|
|
return nil, gas, ErrInvalidInput
|
|
}
|
|
pt, err := decodePoint(curveID, data[:PointLen])
|
|
if err != nil {
|
|
return nil, gas, err
|
|
}
|
|
scalar := new(big.Int).SetBytes(data[PointLen : PointLen+32])
|
|
result := scalarMulPoint(curveID, pt, scalar)
|
|
return encodePoint(result), gas, nil
|
|
}
|
|
|
|
func (p *pastaPrecompile) msm(curveID byte, data []byte, gas uint64) ([]byte, uint64, error) {
|
|
pairSize := PointLen + 32
|
|
if len(data) == 0 || len(data)%pairSize != 0 {
|
|
return nil, gas, ErrInvalidInput
|
|
}
|
|
n := len(data) / pairSize
|
|
result := point{inf: true}
|
|
for i := range n {
|
|
offset := i * pairSize
|
|
pt, err := decodePoint(curveID, data[offset:offset+PointLen])
|
|
if err != nil {
|
|
return nil, gas, err
|
|
}
|
|
scalar := new(big.Int).SetBytes(data[offset+PointLen : offset+pairSize])
|
|
term := scalarMulPoint(curveID, pt, scalar)
|
|
result = addPoints(curveID, result, term)
|
|
}
|
|
return encodePoint(result), gas, nil
|
|
}
|
|
|
|
// Ensure fp import is used (gnark-crypto Stark field for future optimized path)
|
|
var _ = fp.Modulus()
|