Add new modexp package

This commit is contained in:
Kevaundray Wedderburn 2025-07-11 00:07:58 +01:00
parent d72ec587a7
commit e131482fe2
8 changed files with 507 additions and 0 deletions

View file

@ -0,0 +1,45 @@
// Copyright 2024 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package bigint
import (
"math/big"
)
// ModExp performs modular exponentiation using Go's big.Int
// result = base^exp mod mod
func ModExp(base, exp, mod []byte) ([]byte, error) {
baseBig := new(big.Int).SetBytes(base)
expBig := new(big.Int).SetBytes(exp)
modBig := new(big.Int).SetBytes(mod)
// Handle special cases
if modBig.BitLen() == 0 {
// Modulo 0 is undefined, return empty bytes
return []byte{}, nil
}
if baseBig.BitLen() == 1 {
// If base == 1, then we can just return base % mod
result := baseBig.Mod(baseBig, modBig)
return result.Bytes(), nil
}
// Perform modular exponentiation
result := new(big.Int).Exp(baseBig, expBig, modBig)
return result.Bytes(), nil
}

View file

@ -0,0 +1,86 @@
// Copyright 2024 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package bigint
import (
"bytes"
"math/big"
"testing"
)
func TestModExp(t *testing.T) {
tests := []struct {
name string
base string
exp string
mod string
want string
}{
{
name: "simple",
base: "2",
exp: "10",
mod: "1000",
want: "24",
},
{
name: "zero_exponent",
base: "12345",
exp: "0",
mod: "67890",
want: "1",
},
{
name: "base_one",
base: "1",
exp: "999999",
mod: "1000",
want: "1",
},
{
name: "zero_modulus",
base: "2",
exp: "10",
mod: "0",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
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)
wantBig := new(big.Int)
if tt.want != "" {
wantBig.SetString(tt.want, 10)
}
result, err := ModExp(baseBig.Bytes(), expBig.Bytes(), modBig.Bytes())
if err != nil {
t.Fatalf("ModExp error: %v", err)
}
if !bytes.Equal(result, wantBig.Bytes()) {
t.Errorf("ModExp result mismatch: got %x, want %x", result, wantBig.Bytes())
}
})
}
}

View file

@ -0,0 +1,46 @@
# 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 using a direct C call to avoid multiple calls to cgo. The main usage is for the modexp precompile.
### Byte Array Interface
```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)
```
## Testing
Run tests from the go-ethereum root directory:
```bash
go test ./crypto/modexp/gmp/...
```
## License
TODO -- whatever go-ethereum does.

96
crypto/modexp/gmp/gmp.go Normal file
View file

@ -0,0 +1,96 @@
// Copyright 2024 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package gmp
// #cgo LDFLAGS: -lgmp
// #include "modexp.h"
import "C"
import (
"errors"
"runtime"
"unsafe"
)
// ModExp performs modular exponentiation using GMP
// This is thread safe.
func ModExp(base, exp, mod []byte) ([]byte, error) {
// Handle empty modulus - return empty result (EVM behavior)
if len(mod) == 0 {
return []byte{}, nil
}
// Special case: zero modulus
allZero := true
for _, b := range mod {
if b != 0 {
allZero = false
break
}
}
if allZero {
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
// This avoids UB when the length is zero.
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,
)
// Keep the slices alive until after the C call completes
runtime.KeepAlive(base)
runtime.KeepAlive(exp)
runtime.KeepAlive(mod)
runtime.KeepAlive(result)
// 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,135 @@
// Copyright 2024 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package gmp
import (
"bytes"
"math/big"
"testing"
)
// TestWrapperVsBigInt compares the wrapper with Go's math/big
func TestWrapperVsBigInt(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 math/big
bigResult := new(big.Int)
bigResult.Exp(baseBig, expBig, modBig)
expectedResult := bigResult.Bytes()
// Compare results
if !bytes.Equal(wrapperResult, expectedResult) {
t.Errorf("Results differ:\nWrapper: %x\nExpected: %x",
wrapperResult, expectedResult)
}
})
}
}
// 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 BenchmarkModExpBigInt(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 := new(big.Int).SetBytes(baseBytes)
exp := new(big.Int).SetBytes(expBytes)
mod := new(big.Int).SetBytes(modBytes)
b.ResetTimer()
for i := 0; i < b.N; i++ {
result := new(big.Int)
result.Exp(base, exp, mod)
_ = result.Bytes()
}
}

View file

@ -0,0 +1,55 @@
#include <gmp.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
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_inits(base_mpz, exp_mpz, mod_mpz, result_mpz, NULL);
// 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);
}
// Perform modular exponentiation
mpz_powm(result_mpz, base_mpz, exp_mpz, mod_mpz);
// Get exact size needed for result
size_t needed = 0;
mpz_export(NULL, &needed, 1, 1, 0, 0, result_mpz);
// Check if result buffer is large enough
if (*result_len < needed) {
mpz_clears(base_mpz, exp_mpz, mod_mpz, result_mpz, NULL);
return -2;
}
// Export result to big-endian byte array
mpz_export(result, &needed, 1, 1, 0, 0, result_mpz);
*result_len = needed;
// Clean up
mpz_clears(base_mpz, exp_mpz, mod_mpz, result_mpz, NULL);
return 0;
}

View file

@ -0,0 +1,15 @@
#ifndef MODEXP_H
#define MODEXP_H
#include <stdint.h>
#include <stddef.h>
// Perform modular exponentiation: base^exp mod mod
// Returns 0 on success, -1 on invalid input, -2 if result buffer too small
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);
#endif // MODEXP_H

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

@ -0,0 +1,29 @@
// Copyright 2024 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package modexp
import (
"github.com/ethereum/go-ethereum/crypto/modexp/gmp"
)
// ModExp performs modular exponentiation on byte arrays
// result = base^exp mod mod
// This uses the gmp implementation by default.
// To use bigint implementation, import crypto/modexp/bigint directly.
func ModExp(base, exp, mod []byte) ([]byte, error) {
return gmp.ModExp(base, exp, mod)
}