add modexp interface

This commit is contained in:
Kevaundray Wedderburn 2025-07-08 16:46:05 +01:00
parent e71487b033
commit 68181011ee
13 changed files with 1743 additions and 0 deletions

View file

@ -0,0 +1,241 @@
package modexp_test
import (
"testing"
"github.com/ethereum/go-ethereum/crypto/modexp/bigint"
gmpcwrapper "github.com/ethereum/go-ethereum/crypto/modexp/gmp/cwrapper"
gmpgeneric "github.com/ethereum/go-ethereum/crypto/modexp/gmp/generic"
gmppool "github.com/ethereum/go-ethereum/crypto/modexp/gmp/pool"
)
// generateWorstCase generates a byte array with all bits set to 1
func generateWorstCase(size int) []byte {
result := make([]byte, size)
for i := range result {
result[i] = 0xFF
}
return result
}
// BenchmarkComprehensive tests with specific bit sizes and worst-case scenarios
func BenchmarkComprehensive(b *testing.B) {
// Test cases with specific bit sizes
testCases := []struct {
name string
bits int
bytes int
}{
{"1bit", 1, 1}, // 1 bit
{"8bit", 8, 1}, // 1 byte
{"16bit", 16, 2}, // 2 bytes
{"32bit", 32, 4}, // 4 bytes
{"64bit", 64, 8}, // 8 bytes
{"128bit", 128, 16}, // 16 bytes
{"256bit", 256, 32}, // 32 bytes
{"512bit", 512, 64}, // 64 bytes
{"1024bit", 1024, 128}, // 128 bytes
{"2048bit", 2048, 256}, // 256 bytes
{"4096bit", 4096, 512}, // 512 bytes
{"8192bit", 8192, 1024}, // 1024 bytes
}
implementations := []struct {
name string
fn func([]byte, []byte, []byte) ([]byte, error)
}{
{"BigInt", bigint.ModExp},
{"GMPGeneric", gmpgeneric.ModExp},
{"GMPCWrapper", gmpcwrapper.ModExp},
{"GMPPooled", gmppool.ModExp},
}
for _, tc := range testCases {
// Generate worst-case inputs (all bits set)
base := generateWorstCase(tc.bytes)
exp := generateWorstCase(tc.bytes)
mod := generateWorstCase(tc.bytes)
// For 1-bit case, use specific values
if tc.bits == 1 {
base = []byte{0x01}
exp = []byte{0x01}
mod = []byte{0x03} // Must be > 1 for valid modulo
}
for _, impl := range implementations {
b.Run(tc.name+"/"+impl.name, func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = impl.fn(base, exp, mod)
}
})
}
}
}
// BenchmarkWorstCaseOnly focuses on worst-case scenarios with maximum values
func BenchmarkWorstCaseOnly(b *testing.B) {
sizes := []struct {
name string
bytes int
}{
{"8bit", 1},
{"16bit", 2},
{"32bit", 4},
{"64bit", 8},
{"128bit", 16},
{"256bit", 32},
{"512bit", 64},
{"1024bit", 128},
{"2048bit", 256},
{"4096bit", 512},
{"8192bit", 1024},
}
implementations := []struct {
name string
fn func([]byte, []byte, []byte) ([]byte, error)
}{
{"BigInt", bigint.ModExp},
{"GMPGeneric", gmpgeneric.ModExp},
{"GMPCWrapper", gmpcwrapper.ModExp},
{"GMPPooled", gmppool.ModExp},
}
b.Run("AllFF", func(b *testing.B) {
for _, size := range sizes {
base := generateWorstCase(size.bytes)
exp := generateWorstCase(size.bytes)
mod := generateWorstCase(size.bytes)
for _, impl := range implementations {
b.Run(size.name+"/"+impl.name, func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = impl.fn(base, exp, mod)
}
})
}
}
})
}
// BenchmarkMemoryProfile shows memory usage for each size
func BenchmarkMemoryProfile(b *testing.B) {
sizes := []struct {
name string
bytes int
}{
{"1B", 1},
{"2B", 2},
{"4B", 4},
{"8B", 8},
{"16B", 16},
{"32B", 32},
{"64B", 64},
{"128B", 128},
{"256B", 256},
{"512B", 512},
{"1024B", 1024},
}
implementations := []struct {
name string
fn func([]byte, []byte, []byte) ([]byte, error)
}{
{"BigInt", bigint.ModExp},
{"GMPGeneric", gmpgeneric.ModExp},
{"GMPCWrapper", gmpcwrapper.ModExp},
{"GMPPooled", gmppool.ModExp},
}
for _, size := range sizes {
base := generateWorstCase(size.bytes)
exp := generateWorstCase(size.bytes)
mod := generateWorstCase(size.bytes)
for _, impl := range implementations {
b.Run(size.name+"/"+impl.name, func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
result, _ := impl.fn(base, exp, mod)
_ = result
}
})
}
}
}
// BenchmarkComparison provides a direct comparison table
func BenchmarkComparison(b *testing.B) {
// Prepare test data
testData := make(map[string]struct{ base, exp, mod []byte })
sizes := []int{1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024}
for _, size := range sizes {
name := ""
switch {
case size == 1:
name = "8bit"
case size == 2:
name = "16bit"
case size == 4:
name = "32bit"
case size == 8:
name = "64bit"
case size == 16:
name = "128bit"
case size == 32:
name = "256bit"
case size == 64:
name = "512bit"
case size == 128:
name = "1024bit"
case size == 256:
name = "2048bit"
case size == 512:
name = "4096bit"
case size == 1024:
name = "8192bit"
}
testData[name] = struct{ base, exp, mod []byte }{
base: generateWorstCase(size),
exp: generateWorstCase(size),
mod: generateWorstCase(size),
}
}
// Run benchmarks in a structured way
for _, sizeName := range []string{"8bit", "16bit", "32bit", "64bit", "128bit", "256bit", "512bit", "1024bit", "2048bit", "4096bit", "8192bit"} {
data := testData[sizeName]
b.Run(sizeName, func(b *testing.B) {
b.Run("BigInt", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = bigint.ModExp(data.base, data.exp, data.mod)
}
})
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) {
for i := 0; i < b.N; i++ {
_, _ = gmpcwrapper.ModExp(data.base, data.exp, data.mod)
}
})
b.Run("GMPPooled", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = gmppool.ModExp(data.base, data.exp, data.mod)
}
})
})
}
}

View file

@ -0,0 +1,133 @@
package bigint_test
import (
"crypto/rand"
"testing"
"github.com/ethereum/go-ethereum/crypto/modexp/bigint"
)
// generateBytes generates random bytes of given length
func generateBytes(b *testing.B, length int) []byte {
bytes := make([]byte, length)
_, err := rand.Read(bytes)
if err != nil {
b.Fatal(err)
}
// Ensure odd modulus for better performance
if length > 0 {
bytes[length-1] |= 0x01
}
return bytes
}
// BenchmarkModExp runs benchmarks for different input sizes
func BenchmarkModExp(b *testing.B) {
sizes := []struct {
name string
size int
}{
{"32", 32},
{"64", 64},
{"128", 128},
{"256", 256},
{"512", 512},
{"1024", 1024},
{"2048", 2048},
}
for _, size := range sizes {
b.Run(size.name, func(b *testing.B) {
base := generateBytes(b, size.size)
exp := generateBytes(b, size.size)
mod := generateBytes(b, size.size)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = bigint.ModExp(base, exp, mod)
}
})
}
}
// BenchmarkModExpParallel tests parallel performance
func BenchmarkModExpParallel(b *testing.B) {
sizes := []int{128, 256, 512}
for _, size := range sizes {
base := generateBytes(b, size)
exp := generateBytes(b, size)
mod := generateBytes(b, size)
b.Run(string(rune(size))+"bytes", func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, _ = bigint.ModExp(base, exp, mod)
}
})
})
}
}
// BenchmarkModExpEdgeCases tests special cases
func BenchmarkModExpEdgeCases(b *testing.B) {
cases := []struct {
name string
base []byte
exp []byte
mod []byte
}{
{
name: "base_is_one",
base: []byte{0x01},
exp: []byte{0xFF, 0xFF, 0xFF, 0xFF},
mod: []byte{0xFF, 0xFF, 0xFF, 0xFF},
},
{
name: "zero_exponent",
base: []byte{0xFF, 0xFF, 0xFF, 0xFF},
exp: []byte{0x00},
mod: []byte{0xFF, 0xFF, 0xFF, 0xFF},
},
{
name: "small_exponent_rsa",
base: generateBytes(b, 256),
exp: []byte{0x01, 0x00, 0x01}, // 65537
mod: generateBytes(b, 256),
},
{
name: "empty_base",
base: []byte{},
exp: []byte{0xFF},
mod: []byte{0xFF, 0xFF},
},
}
for _, tc := range cases {
b.Run(tc.name, func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = bigint.ModExp(tc.base, tc.exp, tc.mod)
}
})
}
}
// BenchmarkModExpAllocs measures allocations
func BenchmarkModExpAllocs(b *testing.B) {
sizes := []int{32, 128, 256, 1024}
for _, size := range sizes {
base := generateBytes(b, size)
exp := generateBytes(b, size)
mod := generateBytes(b, size)
b.Run(string(rune(size))+"bytes", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = bigint.ModExp(base, exp, mod)
}
})
}
}

View file

@ -0,0 +1,31 @@
package bigint
import (
"math/big"
)
// ModExp performs modular exponentiation on byte arrays using math/big
// result = base^exp mod mod
// This function matches the behavior of the EVM modexp precompile
func ModExp(base, exp, mod []byte) ([]byte, error) {
// Create big.Int values
baseBig := new(big.Int).SetBytes(base)
expBig := new(big.Int).SetBytes(exp)
modBig := new(big.Int).SetBytes(mod)
var v []byte
switch {
case modBig.BitLen() == 0:
// Modulo 0 is undefined, return zero (matching EVM behavior)
return []byte{}, nil
case baseBig.BitLen() == 1: // a bit length of 1 means it's 1 (or -1).
// If base == 1, then we can just return base % mod (if mod >= 1, which it is)
v = baseBig.Mod(baseBig, modBig).Bytes()
default:
v = baseBig.Exp(baseBig, expBig, modBig).Bytes()
}
// Return the result bytes without padding
// The caller is responsible for padding if needed
return v, nil
}

View file

@ -0,0 +1,135 @@
package bigint
import (
"bytes"
"testing"
)
func TestModExp(t *testing.T) {
tests := []struct {
name string
base []byte
exp []byte
mod []byte
want []byte
wantErr bool
}{
{
name: "simple_2^10_mod_1000",
base: []byte{0x02}, // 2
exp: []byte{0x0A}, // 10
mod: []byte{0x03, 0xE8}, // 1000
want: []byte{0x18}, // 24 (1024 mod 1000)
},
{
name: "zero_base",
base: []byte{0x00},
exp: []byte{0x05},
mod: []byte{0x07},
want: []byte{0x00},
},
{
name: "zero_exponent",
base: []byte{0x05},
exp: []byte{0x00},
mod: []byte{0x07},
want: []byte{0x01}, // Any number to the power of 0 is 1
},
{
name: "large_numbers",
base: []byte{0xFF, 0xFF},
exp: []byte{0x02},
mod: []byte{0x01, 0x00, 0x00},
want: []byte{0xFE, 0x01}, // (65535^2) mod 65536 = 65025
},
{
name: "empty_modulus",
base: []byte{0x02},
exp: []byte{0x03},
mod: []byte{},
want: []byte{},
wantErr: false,
},
{
name: "zero_modulus",
base: []byte{0x02},
exp: []byte{0x03},
mod: []byte{0x00, 0x00},
want: []byte{},
wantErr: false,
},
{
name: "empty_base_and_exp",
base: []byte{},
exp: []byte{},
mod: []byte{0x07},
want: []byte{0x01}, // 0^0 = 1 in Go's big.Int
},
{
name: "base_equals_one",
base: []byte{0x01},
exp: []byte{0xFF, 0xFF, 0xFF, 0xFF}, // large exponent
mod: []byte{0x07},
want: []byte{0x01}, // 1^anything mod 7 = 1
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ModExp(tt.base, tt.exp, tt.mod)
if (err != nil) != tt.wantErr {
t.Errorf("ModExp() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && !bytes.Equal(got, tt.want) {
t.Errorf("ModExp() = %x, want %x", got, tt.want)
}
})
}
}
// TestLargeModExp tests with very large numbers
func TestLargeModExp(t *testing.T) {
// Test with 2048-bit numbers
base := make([]byte, 256)
exp := make([]byte, 256)
mod := make([]byte, 256)
// Fill with test data
for i := range base {
base[i] = byte(i)
exp[i] = byte(255 - i)
mod[i] = 0xFF
}
mod[0] = 0x7F // Make sure modulus is not too large
result, err := ModExp(base, exp, mod)
if err != nil {
t.Fatalf("ModExp() with large numbers failed: %v", err)
}
if len(result) == 0 {
t.Error("Expected non-empty result for large numbers")
}
}
// BenchmarkModExp benchmarks the byte-oriented interface
func BenchmarkModExp(b *testing.B) {
base := make([]byte, 32)
exp := make([]byte, 32)
mod := make([]byte, 32)
for i := range base {
base[i] = byte(i * 17)
exp[i] = byte(i * 31)
mod[i] = byte(255 - i)
}
mod[31] |= 0x01 // Ensure odd modulus
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = ModExp(base, exp, mod)
}
}

View file

@ -0,0 +1,81 @@
# GMP Wrapper for go-ethereum
A CGO wrapper for GMP's modular exponentiation functionality.
## Prerequisites
You need to have GMP development libraries installed on your system:
- **Ubuntu/Debian**: `sudo apt-get install libgmp-dev`
- **Fedora/RHEL**: `sudo dnf install gmp-devel`
- **macOS**: `brew install gmp`
- **Windows**: See [GMP Windows builds](https://gmplib.org/)
> TODO: There is an alternative branch, where we compile from source however this is slightly messier (though it allows us to pin to a specific commit like we do for libsecp256k1)
## 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.
There are currently two APIs. This is because its unclear if a direct API is better or using a syncPool with a more flexible API is better.
### Byte Array Interface (Recommended)
```go
import "github.com/ethereum/go-ethereum/crypto/modexp/gmp"
base := []byte{0x02} // 2
exp := []byte{0x0A} // 10
mod := []byte{0x03, 0xE8} // 1000
result, err := gmp.ModExp(base, exp, mod)
if err != nil {
log.Fatal(err)
}
// 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)
```
### Pooled Interface (High Performance)
```go
// Create a pool once
pool := gmp.NewIntPool()
// Use it for many operations
for i := 0; i < 1000000; i++ {
result := gmp.ExpModPooled(pool, base, exp, mod)
// Process result...
}
```
## Testing
Run tests from the go-ethereum root directory:
```bash
go test ./crypto/modexp/gmp/...
```
## License
TODO -- whatever go-ethereum does.

View file

@ -0,0 +1,147 @@
package gmp
// #cgo LDFLAGS: -lgmp
// #include <gmp.h>
// #include <stdlib.h>
// #include <string.h>
// #include <stdint.h>
//
// static int modexp_bytes(
// const uint8_t* base, size_t base_len,
// const uint8_t* exp, size_t exp_len,
// const uint8_t* mod, size_t mod_len,
// uint8_t* result, size_t* result_len)
// {
// mpz_t base_mpz, exp_mpz, mod_mpz, result_mpz;
//
// // Check for NULL pointers
// if (!base || !exp || !mod || !result || !result_len) {
// return -1;
// }
//
// // Initialize GMP integers
// mpz_init(base_mpz);
// mpz_init(exp_mpz);
// mpz_init(mod_mpz);
// mpz_init(result_mpz);
//
// // Import big-endian byte arrays into GMP integers
// // Handle empty arrays specially - GMP treats NULL with size 0 as 0
// if (base_len > 0) {
// mpz_import(base_mpz, base_len, 1, 1, 0, 0, base);
// }
// if (exp_len > 0) {
// mpz_import(exp_mpz, exp_len, 1, 1, 0, 0, exp);
// }
// if (mod_len > 0) {
// mpz_import(mod_mpz, mod_len, 1, 1, 0, 0, mod);
// }
//
// // Special case: modulus is zero - return empty result (EVM behavior)
// if (mpz_cmp_ui(mod_mpz, 0) == 0) {
// *result_len = 0;
// mpz_clear(base_mpz);
// mpz_clear(exp_mpz);
// mpz_clear(mod_mpz);
// mpz_clear(result_mpz);
// return 0;
// }
//
// // Special case: base has bit length 1 (base == 1)
// // Just return base % mod
// if (mpz_sizeinbase(base_mpz, 2) == 1) {
// mpz_mod(result_mpz, base_mpz, mod_mpz);
// } else {
// // Normal case: perform modular exponentiation
// mpz_powm(result_mpz, base_mpz, exp_mpz, mod_mpz);
// }
//
// // Get size needed for result
// size_t needed = (mpz_sizeinbase(result_mpz, 2) + 7) / 8;
// if (needed == 0) needed = 1; // For zero result
//
// // Check if result buffer is large enough
// if (*result_len < needed) {
// mpz_clear(base_mpz);
// mpz_clear(exp_mpz);
// mpz_clear(mod_mpz);
// mpz_clear(result_mpz);
// return -2;
// }
//
// // Export result to big-endian byte array
// size_t count;
// mpz_export(result, &count, 1, 1, 0, 0, result_mpz);
// *result_len = count;
//
// // Handle zero result specially
// if (count == 0) {
// result[0] = 0;
// *result_len = 1;
// }
//
// // Clean up
// mpz_clear(base_mpz);
// mpz_clear(exp_mpz);
// mpz_clear(mod_mpz);
// mpz_clear(result_mpz);
//
// return 0;
// }
import "C"
import (
"errors"
"unsafe"
)
// ModExp performs modular exponentiation using the C implementation directly
// This is a lower-level function that bypasses the Go wrapper types
func ModExp(base, exp, mod []byte) ([]byte, error) {
// Handle empty modulus - return empty result (EVM behavior)
if len(mod) == 0 {
return []byte{}, nil
}
// Allocate result buffer (size of modulus is the max possible result)
result := make([]byte, len(mod))
resultLen := C.size_t(len(result))
// Handle empty slices - pass a dummy non-nil pointer with length 0
dummy := C.uint8_t(0)
var basePtr, expPtr, modPtr *C.uint8_t = &dummy, &dummy, &dummy
if len(base) > 0 {
basePtr = (*C.uint8_t)(unsafe.Pointer(&base[0]))
}
if len(exp) > 0 {
expPtr = (*C.uint8_t)(unsafe.Pointer(&exp[0]))
}
if len(mod) > 0 {
modPtr = (*C.uint8_t)(unsafe.Pointer(&mod[0]))
}
// Call C function
ret := C.modexp_bytes(
basePtr, C.size_t(len(base)),
expPtr, C.size_t(len(exp)),
modPtr, C.size_t(len(mod)),
(*C.uint8_t)(unsafe.Pointer(&result[0])), &resultLen,
)
// Check for errors
switch ret {
case 0:
// Success - trim result to actual size
if resultLen == 0 {
return []byte{}, nil
}
return result[:resultLen], nil
case -1:
return nil, errors.New("invalid parameter")
case -2:
return nil, errors.New("result buffer too small")
default:
return nil, errors.New("unknown error")
}
}

View file

@ -0,0 +1,128 @@
package gmp
import (
"bytes"
"math/big"
"testing"
)
// TestWrapperVsExisting compares the wrapper with existing GMP bindings
func TestWrapperVsExisting(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: "123456789",
exp: "987654321",
mod: "1000000007",
},
{
name: "large_numbers",
base: "123456789012345678901234567890",
exp: "987654321098765432109876543210",
mod: "111111111111111111111111111111",
},
{
name: "zero_exponent",
base: "12345",
exp: "0",
mod: "67890",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Parse numbers
baseBig := new(big.Int)
baseBig.SetString(tt.base, 10)
expBig := new(big.Int)
expBig.SetString(tt.exp, 10)
modBig := new(big.Int)
modBig.SetString(tt.mod, 10)
// Test with wrapper
baseBytes := baseBig.Bytes()
expBytes := expBig.Bytes()
modBytes := modBig.Bytes()
wrapperResult, err := ModExp(baseBytes, expBytes, modBytes)
if err != nil {
t.Fatalf("Wrapper error: %v", err)
}
// Test with existing GMP bindings
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)
existingResult := gmpResult.Bytes()
// Compare results
if !bytes.Equal(wrapperResult, existingResult) {
t.Errorf("Results differ:\nWrapper: %x\nExisting: %x",
wrapperResult, existingResult)
}
})
}
}
// BenchmarkWrapperVsExisting compares performance
func BenchmarkModExp(b *testing.B) {
base := make([]byte, 60)
exp := make([]byte, 60)
mod := make([]byte, 60)
for i := range base {
base[i] = byte(i * 17)
exp[i] = byte(i * 31)
mod[i] = byte(255 - i)
}
mod[59] |= 0x01
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = ModExp(base, exp, mod)
}
}
func BenchmarkModExpExisting(b *testing.B) {
baseBytes := make([]byte, 60)
expBytes := make([]byte, 60)
modBytes := make([]byte, 60)
for i := range baseBytes {
baseBytes[i] = byte(i * 17)
expBytes[i] = byte(i * 31)
modBytes[i] = byte(255 - i)
}
modBytes[59] |= 0x01
base := NewInt()
base.SetBytes(baseBytes)
exp := NewInt()
exp.SetBytes(expBytes)
mod := NewInt()
mod.SetBytes(modBytes)
result := NewInt()
b.ResetTimer()
for i := 0; i < b.N; i++ {
result.ExpMod(base, exp, mod)
_ = result.Bytes()
}
}

View file

@ -0,0 +1,170 @@
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
}
// String returns the decimal representation of z
func (z *Int) String() string {
if z == nil {
return "<nil>"
}
// Get string from GMP
cs := C.mpz_get_str(nil, 10, &z.mpz[0])
defer C.free(unsafe.Pointer(cs))
return C.GoString(cs)
}
// 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

@ -0,0 +1,257 @@
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
}

View file

@ -0,0 +1,96 @@
package gmp
// #include <gmp.h>
import "C"
import (
"sync"
generic "github.com/ethereum/go-ethereum/crypto/modexp/gmp/generic"
)
// IntPool provides a pool of reusable GMP Int objects to reduce allocations
type IntPool struct {
pool sync.Pool
}
// NewIntPool creates a new pool for GMP Int objects
func NewIntPool() *IntPool {
return &IntPool{
pool: sync.Pool{
New: func() interface{} {
return generic.NewInt()
},
},
}
}
// Get retrieves an Int from the pool
func (p *IntPool) Get() *generic.Int {
return p.pool.Get().(*generic.Int)
}
// Put returns an Int to the pool after clearing it
func (p *IntPool) Put(i *generic.Int) {
// Clear the Int to avoid keeping large numbers in memory
i.SetUint64(0)
p.pool.Put(i)
}
// ExpModPooled performs modular exponentiation using pooled Int objects
// This is useful for high-throughput scenarios where you want to minimize allocations
// This function matches the behavior of the EVM modexp precompile
func ExpModPooled(pool *IntPool, base, exp, mod []byte) []byte {
// Handle empty modulus - return empty result (EVM behavior)
if len(mod) == 0 {
return []byte{}
}
// Get Ints from pool
baseInt := pool.Get()
expInt := pool.Get()
modInt := pool.Get()
resultInt := pool.Get()
// Ensure we return Ints to pool when done
defer func() {
pool.Put(baseInt)
pool.Put(expInt)
pool.Put(modInt)
pool.Put(resultInt)
}()
// Set values
baseInt.SetBytes(base)
expInt.SetBytes(exp)
modInt.SetBytes(mod)
// Check for zero modulus
if modInt.BitLen() == 0 {
return []byte{}
}
// 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 (this allocates, but much less than creating new Ints)
return resultInt.Bytes()
}
// ModExp performs modular exponentiation using a new pool for each operation
// For better performance, create a pool once and reuse it with ExpModPooled
func ModExp(base, exp, mod []byte) ([]byte, error) {
pool := NewIntPool()
result := ExpModPooled(pool, base, exp, mod)
return result, nil
}
// PreallocatedExpMod performs modular exponentiation with pre-allocated Int objects
// This gives the caller full control over object lifecycle
func PreallocatedExpMod(result, base, exp, mod *generic.Int) {
result.ExpMod(base, exp, mod)
}

View file

@ -0,0 +1,236 @@
package gmp
import (
"math/big"
"sync"
"testing"
)
// TestIntPool tests basic pool functionality
func TestIntPool(t *testing.T) {
pool := NewIntPool()
// Get an Int from pool
i1 := pool.Get()
i1.SetString("12345", 10)
// Return it to pool
pool.Put(i1)
// Get another Int (should be the same one, cleared)
i2 := pool.Get()
if i2.String() != "0" {
t.Errorf("Expected cleared Int, got %s", i2.String())
}
// Verify it's the same object
i2.SetString("67890", 10)
pool.Put(i2)
i3 := pool.Get()
// Should be cleared again
if i3.String() != "0" {
t.Errorf("Expected cleared Int after second put, got %s", i3.String())
}
}
// TestExpModPooled tests pooled modular exponentiation
func TestExpModPooled(t *testing.T) {
pool := NewIntPool()
base := []byte{0x02} // 2
exp := []byte{0x0A} // 10
mod := []byte{0x03, 0xE8} // 1000
result := ExpModPooled(pool, base, exp, mod)
// 2^10 mod 1000 = 1024 mod 1000 = 24
expected := []byte{0x18} // 24
if !bytesEqual(result, expected) {
t.Errorf("ExpModPooled: expected %x, got %x", expected, result)
}
}
// TestModExp tests the convenience function
func TestModExp(t *testing.T) {
base := []byte{0x02}
exp := []byte{0x0A}
mod := []byte{0x03, 0xE8}
result, err := ModExp(base, exp, mod)
if err != nil {
t.Fatalf("ModExp failed: %v", err)
}
expected := []byte{0x18}
if !bytesEqual(result, expected) {
t.Errorf("ModExp: expected %x, got %x", expected, result)
}
// Test empty modulus case
result, err = ModExp(base, exp, []byte{})
if err != nil {
t.Errorf("Expected no error for empty modulus, got %v", err)
}
if len(result) != 0 {
t.Errorf("Expected empty result for empty modulus, got %x", result)
}
}
// TestPreallocatedExpMod tests pre-allocated operations
func TestPreallocatedExpMod(t *testing.T) {
base := NewInt()
exp := NewInt()
mod := NewInt()
result := NewInt()
base.SetString("2", 10)
exp.SetString("10", 10)
mod.SetString("1000", 10)
PreallocatedExpMod(result, base, exp, mod)
if result.String() != "24" {
t.Errorf("PreallocatedExpMod: expected 24, got %s", result.String())
}
}
// BenchmarkExpModPooled compares pooled vs non-pooled performance
func BenchmarkExpModPooled(b *testing.B) {
pool := NewIntPool()
base := []byte("123456789012345678901234567890")
exp := []byte("987654321098765432109876543210")
mod := []byte("111111111111111111111111111111")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = ExpModPooled(pool, base, exp, mod)
}
}
// BenchmarkExpModNonPooled benchmarks non-pooled operations for comparison
func BenchmarkExpModNonPooled(b *testing.B) {
base := []byte("123456789012345678901234567890")
exp := []byte("987654321098765432109876543210")
mod := []byte("111111111111111111111111111111")
b.ResetTimer()
for i := 0; i < b.N; i++ {
baseInt := NewInt()
expInt := NewInt()
modInt := NewInt()
resultInt := NewInt()
baseInt.SetBytes(base)
expInt.SetBytes(exp)
modInt.SetBytes(mod)
resultInt.ExpMod(baseInt, expInt, modInt)
_ = resultInt.Bytes()
}
}
// TestPoolConcurrency tests that the pool is safe for concurrent use
func TestPoolConcurrency(t *testing.T) {
pool := NewIntPool()
var wg sync.WaitGroup
// Run many concurrent operations
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
// Get and use an Int
num := pool.Get()
num.SetString("12345", 10)
// Simulate some work
result := NewInt()
result.ExpMod(num, num, num)
// Return to pool
pool.Put(num)
}(i)
}
wg.Wait()
}
// TestPoolWithBigNumbers tests pool with large numbers
func TestPoolWithBigNumbers(t *testing.T) {
pool := NewIntPool()
// Test with 2048-bit numbers
base := make([]byte, 256)
exp := make([]byte, 256)
mod := make([]byte, 256)
// Fill with test data
for i := range base {
base[i] = byte(i)
exp[i] = byte(255 - i)
mod[i] = 0xFF
}
mod[0] = 0x7F // Make sure modulus is not too large
// This should not panic or leak memory
for i := 0; i < 10; i++ {
result := ExpModPooled(pool, base, exp, mod)
if len(result) == 0 {
t.Error("Expected non-empty result")
}
}
}
// TestPoolComparison verifies pooled operations match non-pooled
func TestPoolComparison(t *testing.T) {
pool := NewIntPool()
testCases := []struct {
base string
exp string
mod string
}{
{"2", "10", "1000"},
{"123456789", "987654321", "1000000007"},
{"999999999999", "888888888888", "777777777777"},
}
for _, tc := range testCases {
// Non-pooled
base1 := NewInt()
exp1 := NewInt()
mod1 := NewInt()
result1 := NewInt()
base1.SetString(tc.base, 10)
exp1.SetString(tc.exp, 10)
mod1.SetString(tc.mod, 10)
result1.ExpMod(base1, exp1, mod1)
// Pooled
result2 := ExpModPooled(pool, base1.Bytes(), exp1.Bytes(), mod1.Bytes())
// Compare
if !bytesEqual(result1.Bytes(), result2) {
t.Errorf("Pooled vs non-pooled mismatch for %s^%s mod %s", tc.base, tc.exp, tc.mod)
}
// Also compare with math/big
bigBase := new(big.Int)
bigExp := new(big.Int)
bigMod := new(big.Int)
bigResult := new(big.Int)
bigBase.SetString(tc.base, 10)
bigExp.SetString(tc.exp, 10)
bigMod.SetString(tc.mod, 10)
bigResult.Exp(bigBase, bigExp, bigMod)
if !bytesEqual(bigResult.Bytes(), result2) {
t.Errorf("Pooled vs math/big mismatch for %s^%s mod %s", tc.base, tc.exp, tc.mod)
}
}
}

13
crypto/modexp/modexp.go Normal file
View file

@ -0,0 +1,13 @@
package modexp
import (
"github.com/ethereum/go-ethereum/crypto/modexp/bigint"
)
// ModExp performs modular exponentiation on byte arrays
// result = base^exp mod mod
// This uses the bigint implementation by default.
// To use GMP implementation, import crypto/modexp/gmp directly.
func ModExp(base, exp, mod []byte) ([]byte, error) {
return bigint.ModExp(base, exp, mod)
}

View file

@ -0,0 +1,75 @@
package modexp
import (
"bytes"
"testing"
)
func TestModExp(t *testing.T) {
tests := []struct {
name string
base []byte
exp []byte
mod []byte
want []byte
}{
{
name: "simple",
base: []byte{0x02}, // 2
exp: []byte{0x0A}, // 10
mod: []byte{0x03, 0xE8}, // 1000
want: []byte{0x18}, // 24
},
{
name: "zero_modulus",
base: []byte{0x02},
exp: []byte{0x03},
mod: []byte{},
want: []byte{},
},
{
name: "base_equals_one",
base: []byte{0x01},
exp: []byte{0xFF, 0xFF},
mod: []byte{0x07},
want: []byte{0x01},
},
{
name: "large_numbers",
base: []byte{0xFF, 0xFF},
exp: []byte{0x02},
mod: []byte{0x01, 0x00, 0x00},
want: []byte{0xFE, 0x01},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ModExp(tt.base, tt.exp, tt.mod)
if err != nil {
t.Fatalf("ModExp error: %v", err)
}
if !bytes.Equal(got, tt.want) {
t.Errorf("ModExp() = %x, want %x", got, tt.want)
}
})
}
}
func BenchmarkModExp(b *testing.B) {
base := make([]byte, 32)
exp := make([]byte, 32)
mod := make([]byte, 32)
for i := range base {
base[i] = byte(i * 17)
exp[i] = byte(i * 31)
mod[i] = byte(255 - i)
}
mod[31] |= 0x01 // Ensure odd modulus
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = ModExp(base, exp, mod)
}
}