From e131482fe298895ffc0bd1d57ed00c527ecf024f Mon Sep 17 00:00:00 2001 From: Kevaundray Wedderburn Date: Fri, 11 Jul 2025 00:07:58 +0100 Subject: [PATCH] Add new modexp package --- crypto/modexp/bigint/bigint.go | 45 ++++++++++ crypto/modexp/bigint/bigint_test.go | 86 ++++++++++++++++++ crypto/modexp/gmp/README.md | 46 ++++++++++ crypto/modexp/gmp/gmp.go | 96 ++++++++++++++++++++ crypto/modexp/gmp/gmp_test.go | 135 ++++++++++++++++++++++++++++ crypto/modexp/gmp/modexp.c | 55 ++++++++++++ crypto/modexp/gmp/modexp.h | 15 ++++ crypto/modexp/modexp.go | 29 ++++++ 8 files changed, 507 insertions(+) create mode 100644 crypto/modexp/bigint/bigint.go create mode 100644 crypto/modexp/bigint/bigint_test.go create mode 100644 crypto/modexp/gmp/README.md create mode 100644 crypto/modexp/gmp/gmp.go create mode 100644 crypto/modexp/gmp/gmp_test.go create mode 100644 crypto/modexp/gmp/modexp.c create mode 100644 crypto/modexp/gmp/modexp.h create mode 100644 crypto/modexp/modexp.go diff --git a/crypto/modexp/bigint/bigint.go b/crypto/modexp/bigint/bigint.go new file mode 100644 index 0000000000..2d1a7a2eea --- /dev/null +++ b/crypto/modexp/bigint/bigint.go @@ -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 . + +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 +} diff --git a/crypto/modexp/bigint/bigint_test.go b/crypto/modexp/bigint/bigint_test.go new file mode 100644 index 0000000000..23c52d81d3 --- /dev/null +++ b/crypto/modexp/bigint/bigint_test.go @@ -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 . + +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()) + } + }) + } +} diff --git a/crypto/modexp/gmp/README.md b/crypto/modexp/gmp/README.md new file mode 100644 index 0000000000..581fa6e885 --- /dev/null +++ b/crypto/modexp/gmp/README.md @@ -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. \ No newline at end of file diff --git a/crypto/modexp/gmp/gmp.go b/crypto/modexp/gmp/gmp.go new file mode 100644 index 0000000000..70e8e5504d --- /dev/null +++ b/crypto/modexp/gmp/gmp.go @@ -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 . + +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") + } +} diff --git a/crypto/modexp/gmp/gmp_test.go b/crypto/modexp/gmp/gmp_test.go new file mode 100644 index 0000000000..5ac253dfb1 --- /dev/null +++ b/crypto/modexp/gmp/gmp_test.go @@ -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 . + +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() + } +} diff --git a/crypto/modexp/gmp/modexp.c b/crypto/modexp/gmp/modexp.c new file mode 100644 index 0000000000..dd5457aa67 --- /dev/null +++ b/crypto/modexp/gmp/modexp.c @@ -0,0 +1,55 @@ +#include +#include +#include +#include + +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; +} \ No newline at end of file diff --git a/crypto/modexp/gmp/modexp.h b/crypto/modexp/gmp/modexp.h new file mode 100644 index 0000000000..7df8fcf614 --- /dev/null +++ b/crypto/modexp/gmp/modexp.h @@ -0,0 +1,15 @@ +#ifndef MODEXP_H +#define MODEXP_H + +#include +#include + +// 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 \ No newline at end of file diff --git a/crypto/modexp/modexp.go b/crypto/modexp/modexp.go new file mode 100644 index 0000000000..7aed2b30c8 --- /dev/null +++ b/crypto/modexp/modexp.go @@ -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 . + +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) +} \ No newline at end of file