remove gmp_generic variant too -- reduce number of cgo calls

This commit is contained in:
Kevaundray Wedderburn 2025-07-08 17:14:32 +01:00
parent 7fc1617ac1
commit e33fcb3811
5 changed files with 16 additions and 468 deletions

View file

@ -5,7 +5,6 @@ import (
"github.com/ethereum/go-ethereum/crypto/modexp/bigint" "github.com/ethereum/go-ethereum/crypto/modexp/bigint"
gmpcwrapper "github.com/ethereum/go-ethereum/crypto/modexp/gmp/cwrapper" gmpcwrapper "github.com/ethereum/go-ethereum/crypto/modexp/gmp/cwrapper"
gmpgeneric "github.com/ethereum/go-ethereum/crypto/modexp/gmp/generic"
) )
// generateWorstCase generates a byte array with all bits set to 1 // generateWorstCase generates a byte array with all bits set to 1
@ -44,7 +43,6 @@ func BenchmarkComprehensive(b *testing.B) {
fn func([]byte, []byte, []byte) ([]byte, error) fn func([]byte, []byte, []byte) ([]byte, error)
}{ }{
{"BigInt", bigint.ModExp}, {"BigInt", bigint.ModExp},
{"GMPGeneric", gmpgeneric.ModExp},
{"GMPCWrapper", gmpcwrapper.ModExp}, {"GMPCWrapper", gmpcwrapper.ModExp},
} }
@ -96,7 +94,6 @@ func BenchmarkWorstCaseOnly(b *testing.B) {
fn func([]byte, []byte, []byte) ([]byte, error) fn func([]byte, []byte, []byte) ([]byte, error)
}{ }{
{"BigInt", bigint.ModExp}, {"BigInt", bigint.ModExp},
{"GMPGeneric", gmpgeneric.ModExp},
{"GMPCWrapper", gmpcwrapper.ModExp}, {"GMPCWrapper", gmpcwrapper.ModExp},
} }
@ -142,7 +139,6 @@ func BenchmarkMemoryProfile(b *testing.B) {
fn func([]byte, []byte, []byte) ([]byte, error) fn func([]byte, []byte, []byte) ([]byte, error)
}{ }{
{"BigInt", bigint.ModExp}, {"BigInt", bigint.ModExp},
{"GMPGeneric", gmpgeneric.ModExp},
{"GMPCWrapper", gmpcwrapper.ModExp}, {"GMPCWrapper", gmpcwrapper.ModExp},
} }
@ -215,12 +211,6 @@ func BenchmarkComparison(b *testing.B) {
} }
}) })
b.Run("GMPGeneric", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = gmpgeneric.ModExp(data.base, data.exp, data.mod)
}
})
b.Run("GMPCWrapper", func(b *testing.B) { b.Run("GMPCWrapper", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_, _ = gmpcwrapper.ModExp(data.base, data.exp, data.mod) _, _ = gmpcwrapper.ModExp(data.base, data.exp, data.mod)

View file

@ -15,9 +15,7 @@ You need to have GMP development libraries installed on your system:
## Usage ## Usage
This package provides a GMP-backed implementation for modular exponentiation. The API can be expanded, however right now, the main usage is for the modexp precompile. This package provides a GMP-backed implementation for modular exponentiation using direct C calls for maximum performance. The API can be expanded, however right now, the main usage is for the modexp precompile.
There are currently two implementations: generic (using Go wrapper types) and cwrapper (direct C calls).
### Byte Array Interface (Recommended) ### Byte Array Interface (Recommended)
@ -35,25 +33,6 @@ if err != nil {
// result = 24 (2^10 mod 1000) // result = 24 (2^10 mod 1000)
``` ```
### Direct GMP Interface
```go
// Create numbers
base := gmp.NewInt()
base.SetString("123456789", 10)
exp := gmp.NewInt()
exp.SetString("987654321", 10)
mod := gmp.NewInt()
mod.SetString("1000000007", 10)
// Compute base^exp mod mod
result := gmp.NewInt()
result.ExpMod(base, exp, mod)
fmt.Printf("Result: %s\n", result)
```
## Testing ## Testing

View file

@ -6,8 +6,8 @@ import (
"testing" "testing"
) )
// TestWrapperVsExisting compares the wrapper with existing GMP bindings // TestWrapperVsBigInt compares the wrapper with Go's math/big
func TestWrapperVsExisting(t *testing.T) { func TestWrapperVsBigInt(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
base string base string
@ -60,22 +60,16 @@ func TestWrapperVsExisting(t *testing.T) {
t.Fatalf("Wrapper error: %v", err) t.Fatalf("Wrapper error: %v", err)
} }
// Test with existing GMP bindings // Test with math/big
gmpBase := NewInt() bigResult := new(big.Int)
gmpBase.SetString(tt.base, 10) bigResult.Exp(baseBig, expBig, modBig)
gmpExp := NewInt()
gmpExp.SetString(tt.exp, 10)
gmpMod := NewInt()
gmpMod.SetString(tt.mod, 10)
gmpResult := NewInt()
gmpResult.ExpMod(gmpBase, gmpExp, gmpMod)
existingResult := gmpResult.Bytes() expectedResult := bigResult.Bytes()
// Compare results // Compare results
if !bytes.Equal(wrapperResult, existingResult) { if !bytes.Equal(wrapperResult, expectedResult) {
t.Errorf("Results differ:\nWrapper: %x\nExisting: %x", t.Errorf("Results differ:\nWrapper: %x\nExpected: %x",
wrapperResult, existingResult) wrapperResult, expectedResult)
} }
}) })
} }
@ -100,7 +94,7 @@ func BenchmarkModExp(b *testing.B) {
} }
} }
func BenchmarkModExpExisting(b *testing.B) { func BenchmarkModExpBigInt(b *testing.B) {
baseBytes := make([]byte, 60) baseBytes := make([]byte, 60)
expBytes := make([]byte, 60) expBytes := make([]byte, 60)
modBytes := make([]byte, 60) modBytes := make([]byte, 60)
@ -112,17 +106,17 @@ func BenchmarkModExpExisting(b *testing.B) {
} }
modBytes[59] |= 0x01 modBytes[59] |= 0x01
base := NewInt() base := new(big.Int)
base.SetBytes(baseBytes) base.SetBytes(baseBytes)
exp := NewInt() exp := new(big.Int)
exp.SetBytes(expBytes) exp.SetBytes(expBytes)
mod := NewInt() mod := new(big.Int)
mod.SetBytes(modBytes) mod.SetBytes(modBytes)
result := NewInt() result := new(big.Int)
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
result.ExpMod(base, exp, mod) result.Exp(base, exp, mod)
_ = result.Bytes() _ = result.Bytes()
} }
} }

View file

@ -1,158 +0,0 @@
package gmp
// Uses system-installed GMP library
// #cgo LDFLAGS: -lgmp
// #include <gmp.h>
// #include <stdlib.h>
//
// static inline int mpz_sgn_wrapper(const mpz_t op) {
// return mpz_sgn(op);
// }
import "C"
import (
"runtime"
"unsafe"
)
// Int represents a GMP integer
type Int struct {
mpz C.mpz_t
}
// NewInt creates a new GMP integer
func NewInt() *Int {
z := &Int{}
C.mpz_init(&z.mpz[0])
runtime.SetFinalizer(z, (*Int).destroy)
return z
}
// destroy cleans up the GMP integer
func (z *Int) destroy() {
C.mpz_clear(&z.mpz[0])
}
// SetString sets the integer from a string in the given base
func (z *Int) SetString(s string, base int) (*Int, bool) {
cs := C.CString(s)
defer C.free(unsafe.Pointer(cs))
if C.mpz_set_str(&z.mpz[0], cs, C.int(base)) != 0 {
return nil, false
}
return z, true
}
// ExpMod computes z = base^exp mod mod (modular exponentiation)
func (z *Int) ExpMod(base, exp, mod *Int) *Int {
C.mpz_powm(&z.mpz[0], &base.mpz[0], &exp.mpz[0], &mod.mpz[0])
return z
}
// SetBytes sets z to the value of buf interpreted as a big-endian unsigned integer
func (z *Int) SetBytes(buf []byte) *Int {
if len(buf) == 0 {
C.mpz_set_ui(&z.mpz[0], 0)
return z
}
// Use GMP's import function for efficiency
C.mpz_import(&z.mpz[0], C.size_t(len(buf)), 1, 1, 0, 0, unsafe.Pointer(&buf[0]))
return z
}
// Bytes returns the absolute value of z as a big-endian byte slice
func (z *Int) Bytes() []byte {
if z == nil {
return nil
}
// Special case: zero returns empty slice (matching big.Int)
if C.mpz_sgn_wrapper(&z.mpz[0]) == 0 {
return []byte{}
}
// Get the number of bytes needed
size := (C.mpz_sizeinbase(&z.mpz[0], 2) + 7) / 8
// Allocate buffer
buf := make([]byte, size)
// Export to bytes
var count C.size_t
C.mpz_export(unsafe.Pointer(&buf[0]), &count, 1, 1, 0, 0, &z.mpz[0])
// Trim if needed (shouldn't happen but just in case)
if int(count) < len(buf) {
buf = buf[:count]
}
return buf
}
// BitLen returns the number of bits required to represent z
func (z *Int) BitLen() int {
if z == nil {
return 0
}
return int(C.mpz_sizeinbase(&z.mpz[0], 2))
}
// Mod sets z to x mod y and returns z
func (z *Int) Mod(x, y *Int) *Int {
C.mpz_mod(&z.mpz[0], &x.mpz[0], &y.mpz[0])
return z
}
// SetUint64 sets z to the value of x
func (z *Int) SetUint64(x uint64) *Int {
C.mpz_set_ui(&z.mpz[0], C.ulong(x))
return z
}
// ModExp performs modular exponentiation on byte arrays using GMP
// result = base^exp mod mod
// This function matches the behavior of the EVM modexp precompile
func ModExp(base, exp, mod []byte) ([]byte, error) {
// Handle empty modulus - return empty result (EVM behavior)
if len(mod) == 0 {
return []byte{}, nil
}
// Check for zero modulus
allZero := true
for _, b := range mod {
if b != 0 {
allZero = false
break
}
}
if allZero {
return []byte{}, nil
}
// Create GMP integers
baseInt := NewInt()
expInt := NewInt()
modInt := NewInt()
resultInt := NewInt()
// Set values
baseInt.SetBytes(base)
expInt.SetBytes(exp)
modInt.SetBytes(mod)
// Special case: base has bit length 1 (base == 1)
if baseInt.BitLen() == 1 {
// Just return base % mod
resultInt.Mod(baseInt, modInt)
} else {
// Normal case: perform modular exponentiation
resultInt.ExpMod(baseInt, expInt, modInt)
}
// Get result bytes
return resultInt.Bytes(), nil
}

View file

@ -1,257 +0,0 @@
package gmp
import (
"bytes"
"math/big"
"testing"
)
// TestModExpAgainstBigInt tests our GMP implementation against Go's math/big
func TestModExpAgainstBigInt(t *testing.T) {
tests := []struct {
name string
base string
exp string
mod string
}{
{
name: "small_numbers",
base: "2",
exp: "10",
mod: "1000",
},
{
name: "medium_numbers",
base: "123",
exp: "456",
mod: "789",
},
{
name: "large_base",
base: "123456789012345678901234567890",
exp: "2",
mod: "1000000007",
},
{
name: "large_exponent",
base: "2",
exp: "123456789012345678901234567890",
mod: "1000000007",
},
{
name: "all_large",
base: "123456789012345678901234567890",
exp: "987654321098765432109876543210",
mod: "111111111111111111111111111111",
},
{
name: "prime_modulus",
base: "12345",
exp: "67890",
mod: "2147483647", // 2^31 - 1 (Mersenne prime)
},
{
name: "fermat_little_theorem",
base: "3",
exp: "16",
mod: "17",
},
{
name: "carmichael_number",
base: "2",
exp: "560",
mod: "561", // First Carmichael number
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// GMP calculation
gmpBase := NewInt()
gmpBase.SetString(tt.base, 10)
gmpExp := NewInt()
gmpExp.SetString(tt.exp, 10)
gmpMod := NewInt()
gmpMod.SetString(tt.mod, 10)
gmpResult := NewInt()
gmpResult.ExpMod(gmpBase, gmpExp, gmpMod)
// math/big calculation
bigBase := new(big.Int)
bigBase.SetString(tt.base, 10)
bigExp := new(big.Int)
bigExp.SetString(tt.exp, 10)
bigMod := new(big.Int)
bigMod.SetString(tt.mod, 10)
bigResult := new(big.Int)
bigResult.Exp(bigBase, bigExp, bigMod)
// Compare results
if gmpResult.String() != bigResult.String() {
t.Errorf("ModExp mismatch:\n GMP: %s\n big: %s",
gmpResult.String(), bigResult.String())
}
})
}
}
// TestSetBytesAgainstBigInt tests byte conversion against math/big
func TestSetBytesAgainstBigInt(t *testing.T) {
tests := []struct {
name string
bytes []byte
}{
{"empty", []byte{}},
{"single_byte", []byte{0x42}},
{"two_bytes", []byte{0x12, 0x34}},
{"four_bytes", []byte{0xDE, 0xAD, 0xBE, 0xEF}},
{"eight_bytes", []byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF}},
{"all_zeros", []byte{0x00, 0x00, 0x00, 0x00}},
{"leading_zeros", []byte{0x00, 0x00, 0x12, 0x34}},
{"all_ones", []byte{0xFF, 0xFF, 0xFF, 0xFF}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// GMP SetBytes
gmpInt := NewInt()
gmpInt.SetBytes(tt.bytes)
// math/big SetBytes
bigInt := new(big.Int)
bigInt.SetBytes(tt.bytes)
// Compare string representations
if gmpInt.String() != bigInt.String() {
t.Errorf("SetBytes mismatch for %x:\n GMP: %s\n big: %s",
tt.bytes, gmpInt.String(), bigInt.String())
}
// Test round trip
gmpBytes := gmpInt.Bytes()
bigBytes := bigInt.Bytes()
// Handle empty/zero cases
// Note: both empty input and all-zeros should produce empty output
if len(bigBytes) == 0 {
if len(gmpBytes) != 0 {
t.Errorf("Expected empty bytes, got %x", gmpBytes)
}
return
}
// Compare bytes (handling leading zeros)
if !bytesEqual(gmpBytes, bigBytes) {
t.Errorf("Bytes() mismatch:\n GMP: %x\n big: %x",
gmpBytes, bigBytes)
}
})
}
}
// TestModExpByteArrays tests modular exponentiation with byte arrays
func TestModExpByteArrays(t *testing.T) {
// Test case: RSA-like encryption with byte arrays
msgBytes := []byte("Hello!")
// Convert to numbers
gmpMsg := NewInt()
gmpMsg.SetBytes(msgBytes)
bigMsg := new(big.Int)
bigMsg.SetBytes(msgBytes)
// Public exponent (common RSA value)
e := "65537"
// Small modulus for testing
n := "12345678901234567890"
// GMP calculation
gmpE := NewInt()
gmpE.SetString(e, 10)
gmpN := NewInt()
gmpN.SetString(n, 10)
gmpResult := NewInt()
gmpResult.ExpMod(gmpMsg, gmpE, gmpN)
// math/big calculation
bigE := new(big.Int)
bigE.SetString(e, 10)
bigN := new(big.Int)
bigN.SetString(n, 10)
bigResult := new(big.Int)
bigResult.Exp(bigMsg, bigE, bigN)
// Compare
if gmpResult.String() != bigResult.String() {
t.Errorf("Byte array ModExp mismatch:\n GMP: %s\n big: %s",
gmpResult.String(), bigResult.String())
}
// Verify we can convert back to bytes
resultBytes := gmpResult.Bytes()
if len(resultBytes) == 0 {
t.Error("Result bytes should not be empty")
}
}
// Benchmark against math/big
func BenchmarkModExpGMP(b *testing.B) {
base := NewInt()
base.SetString("123456789012345678901234567890123456789012345678901234567890", 10)
exp := NewInt()
exp.SetString("987654321098765432109876543210987654321098765432109876543210", 10)
mod := NewInt()
mod.SetString("111111111111111111111111111111111111111111111111111111111111", 10)
result := NewInt()
b.ResetTimer()
for i := 0; i < b.N; i++ {
result.ExpMod(base, exp, mod)
}
}
func BenchmarkModExpBigInt(b *testing.B) {
base := new(big.Int)
base.SetString("123456789012345678901234567890123456789012345678901234567890", 10)
exp := new(big.Int)
exp.SetString("987654321098765432109876543210987654321098765432109876543210", 10)
mod := new(big.Int)
mod.SetString("111111111111111111111111111111111111111111111111111111111111", 10)
result := new(big.Int)
b.ResetTimer()
for i := 0; i < b.N; i++ {
result.Exp(base, exp, mod)
}
}
// Helper function
func bytesEqual(a, b []byte) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}