mirror of
https://github.com/luxfi/accel.git
synced 2026-10-30 17:03:45 +00:00
The SigMLDSA65 case of batchVerifyGPU was calling the single-sig DilithiumVerify ONCE for the entire batch, then broadcasting the single boolean to every output position. A batch of N=3 with verdicts [true, false, true] would return [true, true, true] or [false, false, false] depending on which side the single dispatch short-circuited on. The EVM precompile (lux/precompile/mldsa/ contract.go:339) writes that broadcast junk to its output; CPU-only validators (batchVerifyCPU) compute per-element verdicts. Verdict asymmetry across the validator set = consensus split. Fix: route SigMLDSA65 through LatticeOps.MLDSAVerifyBatch — the real batch entry which writes per-element verdicts into a uint8[n] results tensor, same shape as the ECDSA / Ed25519 / BLS cases. The unified Sync + ToSlice + bytes→bool conversion below handles the result identically. Honors the Red M-1 (CRITICAL #176) propagation policy: ErrInvalidArgument from the C ABI is a HARD error — propagate it so the precompile fails closed instead of silently falling back to CPU. Other accel errors (NotSupported / OutOfMemory / KernelFailed) remain recoverable. Adds TestRed177_SigMLDSA65_NoBroadcastSemantics and TestRed177_SigMLDSA65_InvalidArgumentPropagation in ops/crypto/crypto_mldsa_batch_test.go to pin the per-element-result shape contract and the propagation policy.
460 lines
12 KiB
Go
460 lines
12 KiB
Go
//go:build accel
|
|
|
|
package crypto
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"github.com/luxfi/accel"
|
|
)
|
|
|
|
// GPU-accelerated cryptographic implementations.
|
|
// These dispatch to the accel library's GPU backends (CUDA, Metal, Vulkan)
|
|
// for parallel signature verification, hashing, and EC operations.
|
|
|
|
func batchVerifyGPU(sess *accel.Session, sigType SigType, sigs, msgs, pubkeys [][]byte) ([]bool, error) {
|
|
n := len(sigs)
|
|
if n == 0 {
|
|
return []bool{}, nil
|
|
}
|
|
|
|
// Determine sizes based on signature type
|
|
var sigSize, msgSize, pkSize int
|
|
switch sigType {
|
|
case SigECDSA:
|
|
sigSize = 64 // r || s
|
|
msgSize = 32 // message hash
|
|
pkSize = 33 // compressed secp256k1 pubkey
|
|
case SigEd25519:
|
|
sigSize = 64 // Ed25519 signature
|
|
msgSize = 32 // message (or hash)
|
|
pkSize = 32 // Ed25519 public key
|
|
case SigBLS:
|
|
sigSize = 96 // BLS12-381 G2 point
|
|
msgSize = 32 // message hash
|
|
pkSize = 48 // BLS12-381 G1 point
|
|
case SigMLDSA65:
|
|
sigSize = 3309 // ML-DSA-65 signature
|
|
msgSize = 32 // message hash
|
|
pkSize = 1952 // ML-DSA-65 public key
|
|
default:
|
|
return batchVerifyCPU(sigType, sigs, msgs, pubkeys)
|
|
}
|
|
|
|
// Pack signatures into flat byte array
|
|
sigBytes := make([]byte, n*sigSize)
|
|
for i, sig := range sigs {
|
|
if len(sig) >= sigSize {
|
|
copy(sigBytes[i*sigSize:(i+1)*sigSize], sig[:sigSize])
|
|
} else {
|
|
copy(sigBytes[i*sigSize:i*sigSize+len(sig)], sig)
|
|
}
|
|
}
|
|
|
|
// Pack messages
|
|
msgBytes := make([]byte, n*msgSize)
|
|
for i, msg := range msgs {
|
|
if len(msg) >= msgSize {
|
|
copy(msgBytes[i*msgSize:(i+1)*msgSize], msg[:msgSize])
|
|
} else {
|
|
copy(msgBytes[i*msgSize:i*msgSize+len(msg)], msg)
|
|
}
|
|
}
|
|
|
|
// Pack public keys
|
|
pkBytes := make([]byte, n*pkSize)
|
|
for i, pk := range pubkeys {
|
|
if len(pk) >= pkSize {
|
|
copy(pkBytes[i*pkSize:(i+1)*pkSize], pk[:pkSize])
|
|
} else {
|
|
copy(pkBytes[i*pkSize:i*pkSize+len(pk)], pk)
|
|
}
|
|
}
|
|
|
|
// Create GPU tensors
|
|
msgTensor, err := accel.NewTensorWithData[uint8](sess, []int{n, msgSize}, msgBytes)
|
|
if err != nil {
|
|
return batchVerifyCPU(sigType, sigs, msgs, pubkeys)
|
|
}
|
|
defer msgTensor.Close()
|
|
|
|
sigTensor, err := accel.NewTensorWithData[uint8](sess, []int{n, sigSize}, sigBytes)
|
|
if err != nil {
|
|
return batchVerifyCPU(sigType, sigs, msgs, pubkeys)
|
|
}
|
|
defer sigTensor.Close()
|
|
|
|
pkTensor, err := accel.NewTensorWithData[uint8](sess, []int{n, pkSize}, pkBytes)
|
|
if err != nil {
|
|
return batchVerifyCPU(sigType, sigs, msgs, pubkeys)
|
|
}
|
|
defer pkTensor.Close()
|
|
|
|
// Result tensor (1 byte per signature: 0 = invalid, 1 = valid)
|
|
resultTensor, err := accel.NewTensor[uint8](sess, []int{n})
|
|
if err != nil {
|
|
return batchVerifyCPU(sigType, sigs, msgs, pubkeys)
|
|
}
|
|
defer resultTensor.Close()
|
|
|
|
// Dispatch to appropriate GPU kernel
|
|
cryptoOps := sess.Crypto()
|
|
switch sigType {
|
|
case SigECDSA:
|
|
err = cryptoOps.ECDSAVerifyBatch(
|
|
msgTensor.Untyped(),
|
|
sigTensor.Untyped(),
|
|
pkTensor.Untyped(),
|
|
resultTensor.Untyped(),
|
|
)
|
|
case SigEd25519:
|
|
err = cryptoOps.Ed25519VerifyBatch(
|
|
msgTensor.Untyped(),
|
|
sigTensor.Untyped(),
|
|
pkTensor.Untyped(),
|
|
resultTensor.Untyped(),
|
|
)
|
|
case SigBLS:
|
|
err = cryptoOps.BLSVerifyBatch(
|
|
msgTensor.Untyped(),
|
|
sigTensor.Untyped(),
|
|
pkTensor.Untyped(),
|
|
resultTensor.Untyped(),
|
|
)
|
|
case SigMLDSA65:
|
|
// Red CRITICAL #177: real per-element batch dispatch via the
|
|
// FIPS 204 batch entry LatticeOps.MLDSAVerifyBatch. The prior
|
|
// code called the single-sig DilithiumVerify ONCE for the
|
|
// whole batch and broadcasted the single boolean to every
|
|
// output position — pure consensus-split fuel. A batch of
|
|
// [valid, INVALID, valid] returned [true, true, true] (or
|
|
// [false, false, false] if the single dispatch happened to
|
|
// short-circuit on the malformed entry), shipping wrong
|
|
// verdicts into the EVM precompile output (mldsa/contract.go:
|
|
// 339) for any GPU-accelerated validator. CPU-only validators
|
|
// (batchVerifyCPU computes per-element verdicts) would see
|
|
// [true, false, true]. Verdict asymmetry across the validator
|
|
// set = consensus split on the EVM precompile path.
|
|
//
|
|
// MLDSAVerifyBatch writes per-element verdicts into the
|
|
// shared resultTensor[n] uint8 — same shape as the
|
|
// ECDSA/Ed25519/BLS cases above, so the unified Sync + ToSlice
|
|
// + bytes→bool conversion below handles MLDSA identically.
|
|
//
|
|
// Honor the Red M-1 / CRITICAL #176 propagation policy:
|
|
// ErrInvalidArgument from the C ABI is a HARD error —
|
|
// propagate it so the precompile fails closed. Silent CPU
|
|
// fallback here would let CPU-only validators accept input
|
|
// the GPU plugin already declared malformed → asymmetric
|
|
// verdicts → consensus split. Other accel errors
|
|
// (NotSupported, OutOfMemory, KernelFailed) are recoverable;
|
|
// fall back to CPU per the existing pattern.
|
|
err = sess.Lattice().MLDSAVerifyBatch(
|
|
accel.MLDSAMode65,
|
|
msgTensor.Untyped(),
|
|
sigTensor.Untyped(),
|
|
pkTensor.Untyped(),
|
|
resultTensor.Untyped(),
|
|
)
|
|
if err != nil && errors.Is(err, accel.ErrInvalidArgument) {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
return batchVerifyCPU(sigType, sigs, msgs, pubkeys)
|
|
}
|
|
|
|
// Sync GPU operations
|
|
if err := sess.Sync(); err != nil {
|
|
return batchVerifyCPU(sigType, sigs, msgs, pubkeys)
|
|
}
|
|
|
|
// Read results
|
|
resultBytes, err := resultTensor.ToSlice()
|
|
if err != nil {
|
|
return batchVerifyCPU(sigType, sigs, msgs, pubkeys)
|
|
}
|
|
|
|
results := make([]bool, n)
|
|
for i, r := range resultBytes {
|
|
results[i] = r != 0
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
func hashGPU(sess *accel.Session, hashType HashType, inputs [][]byte) ([][32]byte, error) {
|
|
n := len(inputs)
|
|
if n == 0 {
|
|
return [][32]byte{}, nil
|
|
}
|
|
|
|
// Find max input length for padding
|
|
maxLen := 0
|
|
for _, input := range inputs {
|
|
if len(input) > maxLen {
|
|
maxLen = len(input)
|
|
}
|
|
}
|
|
|
|
// Pad all inputs to same length (required for batched GPU processing)
|
|
// Use zero-padding which is safe for these hash functions
|
|
inputBytes := make([]byte, n*maxLen)
|
|
for i, input := range inputs {
|
|
copy(inputBytes[i*maxLen:i*maxLen+len(input)], input)
|
|
}
|
|
|
|
// Create input tensor
|
|
inputTensor, err := accel.NewTensorWithData[uint8](sess, []int{n, maxLen}, inputBytes)
|
|
if err != nil {
|
|
return hashCPU(hashType, inputs)
|
|
}
|
|
defer inputTensor.Close()
|
|
|
|
// Create output tensor (32 bytes per hash)
|
|
outputTensor, err := accel.NewTensor[uint8](sess, []int{n, 32})
|
|
if err != nil {
|
|
return hashCPU(hashType, inputs)
|
|
}
|
|
defer outputTensor.Close()
|
|
|
|
// Dispatch to appropriate hash kernel
|
|
cryptoOps := sess.Crypto()
|
|
switch hashType {
|
|
case HashSHA256:
|
|
err = cryptoOps.SHA256(inputTensor.Untyped(), outputTensor.Untyped())
|
|
case HashKeccak256:
|
|
err = cryptoOps.Keccak256(inputTensor.Untyped(), outputTensor.Untyped())
|
|
case HashBlake3:
|
|
// Blake3 not directly supported, use SHA256 as fallback via CPU
|
|
return hashCPU(hashType, inputs)
|
|
case HashPoseidon:
|
|
err = cryptoOps.Poseidon(inputTensor.Untyped(), outputTensor.Untyped())
|
|
default:
|
|
return hashCPU(hashType, inputs)
|
|
}
|
|
|
|
if err != nil {
|
|
return hashCPU(hashType, inputs)
|
|
}
|
|
|
|
// Sync GPU operations
|
|
if err := sess.Sync(); err != nil {
|
|
return hashCPU(hashType, inputs)
|
|
}
|
|
|
|
// Read results
|
|
outputBytes, err := outputTensor.ToSlice()
|
|
if err != nil {
|
|
return hashCPU(hashType, inputs)
|
|
}
|
|
|
|
// Convert to [][32]byte
|
|
results := make([][32]byte, n)
|
|
for i := 0; i < n; i++ {
|
|
copy(results[i][:], outputBytes[i*32:(i+1)*32])
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
func msmGPU(sess *accel.Session, curve Curve, scalars, points [][]byte) ([]byte, error) {
|
|
n := len(scalars)
|
|
if n == 0 {
|
|
return nil, ErrInvalidInput
|
|
}
|
|
|
|
const scalarSize = 32
|
|
pointSize := curve.pointSize()
|
|
switch curve {
|
|
case CurveSecp256k1, CurveBN254, CurveBLS12_381, CurveBanderwagon:
|
|
// supported
|
|
default:
|
|
return msmCPU(curve, scalars, points)
|
|
}
|
|
|
|
// Pack scalars
|
|
scalarBytes := make([]byte, n*scalarSize)
|
|
for i, s := range scalars {
|
|
if len(s) >= scalarSize {
|
|
copy(scalarBytes[i*scalarSize:(i+1)*scalarSize], s[:scalarSize])
|
|
} else {
|
|
copy(scalarBytes[i*scalarSize:i*scalarSize+len(s)], s)
|
|
}
|
|
}
|
|
|
|
// Pack points
|
|
pointBytes := make([]byte, n*pointSize)
|
|
for i, p := range points {
|
|
if len(p) >= pointSize {
|
|
copy(pointBytes[i*pointSize:(i+1)*pointSize], p[:pointSize])
|
|
} else {
|
|
copy(pointBytes[i*pointSize:i*pointSize+len(p)], p)
|
|
}
|
|
}
|
|
|
|
// Create tensors
|
|
scalarTensor, err := accel.NewTensorWithData[uint8](sess, []int{n, scalarSize}, scalarBytes)
|
|
if err != nil {
|
|
return msmCPU(curve, scalars, points)
|
|
}
|
|
defer scalarTensor.Close()
|
|
|
|
pointTensor, err := accel.NewTensorWithData[uint8](sess, []int{n, pointSize}, pointBytes)
|
|
if err != nil {
|
|
return msmCPU(curve, scalars, points)
|
|
}
|
|
defer pointTensor.Close()
|
|
|
|
// Result tensor (single point)
|
|
resultTensor, err := accel.NewTensor[uint8](sess, []int{pointSize})
|
|
if err != nil {
|
|
return msmCPU(curve, scalars, points)
|
|
}
|
|
defer resultTensor.Close()
|
|
|
|
// Dispatch MSM kernel via ZK ops
|
|
err = sess.ZK().MSM(
|
|
scalarTensor.Untyped(),
|
|
pointTensor.Untyped(),
|
|
resultTensor.Untyped(),
|
|
)
|
|
if err != nil {
|
|
return msmCPU(curve, scalars, points)
|
|
}
|
|
|
|
// Sync
|
|
if err := sess.Sync(); err != nil {
|
|
return msmCPU(curve, scalars, points)
|
|
}
|
|
|
|
// Read result
|
|
result, err := resultTensor.ToSlice()
|
|
if err != nil {
|
|
return msmCPU(curve, scalars, points)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func aggregateSigsGPU(sess *accel.Session, sigs [][]byte) ([]byte, error) {
|
|
n := len(sigs)
|
|
if n == 0 {
|
|
return nil, ErrBatchEmpty
|
|
}
|
|
|
|
// BLS signature is 96 bytes (G2 point)
|
|
const sigSize = 96
|
|
|
|
// Pack signatures
|
|
sigBytes := make([]byte, n*sigSize)
|
|
for i, sig := range sigs {
|
|
if len(sig) >= sigSize {
|
|
copy(sigBytes[i*sigSize:(i+1)*sigSize], sig[:sigSize])
|
|
} else {
|
|
copy(sigBytes[i*sigSize:i*sigSize+len(sig)], sig)
|
|
}
|
|
}
|
|
|
|
// Create tensors
|
|
sigTensor, err := accel.NewTensorWithData[uint8](sess, []int{n, sigSize}, sigBytes)
|
|
if err != nil {
|
|
return aggregateSigsCPU(sigs)
|
|
}
|
|
defer sigTensor.Close()
|
|
|
|
// Result tensor (single aggregated signature)
|
|
resultTensor, err := accel.NewTensor[uint8](sess, []int{sigSize})
|
|
if err != nil {
|
|
return aggregateSigsCPU(sigs)
|
|
}
|
|
defer resultTensor.Close()
|
|
|
|
// Dispatch BLS aggregation kernel
|
|
err = sess.Crypto().BLSAggregate(sigTensor.Untyped(), resultTensor.Untyped())
|
|
if err != nil {
|
|
return aggregateSigsCPU(sigs)
|
|
}
|
|
|
|
// Sync
|
|
if err := sess.Sync(); err != nil {
|
|
return aggregateSigsCPU(sigs)
|
|
}
|
|
|
|
// Read result
|
|
result, err := resultTensor.ToSlice()
|
|
if err != nil {
|
|
return aggregateSigsCPU(sigs)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func aggregatePksGPU(sess *accel.Session, pks [][]byte) ([]byte, error) {
|
|
n := len(pks)
|
|
if n == 0 {
|
|
return nil, ErrBatchEmpty
|
|
}
|
|
|
|
// BLS public key is 48 bytes (G1 point)
|
|
const pkSize = 48
|
|
|
|
// Pack public keys
|
|
pkBytes := make([]byte, n*pkSize)
|
|
for i, pk := range pks {
|
|
if len(pk) >= pkSize {
|
|
copy(pkBytes[i*pkSize:(i+1)*pkSize], pk[:pkSize])
|
|
} else {
|
|
copy(pkBytes[i*pkSize:i*pkSize+len(pk)], pk)
|
|
}
|
|
}
|
|
|
|
// Create tensors
|
|
pkTensor, err := accel.NewTensorWithData[uint8](sess, []int{n, pkSize}, pkBytes)
|
|
if err != nil {
|
|
return aggregatePksCPU(pks)
|
|
}
|
|
defer pkTensor.Close()
|
|
|
|
// Result tensor (single aggregated public key)
|
|
resultTensor, err := accel.NewTensor[uint8](sess, []int{pkSize})
|
|
if err != nil {
|
|
return aggregatePksCPU(pks)
|
|
}
|
|
defer resultTensor.Close()
|
|
|
|
// Use MSM for public key aggregation: sum(pk_i) where all scalars are 1
|
|
// This is equivalent to adding all G1 points
|
|
// Create all-ones scalars
|
|
ones := make([]byte, n*32)
|
|
for i := 0; i < n; i++ {
|
|
ones[i*32] = 1 // scalar = 1 in little-endian
|
|
}
|
|
|
|
onesTensor, err := accel.NewTensorWithData[uint8](sess, []int{n, 32}, ones)
|
|
if err != nil {
|
|
return aggregatePksCPU(pks)
|
|
}
|
|
defer onesTensor.Close()
|
|
|
|
// Use MSM: result = sum(1 * pk_i) = sum(pk_i)
|
|
err = sess.ZK().MSM(onesTensor.Untyped(), pkTensor.Untyped(), resultTensor.Untyped())
|
|
if err != nil {
|
|
return aggregatePksCPU(pks)
|
|
}
|
|
|
|
// Sync
|
|
if err := sess.Sync(); err != nil {
|
|
return aggregatePksCPU(pks)
|
|
}
|
|
|
|
// Read result
|
|
result, err := resultTensor.ToSlice()
|
|
if err != nil {
|
|
return aggregatePksCPU(pks)
|
|
}
|
|
|
|
return result, nil
|
|
}
|