mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
magnetar forges under a single reverted read, measured: declare 7924 bytes, put the true final byte in the spare capacity, and the precompile answers verdict word 1 over 7925 bytes. len=7924 cap=8181, no panic, no error. That is what a missing bound costs when the input is a window into EVM memory rather than a copy. ring does not forge on one reverted read, and the reason is structural rather than lucky: every length-bearing field is followed by another bound, and the trailing open field is s[low:], which bounds against len even when the read before it walked into cap. So the over-read lands as a halt instead. Recorded as a halt, not a forgery. magnetar's exact framing survives as End(); deleting only that term accepts a single trailing byte, which would let one signature ride many calldatas. zk: plonkVerify reads its nine points and six evaluations through the cursor. Two floors in verifyPLONK and verifyFflonk survived deletion because the only thing they protected was the KEYED path — with a key registered, a short proof must come back malformed rather than merely false, and nothing went down that path. That distinction now has a test. parseFflonkProof's floor was redundant against its own reads and is gone. Removed in ring: a nil check on a slice make() cannot return nil, left over from the GPU batch path deleted under M-06.
411 lines
12 KiB
Go
411 lines
12 KiB
Go
// Copyright (C) 2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Package ring implements LSAG (Linkable Spontaneous Anonymous Group) ring signature
|
|
// verification precompile for the Lux EVM. Address: 0x9202 (Lux Crypto Privacy range)
|
|
//
|
|
// Ring signatures enable privacy transactions where the sender's identity
|
|
// is hidden among a set of possible signers.
|
|
//
|
|
// Operations:
|
|
// - 0x02: Verify -- verify an LSAG ring signature
|
|
//
|
|
// Sign and ComputeKeyImage are intentionally excluded: both require the
|
|
// signer's private key in calldata, which is public on-chain. Signing
|
|
// MUST be performed off-chain.
|
|
//
|
|
// GPU: LSAG verify loop is sequential (each step feeds the next c[i+1]).
|
|
// Individual EC scalar mults within each step could be parallelized on GPU
|
|
// via secp256k1_recover.metal's ec_mul_affine, but the sequential dependency
|
|
// limits speedup. Batch ring verify across a block (multiple ring sig txs)
|
|
// is the effective GPU path via parallel.BlockExecutor.
|
|
// See LP-3664 for full specification.
|
|
package ring
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
|
|
gnarksecp "github.com/consensys/gnark-crypto/ecc/secp256k1"
|
|
"github.com/luxfi/crypto/secp256k1"
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/luxfi/precompile/contract"
|
|
)
|
|
|
|
var (
|
|
// ContractAddress is the address of the Ring Signature precompile (Lux Crypto Privacy range 0x9202)
|
|
ContractAddress = common.HexToAddress("0x9202")
|
|
|
|
// Singleton instance
|
|
RingSignaturePrecompile = &ringSignaturePrecompile{}
|
|
|
|
_ contract.StatefulPrecompiledContract = &ringSignaturePrecompile{}
|
|
|
|
ErrInvalidInput = contract.ErrInvalidInput
|
|
ErrInvalidScheme = errors.New("invalid signature scheme")
|
|
ErrInvalidRingSize = errors.New("ring size must be >= 2")
|
|
ErrInvalidSignature = errors.New("invalid ring signature")
|
|
ErrInvalidPublicKey = errors.New("invalid public key in ring")
|
|
)
|
|
|
|
// Operation selectors -- verify only, sign is excluded for key safety
|
|
const (
|
|
OpVerify = 0x02
|
|
)
|
|
|
|
// Scheme IDs
|
|
const (
|
|
SchemeLSAGSecp256k1 = 0x01
|
|
SchemeLSAGEd25519 = 0x02
|
|
SchemeDualRing = 0x03
|
|
SchemeLatticeLSAG = 0x10
|
|
)
|
|
|
|
// Sizes
|
|
const (
|
|
CompressedPubKeySize = 33
|
|
ScalarSize = 32
|
|
)
|
|
|
|
// Gas costs
|
|
const (
|
|
GasVerifyBase = 4000
|
|
GasVerifyPerMember = 2500
|
|
)
|
|
|
|
type ringSignaturePrecompile struct{}
|
|
|
|
// Address returns the address of the Ring Signature precompile
|
|
func (p *ringSignaturePrecompile) Address() common.Address {
|
|
return ContractAddress
|
|
}
|
|
|
|
// RequiredGas calculates gas for ring signature operations.
|
|
//
|
|
// The price is read off the three-byte header [op][scheme][ring size] and
|
|
// nothing else. A call too short to carry the header, or naming another
|
|
// operation, is free -- Run refuses it before any verification work happens.
|
|
// A price cannot report a parse failure, so a short read is that free case.
|
|
func (p *ringSignaturePrecompile) RequiredGas(input []byte) uint64 {
|
|
in := contract.Read(input)
|
|
op, opErr := in.Byte()
|
|
scheme, schemeErr := in.Byte()
|
|
ringSize, ringSizeErr := in.Byte()
|
|
if opErr != nil || schemeErr != nil || ringSizeErr != nil || op != OpVerify {
|
|
return 0
|
|
}
|
|
|
|
var baseGas, perMemberGas uint64
|
|
|
|
switch scheme {
|
|
case SchemeLSAGSecp256k1:
|
|
baseGas = GasVerifyBase
|
|
perMemberGas = GasVerifyPerMember
|
|
case SchemeLSAGEd25519:
|
|
baseGas = GasVerifyBase - 1000
|
|
perMemberGas = GasVerifyPerMember - 500
|
|
case SchemeLatticeLSAG:
|
|
baseGas = 50000
|
|
perMemberGas = 10000
|
|
default:
|
|
return 0
|
|
}
|
|
|
|
return baseGas + uint64(ringSize)*perMemberGas
|
|
}
|
|
|
|
// Run executes the Ring Signature precompile
|
|
func (p *ringSignaturePrecompile) Run(
|
|
accessibleState contract.AccessibleState,
|
|
caller common.Address,
|
|
addr common.Address,
|
|
input []byte,
|
|
suppliedGas uint64,
|
|
readOnly bool,
|
|
) ([]byte, uint64, error) {
|
|
gasCost := p.RequiredGas(input)
|
|
remainingGas, err := contract.DeductGas(suppliedGas, gasCost)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// The same three-byte header RequiredGas priced. The ring size is read
|
|
// here rather than inside verify so that calldata too short to carry the
|
|
// header is refused as short calldata whatever operation or scheme it
|
|
// names -- and so the refusal is reachable through the precompile, which
|
|
// is where it has to hold.
|
|
in := contract.Read(input)
|
|
op, opErr := in.Byte()
|
|
scheme, schemeErr := in.Byte()
|
|
ringSize, ringSizeErr := in.Byte()
|
|
if opErr != nil || schemeErr != nil || ringSizeErr != nil {
|
|
return nil, remainingGas, ErrInvalidInput
|
|
}
|
|
|
|
var result []byte
|
|
|
|
switch op {
|
|
case OpVerify:
|
|
result, err = p.verify(scheme, int(ringSize), in)
|
|
default:
|
|
err = fmt.Errorf("unsupported operation: 0x%02x", op)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, remainingGas, err
|
|
}
|
|
|
|
return result, remainingGas, nil
|
|
}
|
|
|
|
// LSAGSignature represents an LSAG ring signature
|
|
type LSAGSignature struct {
|
|
KeyImage []byte // 33 bytes
|
|
C []*big.Int // n challenges
|
|
S []*big.Int // n responses
|
|
}
|
|
|
|
// verify verifies an LSAG ring signature. Run has already read the header, so
|
|
// in is positioned at the first ring member and ringSize is what the size byte
|
|
// claimed -- a claim, not a measurement, which is why every field below is
|
|
// taken from the cursor rather than sliced off calldata. See
|
|
// contract/cursor.go: past the declared end lies memory the caller filled, so
|
|
// a ring read by slice expression is a ring the caller chose.
|
|
func (p *ringSignaturePrecompile) verify(scheme byte, ringSize int, in *contract.Cursor) ([]byte, error) {
|
|
if scheme != SchemeLSAGSecp256k1 {
|
|
return nil, ErrInvalidScheme
|
|
}
|
|
|
|
if ringSize < 2 {
|
|
return nil, ErrInvalidRingSize
|
|
}
|
|
|
|
// Parse ring
|
|
ring := make([][]byte, ringSize)
|
|
for i := range ringSize {
|
|
member, err := in.Bytes(CompressedPubKeySize)
|
|
if err != nil {
|
|
return nil, ErrInvalidInput
|
|
}
|
|
ring[i] = member
|
|
}
|
|
|
|
// Signature: keyImage (33) + c[n] (32 each) + s[n] (32 each)
|
|
sigLen := CompressedPubKeySize + ringSize*ScalarSize + ringSize*ScalarSize
|
|
signature, err := in.Bytes(uint64(sigLen))
|
|
if err != nil {
|
|
return nil, ErrInvalidInput
|
|
}
|
|
|
|
// The format ends open: whatever follows the signature is the message, so
|
|
// there is no End() here. Rest stops at the declared end all the same.
|
|
message := in.Rest()
|
|
|
|
// Parse and verify signature
|
|
sig, err := parseLSAGSignature(signature, ringSize)
|
|
if err != nil {
|
|
return []byte{0x00}, nil
|
|
}
|
|
|
|
valid := lsagVerify(ring, sig, message)
|
|
if valid {
|
|
return []byte{0x01}, nil
|
|
}
|
|
return []byte{0x00}, nil
|
|
}
|
|
|
|
// lsagVerify verifies an LSAG signature.
|
|
//
|
|
// GPU fast path: when accel is available, batch SHA256 precomputes all
|
|
// hashToPoint values in a single GPU dispatch. The verify loop itself is
|
|
// sequential (c[i+1] depends on c[i]) so EC scalar mults remain on CPU.
|
|
// For block-level parallelism (many ring sig txs), use parallel.BlockExecutor.
|
|
func lsagVerify(ring [][]byte, sig *LSAGSignature, message []byte) bool {
|
|
n := len(ring)
|
|
curve := secp256k1.S256()
|
|
|
|
// Parse key image
|
|
imgX, imgY := secp256k1.DecompressPubkey(sig.KeyImage)
|
|
if imgX == nil {
|
|
return false
|
|
}
|
|
|
|
// Precompute all hash-to-point values. batchHashToPoint returns
|
|
// make([]*Point, len(ring)), which is never nil and always exactly n long,
|
|
// so there is nothing here to check: the nil arm that used to sit here was
|
|
// a fossil of the GPU batch path removed under M-06, and no test could
|
|
// reach it.
|
|
hps := batchHashToPoint(ring)
|
|
|
|
// Verify ring -- sequential: c[i+1] = H(m, L_i, R_i)
|
|
cPrev := sig.C[0]
|
|
for i := range n {
|
|
// Parse P[i]
|
|
pkX, pkY := secp256k1.DecompressPubkey(ring[i])
|
|
if pkX == nil {
|
|
return false
|
|
}
|
|
|
|
// L = s[i] * G + c[i] * P[i]
|
|
sGx, sGy := curve.ScalarBaseMult(sig.S[i].Bytes())
|
|
cPx, cPy := curve.ScalarMult(pkX, pkY, cPrev.Bytes())
|
|
|
|
// R = s[i] * H(P[i]) + c[i] * I
|
|
hp := hps[i]
|
|
sHx, sHy := curve.ScalarMult(hp.X, hp.Y, sig.S[i].Bytes())
|
|
cIx, cIy := curve.ScalarMult(imgX, imgY, cPrev.Bytes())
|
|
|
|
// A scalar of zero, or one at or above the group order, has no
|
|
// multiple: libsecp256k1 refuses it and hands back a nil point.
|
|
// s[i] and c[0] are read straight from calldata, so refuse here --
|
|
// passing nil to Add dereferences it.
|
|
if sGx == nil || cPx == nil || sHx == nil || cIx == nil {
|
|
return false
|
|
}
|
|
|
|
Lx, Ly := curve.Add(sGx, sGy, cPx, cPy)
|
|
Rx, Ry := curve.Add(sHx, sHy, cIx, cIy)
|
|
|
|
// c[i+1] = H(m, L, R)
|
|
cNext := hashRing(message, Lx, Ly, Rx, Ry)
|
|
|
|
if i == n-1 {
|
|
return cNext.Cmp(sig.C[0]) == 0
|
|
}
|
|
cPrev = cNext
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// batchHashToPoint precomputes hashToPoint for every ring member.
|
|
//
|
|
// Each call uses RFC 9380 hash-to-curve (SVDW map) which produces a point
|
|
// whose DLOG relative to G is unknown. The previous GPU batch SHA256 path
|
|
// was removed because it fed hashes into ScalarBaseMult (producing h*G
|
|
// with known DLOG, breaking LSAG anonymity -- see M-06).
|
|
func batchHashToPoint(ring [][]byte) []*Point {
|
|
n := len(ring)
|
|
points := make([]*Point, n)
|
|
for i := range n {
|
|
points[i] = hashToPoint(ring[i])
|
|
}
|
|
return points
|
|
}
|
|
|
|
// Point represents a curve point
|
|
type Point struct {
|
|
X, Y *big.Int
|
|
}
|
|
|
|
// hashToPoint maps a public key to a curve point whose discrete log relative
|
|
// to the generator G is unknown. This is critical for LSAG security: if
|
|
// DLOG(H(P), G) were computable, an attacker could extract the signer's
|
|
// identity from the key image.
|
|
//
|
|
// Uses gnark-crypto's SVDW-based HashToG1 (RFC 9380 compliant). The domain
|
|
// separation tag "LUX-LSAG-H2C-SECP256K1" binds the output to this protocol.
|
|
func hashToPoint(pk []byte) *Point {
|
|
dst := []byte("LUX-LSAG-H2C-SECP256K1")
|
|
pt, err := gnarksecp.HashToG1(pk, dst)
|
|
if err != nil {
|
|
// Unreachable: HashToG1 fails only for a DST over 255 bytes or an
|
|
// expansion over 8160 bytes; ours is a 22-byte constant.
|
|
pt, _ = gnarksecp.EncodeToG1(pk, dst)
|
|
}
|
|
x := new(big.Int)
|
|
y := new(big.Int)
|
|
pt.X.BigInt(x)
|
|
pt.Y.BigInt(y)
|
|
return &Point{X: x, Y: y}
|
|
}
|
|
|
|
func hashRing(msg []byte, Lx, Ly, Rx, Ry *big.Int) *big.Int {
|
|
h := sha256.New()
|
|
h.Write(msg)
|
|
h.Write(padTo32(Lx.Bytes()))
|
|
h.Write(padTo32(Ly.Bytes()))
|
|
h.Write(padTo32(Rx.Bytes()))
|
|
h.Write(padTo32(Ry.Bytes()))
|
|
return new(big.Int).SetBytes(h.Sum(nil))
|
|
}
|
|
|
|
func padTo32(b []byte) []byte {
|
|
if len(b) >= 32 {
|
|
return b
|
|
}
|
|
padded := make([]byte, 32)
|
|
copy(padded[32-len(b):], b)
|
|
return padded
|
|
}
|
|
|
|
func (sig *LSAGSignature) Serialize() []byte {
|
|
n := len(sig.C)
|
|
result := make([]byte, CompressedPubKeySize+n*ScalarSize*2)
|
|
copy(result, sig.KeyImage)
|
|
|
|
offset := CompressedPubKeySize
|
|
for i := range n {
|
|
copy(result[offset:], padTo32(sig.C[i].Bytes()))
|
|
offset += ScalarSize
|
|
}
|
|
for i := range n {
|
|
copy(result[offset:], padTo32(sig.S[i].Bytes()))
|
|
offset += ScalarSize
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// order is the secp256k1 group order.
|
|
var order = secp256k1.S256().N
|
|
|
|
// scalar reads one 32-byte field as a group scalar in [1, N-1].
|
|
//
|
|
// Zero has no curve multiple, and a value at or above N is a second encoding
|
|
// of one already below it. Refusing both is what keeps the answer the same
|
|
// everywhere: libsecp256k1 rejects those scalars, while the pure-Go fallback
|
|
// reduces them silently, so the same bytes verify on one build and not the
|
|
// other. This is the low-S rule of ECDSA applied to LSAG.
|
|
func scalar(b []byte) (*big.Int, bool) {
|
|
x := new(big.Int).SetBytes(b)
|
|
return x, x.Sign() > 0 && x.Cmp(order) < 0
|
|
}
|
|
|
|
// parseLSAGSignature reads keyImage || c[0..n-1] || s[0..n-1] out of data.
|
|
// Trailing bytes are ignored rather than refused: verify hands over exactly
|
|
// the field it bounded, so a longer data can only come from a direct caller.
|
|
func parseLSAGSignature(data []byte, ringSize int) (*LSAGSignature, error) {
|
|
in := contract.Read(data)
|
|
|
|
keyImage, err := in.Bytes(CompressedPubKeySize)
|
|
if err != nil {
|
|
return nil, ErrInvalidSignature
|
|
}
|
|
|
|
sig := &LSAGSignature{
|
|
KeyImage: keyImage,
|
|
C: make([]*big.Int, ringSize),
|
|
S: make([]*big.Int, ringSize),
|
|
}
|
|
|
|
// Challenges then responses: same shape, same rule, read back to back.
|
|
for _, field := range [][]*big.Int{sig.C, sig.S} {
|
|
for i := range field {
|
|
b, err := in.Bytes(ScalarSize)
|
|
if err != nil {
|
|
return nil, ErrInvalidSignature
|
|
}
|
|
x, ok := scalar(b)
|
|
if !ok {
|
|
return nil, ErrInvalidSignature
|
|
}
|
|
field[i] = x
|
|
}
|
|
}
|
|
|
|
return sig, nil
|
|
}
|