diff --git a/crypto/modexp/benchmark_comprehensive_test.go b/crypto/modexp/benchmark_comprehensive_test.go new file mode 100644 index 0000000000..f4a518d33e --- /dev/null +++ b/crypto/modexp/benchmark_comprehensive_test.go @@ -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) + } + }) + }) + } +} \ No newline at end of file diff --git a/crypto/modexp/bigint/benchmark_test.go b/crypto/modexp/bigint/benchmark_test.go new file mode 100644 index 0000000000..2b5b18b726 --- /dev/null +++ b/crypto/modexp/bigint/benchmark_test.go @@ -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) + } + }) + } +} \ No newline at end of file diff --git a/crypto/modexp/bigint/bigint.go b/crypto/modexp/bigint/bigint.go new file mode 100644 index 0000000000..acf7459788 --- /dev/null +++ b/crypto/modexp/bigint/bigint.go @@ -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 +} \ No newline at end of file diff --git a/crypto/modexp/bigint/bigint_test.go b/crypto/modexp/bigint/bigint_test.go new file mode 100644 index 0000000000..b60c2988a8 --- /dev/null +++ b/crypto/modexp/bigint/bigint_test.go @@ -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) + } +} + diff --git a/crypto/modexp/gmp/README.md b/crypto/modexp/gmp/README.md new file mode 100644 index 0000000000..5168e9f55c --- /dev/null +++ b/crypto/modexp/gmp/README.md @@ -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. \ No newline at end of file diff --git a/crypto/modexp/gmp/cwrapper/gmp.go b/crypto/modexp/gmp/cwrapper/gmp.go new file mode 100644 index 0000000000..a6415feb0f --- /dev/null +++ b/crypto/modexp/gmp/cwrapper/gmp.go @@ -0,0 +1,147 @@ +package gmp + +// #cgo LDFLAGS: -lgmp +// #include +// #include +// #include +// #include +// +// 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") + } +} \ No newline at end of file diff --git a/crypto/modexp/gmp/cwrapper/gmp_test.go b/crypto/modexp/gmp/cwrapper/gmp_test.go new file mode 100644 index 0000000000..9aa3191a4f --- /dev/null +++ b/crypto/modexp/gmp/cwrapper/gmp_test.go @@ -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() + } +} \ No newline at end of file diff --git a/crypto/modexp/gmp/generic/gmp.go b/crypto/modexp/gmp/generic/gmp.go new file mode 100644 index 0000000000..c06fca65ee --- /dev/null +++ b/crypto/modexp/gmp/generic/gmp.go @@ -0,0 +1,170 @@ +package gmp + +// Uses system-installed GMP library + +// #cgo LDFLAGS: -lgmp +// #include +// #include +// +// 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 "" + } + + // 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 +} \ No newline at end of file diff --git a/crypto/modexp/gmp/generic/gmp_test.go b/crypto/modexp/gmp/generic/gmp_test.go new file mode 100644 index 0000000000..93e427af9c --- /dev/null +++ b/crypto/modexp/gmp/generic/gmp_test.go @@ -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 +} \ No newline at end of file diff --git a/crypto/modexp/gmp/pool/gmp.go b/crypto/modexp/gmp/pool/gmp.go new file mode 100644 index 0000000000..fe103f073a --- /dev/null +++ b/crypto/modexp/gmp/pool/gmp.go @@ -0,0 +1,96 @@ +package gmp + +// #include +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) +} \ No newline at end of file diff --git a/crypto/modexp/gmp/pool/gmp_test.go b/crypto/modexp/gmp/pool/gmp_test.go new file mode 100644 index 0000000000..b7bb27ddf0 --- /dev/null +++ b/crypto/modexp/gmp/pool/gmp_test.go @@ -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) + } + } +} \ No newline at end of file diff --git a/crypto/modexp/modexp.go b/crypto/modexp/modexp.go new file mode 100644 index 0000000000..2fe383f2ac --- /dev/null +++ b/crypto/modexp/modexp.go @@ -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) +} \ No newline at end of file diff --git a/crypto/modexp/modexp_test.go b/crypto/modexp/modexp_test.go new file mode 100644 index 0000000000..cb3b2a25b7 --- /dev/null +++ b/crypto/modexp/modexp_test.go @@ -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) + } +} \ No newline at end of file