diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3960c4e5..7b517326 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -154,7 +154,7 @@ jobs: path: boringssl/build/ native-build-macos: - runs-on: macos-13 + runs-on: macos-15-intel env: SKIP_GRADLE: true steps: @@ -215,7 +215,7 @@ jobs: path: boringssl/build/ native-build-m1: - runs-on: macos-13-xlarge + runs-on: macos-15-xlarge env: SKIP_GRADLE: true steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 32783286..dcffb15e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog # Unreleased +* BREAKING: `LibGnarkEIP196.eip196altbn128G1Add`, `eip196altbn128G1Mul` and `eip196altbn128Pairing` native methods visibility now private, use `eip196_perform_operation` instead +* perf: Optimize EIP-196 AltBn128 [#301](https://github.com/hyperledger/besu-native/pull/301) # 1.4.1 * Add and export JNI functions for IsOnCurve and IsInSubgroup for both G1 and G2 points[#279](https://github.com/hyperledger/besu-native/pull/279) diff --git a/gnark/gnark-jni/gnark-eip-196.go b/gnark/gnark-jni/gnark-eip-196.go index 2a968b78..b280f19c 100644 --- a/gnark/gnark-jni/gnark-eip-196.go +++ b/gnark/gnark-jni/gnark-eip-196.go @@ -22,111 +22,84 @@ import "C" import ( "errors" "math/big" + "sync" "unsafe" "github.com/consensys/gnark-crypto/ecc/bn254" ) -var ErrMalformedPointEIP196 = errors.New("invalid point encoding") -var ErrInvalidInputPairingLengthEIP196 = errors.New("invalid input parameters, invalid input length for pairing") -var ErrPointNotInFieldEIP196 = errors.New("point not in field") -var ErrPointInSubgroupCheckFailedEIP196 = errors.New("point is not in subgroup") -var ErrPointOnCurveCheckFailedEIP196 = errors.New("point is not on curve") +type errorCode = C.int + +// keep in sync with the Java code. We use constant value to avoid passing strings from Java to Go +const ( + errCodeSuccess errorCode = iota + errCodeInvalidInputPairingLengthEIP196 + errCodePointNotInFieldEIP196 + errCodePointInSubgroupCheckFailedEIP196 + errCodePointOnCurveCheckFailedEIP196 + errCodePairingCheckErrorEIP196 +) const ( - EIP196PreallocateForResult = 128 - EIP196PreallocateForError = 256 EIP196PreallocateForScalar = 32 // scalar int is 32 byte EIP196PreallocateForFp = 32 // field elements are 32 bytes EIP196PreallocateForG1 = EIP196PreallocateForFp * 2 // G1 points are encoded as 2 concatenated field elements EIP196PreallocateForG2 = EIP196PreallocateForG1 * 2 // G2 points are encoded as 2 concatenated G1 points ) -var EIP196ScalarTwo = big.NewInt(2) +var bigIntPool = sync.Pool{ + New: func() any { + return new(big.Int) + }, +} -// bn254Modulus is the value 21888242871839275222246405745257275088696311157297823662689037894645226208583 -var bn254Modulus = new(big.Int).SetBytes([]byte{ - 0x30, 0x64, 0x4e, 0x72, 0xe1, 0x31, 0xa0, 0x29, - 0xb8, 0x50, 0x45, 0xb6, 0x81, 0x81, 0x58, 0x5d, - 0x97, 0x81, 0x6a, 0x91, 0x68, 0x71, 0xca, 0x8d, - 0x3c, 0x20, 0x8c, 0x16, 0xd8, 0x7c, 0xfd, 0x47, -}) +var EIP196ScalarTwo = big.NewInt(2) //export eip196altbn128G1Add -func eip196altbn128G1Add(javaInputBuf, javaOutputBuf, javaErrorBuf *C.char, cInputLen C.int, cOutputLen, cErrorLen *C.int) C.int { - inputLen := int(cInputLen) - errorLen := (*int)(unsafe.Pointer(cErrorLen)) - outputLen := (*int)(unsafe.Pointer(cOutputLen)) - - // Convert error C pointers to Go slices - errorBuf := castErrorBufferEIP196(javaErrorBuf, errorLen) - - if inputLen > 2*EIP196PreallocateForG1 { - // trunc if input too long - inputLen = 2 * EIP196PreallocateForG1 +func eip196altbn128G1Add(javaInputBuf, javaOutputBuf *C.char, cInputLen C.int) errorCode { + inputLen := min(int(cInputLen), 2*EIP196PreallocateForG1) // max input length is 2 G1 points + if inputLen == 0 { + return errCodeSuccess } // Convert input C pointers to Go slices input := (*[2 * EIP196PreallocateForG1]byte)(unsafe.Pointer(javaInputBuf))[:inputLen:inputLen] - if inputLen == 0 { - *outputLen = EIP196PreallocateForG1 - return 0 - } - // generate p0 g1 affine var p0 bn254.G1Affine - - err := safeUnmarshalEIP196(&p0, input, 0) - - if err != nil { - dryError(err, errorBuf, outputLen, errorLen) - return 1 + if err := safeUnmarshalEIP196(&p0, input, 0); err != errCodeSuccess { + return err } if inputLen < 2*EIP196PreallocateForG1 { // if incomplete input is all zero, return p0 if isAllZeroEIP196(input, 64, 64) { - ret := p0.Marshal() - g1AffineEncode(ret, javaOutputBuf) - *outputLen = EIP196PreallocateForG1 - return 0 + g1AffineEncode(&p0, javaOutputBuf) + return errCodeSuccess } } // generate p1 g1 affine var p1 bn254.G1Affine - err = safeUnmarshalEIP196(&p1, input, 64) - - if err != nil { - dryError(err, errorBuf, outputLen, errorLen) - return 1 + if err := safeUnmarshalEIP196(&p1, input, 64); err != errCodeSuccess { + return err } - var result *bn254.G1Affine // Use the Add method to combine points - result = p0.Add(&p0, &p1) + p0.Add(&p0, &p1) // marshal the resulting point and encode directly to the output buffer - ret := result.Marshal() - g1AffineEncode(ret, javaOutputBuf) - *outputLen = EIP196PreallocateForG1 - return 0 + g1AffineEncode(&p0, javaOutputBuf) + return errCodeSuccess } //export eip196altbn128G1Mul -func eip196altbn128G1Mul(javaInputBuf, javaOutputBuf, javaErrorBuf *C.char, cInputLen C.int, cOutputLen, cErrorLen *C.int) C.int { +func eip196altbn128G1Mul(javaInputBuf, javaOutputBuf *C.char, cInputLen C.int) errorCode { inputLen := int(cInputLen) - errorLen := (*int)(unsafe.Pointer(cErrorLen)) - outputLen := (*int)(unsafe.Pointer(cOutputLen)) - - // Convert error C pointers to Go slices - errorBuf := castErrorBufferEIP196(javaErrorBuf, errorLen) if inputLen == 0 { // zero input returns 0 - *outputLen = EIP196PreallocateForG1 - return 0 + return errCodeSuccess } if inputLen > EIP196PreallocateForG1+EIP196PreallocateForScalar { @@ -139,34 +112,31 @@ func eip196altbn128G1Mul(javaInputBuf, javaOutputBuf, javaErrorBuf *C.char, cInp // infinity check: if isAllZeroEIP196(input, 0, 64) { - *outputLen = EIP196PreallocateForG1 - return 0 + return errCodeSuccess } // generate p0 g1 affine var p0 bn254.G1Affine - err := safeUnmarshalEIP196(&p0, input, 0) - - if err != nil { - dryError(err, errorBuf, outputLen, errorLen) - return 1 + if err := safeUnmarshalEIP196(&p0, input, 0); err != errCodeSuccess { + return err } if inputLen < EIP196PreallocateForG1+1 { // if there is not even a partial input scalar, return 0 - *outputLen = EIP196PreallocateForG1 - return 0 + return errCodeSuccess } // Convert byte slice to *big.Int scalarBytes := input[EIP196PreallocateForG1:] if 96 > int(cInputLen) { // if the input is truncated, copy the bytes to the high order portion of the scalar - scalarBytes = make([]byte, 32) + var bytes32 [32]byte + scalarBytes = bytes32[:] copy(scalarBytes[:], input[64:int(cInputLen)]) } - scalar := big.NewInt(0) + scalar := bigIntPool.Get().(*big.Int) + defer bigIntPool.Put(scalar) scalar.SetBytes(scalarBytes[:]) var result *bn254.G1Affine @@ -179,34 +149,23 @@ func eip196altbn128G1Mul(javaInputBuf, javaOutputBuf, javaErrorBuf *C.char, cInp } // marshal the resulting point and encode directly to the output buffer - ret := result.Marshal() - g1AffineEncode(ret, javaOutputBuf) - *outputLen = EIP196PreallocateForG1 - return 0 + g1AffineEncode(result, javaOutputBuf) + return errCodeSuccess } //export eip196altbn128Pairing -func eip196altbn128Pairing(javaInputBuf, javaOutputBuf, javaErrorBuf *C.char, cInputLen C.int, cOutputLen, cErrorLen *C.int) C.int { +func eip196altbn128Pairing(javaInputBuf, javaOutputBuf *C.char, cInputLen C.int) C.int { inputLen := int(cInputLen) - errorLen := (*int)(unsafe.Pointer(cErrorLen)) - outputLen := (*int)(unsafe.Pointer(cOutputLen)) - - // Convert error C pointers to Go slices - output := castBufferEIP196(javaOutputBuf, outputLen) - - // Convert error C pointers to Go slices - errorBuf := castErrorBufferEIP196(javaErrorBuf, errorLen) - - *outputLen = 32 if inputLen == 0 { + // Empty input means pairing succeeded with result 1 + output := (*[32]byte)(unsafe.Pointer(javaOutputBuf)) output[31] = 0x01 - return 0 + return errCodeSuccess } if inputLen%(EIP196PreallocateForG2+EIP196PreallocateForG1) != 0 { - dryError(ErrInvalidInputPairingLengthEIP196, errorBuf, outputLen, errorLen) - return 1 + return errCodeInvalidInputPairingLengthEIP196 } // Convert input C pointers to Go slice @@ -219,66 +178,55 @@ func eip196altbn128Pairing(javaInputBuf, javaOutputBuf, javaErrorBuf *C.char, cI for i := 0; i < pairCount; i++ { // g1 x and y are the first 64 bytes of each 192 byte pair - var g1 bn254.G1Affine - err := safeUnmarshalEIP196(&g1, input[i*192:i*192+64], 0) - - if err != nil { - dryError(err, errorBuf, outputLen, errorLen) - return 1 + if err := safeUnmarshalEIP196(&g1Points[i], input[i*192:i*192+64], 0); err != errCodeSuccess { + return err } // g2 points are latter 128 bytes of each 192 byte pair - var g2 bn254.G2Affine - err = safeUnmarshalG2EIP196(&g2, input[i*192+64:(i+1)*192]) - - if err != nil { - dryError(err, errorBuf, outputLen, errorLen) - return 1 + if err := safeUnmarshalG2EIP196(&g2Points[i], input[i*192+64:(i+1)*192]); err != errCodeSuccess { + return err } - - // collect g1, g2 points - g1Points[i] = g1 - g2Points[i] = g2 } isOne, err := bn254.PairingCheck(g1Points, g2Points) if err != nil { - dryError(err, errorBuf, outputLen, errorLen) - return -1 + // this indicates internal pairing check error. Knowing gnark, it only happens when the input slices are with unequal lengths. + // we have constructed them to be of equal length, so it is a sanity check + return errCodePairingCheckErrorEIP196 } + // Write result to output buffer + output := (*[32]byte)(unsafe.Pointer(javaOutputBuf)) if isOne { - // respond with 1 if pairing check was true, leave 0's intact otherwise output[31] = 0x01 } - return 0 + // else: output is already zero-initialized on Java side + return errCodeSuccess } -func g1AffineEncode(g1Point []byte, output *C.char) error { +func g1AffineEncode(point *bn254.G1Affine, output *C.char) error { // Check if point is not nil - if g1Point == nil || len(g1Point) != 64 { + if point == nil { return errors.New("point cannot be nil") } + bts := point.RawBytes() - // gnark bn254 returns two 32 byte points in a packed array - unsafeG1Ptr := unsafe.Pointer(&g1Point[0]) - - // copy unsafe to output[0:64], - C.memcpy(unsafe.Pointer(uintptr(unsafe.Pointer(output))), unsafeG1Ptr, 64) + copy((*[64]byte)(unsafe.Pointer(output))[:], bts[:]) return nil } -func safeUnmarshalEIP196(g1 *bn254.G1Affine, input []byte, offset int) error { +func safeUnmarshalEIP196(g1 *bn254.G1Affine, input []byte, offset int) errorCode { var pointBytes []byte // If we effectively have _NO_ input, return empty if len(input)-offset <= 0 { - return nil + return errCodeSuccess } else if len(input)-offset < 64 { // If we have some input, but it is incomplete, pad with zero - pointBytes = make([]byte, 64) + var bytes64 [64]byte + pointBytes = bytes64[:] shortLen := len(input) - offset copy(pointBytes, input[offset:len(input)]) for i := shortLen; i < 64; i++ { @@ -288,63 +236,45 @@ func safeUnmarshalEIP196(g1 *bn254.G1Affine, input []byte, offset int) error { pointBytes = input[offset : offset+64] } - if !checkInFieldEIP196(pointBytes[0:32]) { - return ErrPointNotInFieldEIP196 + if err := g1.X.SetBytesCanonical(pointBytes[0:32]); err != nil { + return errCodePointNotInFieldEIP196 } - - err := g1.X.SetBytesCanonical(pointBytes[0:32]) - - if err == nil { - - if !checkInFieldEIP196(pointBytes[32:64]) { - return ErrPointNotInFieldEIP196 - } - err := g1.Y.SetBytesCanonical(pointBytes[32:64]) - if err == nil { - if !g1.IsOnCurve() { - return ErrPointOnCurveCheckFailedEIP196 - } - return nil - } + if err := g1.Y.SetBytesCanonical(pointBytes[32:64]); err != nil { + return errCodePointNotInFieldEIP196 + } + if !g1.IsOnCurve() { + return errCodePointOnCurveCheckFailedEIP196 } - return err + return errCodeSuccess } -func safeUnmarshalG2EIP196(g2 *bn254.G2Affine, input []byte) error { +func safeUnmarshalG2EIP196(g2 *bn254.G2Affine, input []byte) errorCode { if len(input) < EIP196PreallocateForG2 { - return ErrInvalidInputPairingLengthEIP196 + return errCodeInvalidInputPairingLengthEIP196 } - if !(checkInFieldEIP196(input[0:32]) && checkInFieldEIP196(input[32:64]) && - checkInFieldEIP196(input[64:96]) && checkInFieldEIP196(input[96:128])) { - return ErrPointNotInFieldEIP196 + if err := g2.X.A1.SetBytesCanonical(input[:32]); err != nil { + return errCodePointNotInFieldEIP196 + } + if err := g2.X.A0.SetBytesCanonical(input[32:64]); err != nil { + return errCodePointNotInFieldEIP196 + } + if err := g2.Y.A1.SetBytesCanonical(input[64:96]); err != nil { + return errCodePointNotInFieldEIP196 + } + if err := g2.Y.A0.SetBytesCanonical(input[96:128]); err != nil { + return errCodePointNotInFieldEIP196 } - - g2.X.A1.SetBytesCanonical(input[:32]) - g2.X.A0.SetBytesCanonical(input[32:64]) - g2.Y.A1.SetBytesCanonical(input[64:96]) - g2.Y.A0.SetBytesCanonical(input[96:128]) if !g2.IsOnCurve() { - return ErrPointOnCurveCheckFailedEIP196 + return errCodePointOnCurveCheckFailedEIP196 } if !g2.IsInSubGroup() { - return ErrPointInSubgroupCheckFailedEIP196 + return errCodePointInSubgroupCheckFailedEIP196 } - return nil -} - -// checkInField checks that an element is in the field, not-in-field will normally -// be caught during unmarshal, but here in case of no-op calls of a single parameter -func checkInFieldEIP196(data []byte) bool { - - // Convert the byte slice to a big.Int - elem := new(big.Int).SetBytes(data) - - // Compare the value to the bn254Modulus - return bn254Modulus.Cmp(elem) == 1 + return errCodeSuccess } // isAllZero checks if all elements in the byte slice are zero @@ -365,32 +295,9 @@ func isAllZeroEIP196(data []byte, offset, length int) bool { return true } -func dryError(err error, errorBuf []byte, outputLen, errorLen *int) { - errStr := "invalid input parameters, " + err.Error() - copy(errorBuf, errStr) - *outputLen = 0 - *errorLen = len(errStr) -} - func castBufferToSliceEIP196(buf unsafe.Pointer, length int) []byte { return unsafe.Slice((*byte)(buf), length) } -func castBufferEIP196(javaOutputBuf *C.char, length *int) []byte { - bufSize := *length - if bufSize != EIP196PreallocateForResult { - bufSize = EIP196PreallocateForResult - } - return (*[EIP196PreallocateForResult]byte)(unsafe.Pointer(javaOutputBuf))[:bufSize:bufSize] -} - -func castErrorBufferEIP196(javaOutputBuf *C.char, length *int) []byte { - bufSize := *length - if bufSize != EIP196PreallocateForError { - bufSize = EIP196PreallocateForError - } - return (*[EIP196PreallocateForError]byte)(unsafe.Pointer(javaOutputBuf))[:bufSize:bufSize] -} - func main() { } diff --git a/gnark/gnark-jni/go.mod b/gnark/gnark-jni/go.mod index 8489bf83..2e697b35 100644 --- a/gnark/gnark-jni/go.mod +++ b/gnark/gnark-jni/go.mod @@ -4,7 +4,7 @@ go 1.25 toolchain go1.25.4 -require github.com/consensys/gnark-crypto v0.19.1-0.20250919185810-d7ecdb060877 +require github.com/consensys/gnark-crypto v0.19.2 require ( github.com/bits-and-blooms/bitset v1.24.0 // indirect diff --git a/gnark/gnark-jni/go.sum b/gnark/gnark-jni/go.sum index 8013ef9c..636896b8 100644 --- a/gnark/gnark-jni/go.sum +++ b/gnark/gnark-jni/go.sum @@ -1,15 +1,15 @@ github.com/bits-and-blooms/bitset v1.24.0 h1:H4x4TuulnokZKvHLfzVRTHJfFfnHEeSYJizujEZvmAM= github.com/bits-and-blooms/bitset v1.24.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= -github.com/consensys/gnark-crypto v0.19.1-0.20250919185810-d7ecdb060877 h1:4xq7C8vx7G5T1FFA+PwCcNrVDLMCAT5qFAsAL9GBs7Y= -github.com/consensys/gnark-crypto v0.19.1-0.20250919185810-d7ecdb060877/go.mod h1:OgCH7cSoJ46c+nOzvQuwOrIE9fawpXMYOQFzj22Vy3E= +github.com/consensys/gnark-crypto v0.19.2 h1:qrEAIXq3T4egxqiliFFoNrepkIWVEeIYwt3UL0fvS80= +github.com/consensys/gnark-crypto v0.19.2/go.mod h1:rT23F0XSZqE0mUA0+pRtnL56IbPxs6gp4CeRsBk4XS0= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= diff --git a/gnark/src/main/java/org/hyperledger/besu/nativelib/gnark/LibGnarkEIP196.java b/gnark/src/main/java/org/hyperledger/besu/nativelib/gnark/LibGnarkEIP196.java index 32b850dd..585264a8 100644 --- a/gnark/src/main/java/org/hyperledger/besu/nativelib/gnark/LibGnarkEIP196.java +++ b/gnark/src/main/java/org/hyperledger/besu/nativelib/gnark/LibGnarkEIP196.java @@ -20,8 +20,8 @@ public class LibGnarkEIP196 { - public static final int EIP196_PREALLOCATE_FOR_RESULT_BYTES = 128; - public static final int EIP196_PREALLOCATE_FOR_ERROR_BYTES = 256; // includes error string + public static final int EIP196_PREALLOCATE_FOR_RESULT_BYTES = 64; + public static final int EIP196_PAIR_PREALLOCATE_FOR_RESULT_BYTES = 32; @SuppressWarnings("WeakerAccess") public static final byte EIP196_ADD_OPERATION_RAW_VALUE = 1; public static final byte EIP196_MUL_OPERATION_RAW_VALUE = 2; @@ -29,6 +29,22 @@ public class LibGnarkEIP196 { public static final boolean ENABLED; + // Keep in sync with the Go code. We use constant values to avoid passing strings from Java to Go + // errCodeSuccess errorCode = iota + // errCodeInvalidInputPairingLengthEIP196 + // errCodePointNotInFieldEIP196 + // errCodePointInSubgroupCheckFailedEIP196 + // errCodePointOnCurveCheckFailedEIP196 + // errCodePairingCheckErrorEIP196 + public static final int EIP196_ERR_CODE_SUCCESS = 0; + public static final int EIP196_ERR_CODE_INVALID_INPUT_PAIRING_LENGTH = 1; + public static final int EIP196_ERR_CODE_POINT_NOT_IN_FIELD = 2; + public static final int EIP196_ERR_CODE_POINT_IN_SUBGROUP_CHECK_FAILED = 3; + public static final int EIP196_ERR_CODE_POINT_ON_CURVE_CHECK_FAILED = 4; + public static final int EIP196_ERR_CODE_PAIRING_CHECK_ERROR = 5; + // only on java side + public static final int EIP196_ERR_CODE_INVALID_OUTPUT_LENGTH = 6; + static { boolean enabled; try { @@ -42,27 +58,40 @@ public class LibGnarkEIP196 { } /** - * Here as a compatibility shim for the pre-existing matter-labs implementation. + * SAFETY: This method validates output buffer size before calling native code to prevent JVM crashes from buffer overflows. + * The native methods use JNA direct mapping without bounds checking. + * + * @param op Operation type (ADD=1, MUL=2, PAIR=3) + * @param i Input data + * @param i_len Length of valid input data + * @param output Output buffer - MUST be at least EIP196_PREALLOCATE_FOR_RESULT_BYTES (64 bytes) + * @return Error code: 0=success, 6=invalid output length, other codes from native operations */ public static int eip196_perform_operation( byte op, byte[] i, int i_len, - byte[] output, - IntByReference o_len, - byte[] err, - IntByReference err_len) { + byte[] output) { int ret = -1; switch(op) { case EIP196_ADD_OPERATION_RAW_VALUE: - ret = eip196altbn128G1Add(i, output, err, i_len, o_len, err_len); + if (output.length < EIP196_PREALLOCATE_FOR_RESULT_BYTES) { + return EIP196_ERR_CODE_INVALID_OUTPUT_LENGTH; + } + ret = eip196altbn128G1Add(i, output, i_len); break; case EIP196_MUL_OPERATION_RAW_VALUE: - ret = eip196altbn128G1Mul(i, output, err, i_len, o_len, err_len); + if (output.length < EIP196_PREALLOCATE_FOR_RESULT_BYTES) { + return EIP196_ERR_CODE_INVALID_OUTPUT_LENGTH; + } + ret = eip196altbn128G1Mul(i, output, i_len); break; case EIP196_PAIR_OPERATION_RAW_VALUE: - ret = eip196altbn128Pairing(i, output, err, i_len, o_len, err_len); + if (output.length < EIP196_PAIR_PREALLOCATE_FOR_RESULT_BYTES) { + return EIP196_ERR_CODE_INVALID_OUTPUT_LENGTH; + } + ret = eip196altbn128Pairing(i, output, i_len); break; default: throw new RuntimeException("Not Implemented EIP-196 operation " + op); @@ -71,21 +100,27 @@ public static int eip196_perform_operation( return ret; } - public static native int eip196altbn128G1Add( + /** + * Assumes output length bounds are already checked, otherwise can lead to JVM crash + */ + private static native int eip196altbn128G1Add( byte[] input, byte[] output, - byte[] error, - int inputSize, IntByReference outputSize, IntByReference err_len); + int inputSize); - public static native int eip196altbn128G1Mul( + /** + * Assumes output length bounds are already checked, otherwise can lead to JVM crash + */ + private static native int eip196altbn128G1Mul( byte[] input, byte[] output, - byte[] error, - int inputSize, IntByReference output_len, IntByReference err_len); + int inputSize); - public static native int eip196altbn128Pairing( + /** + * Assumes output length bounds are already checked, otherwise can lead to JVM crash + */ + private static native int eip196altbn128Pairing( byte[] input, byte[] output, - byte[] error, - int inputSize, IntByReference output_len, IntByReference err_len); -} + int inputSize); +} \ No newline at end of file diff --git a/gnark/src/test/java/org/hyperledger/besu/nativelib/gnark/AltBN128G1AddPrecompiledContractTest.java b/gnark/src/test/java/org/hyperledger/besu/nativelib/gnark/AltBN128G1AddPrecompiledContractTest.java index f2a9400b..0a6d040e 100644 --- a/gnark/src/test/java/org/hyperledger/besu/nativelib/gnark/AltBN128G1AddPrecompiledContractTest.java +++ b/gnark/src/test/java/org/hyperledger/besu/nativelib/gnark/AltBN128G1AddPrecompiledContractTest.java @@ -16,7 +16,6 @@ package org.hyperledger.besu.nativelib.gnark; import com.google.common.io.CharStreams; -import com.sun.jna.ptr.IntByReference; import org.apache.tuweni.bytes.Bytes; import org.junit.Test; import org.junit.runner.RunWith; @@ -60,20 +59,15 @@ public void shouldCalculate() { final byte[] input = Bytes.fromHexString(this.input).toArrayUnsafe(); final byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; - final IntByReference outputLength = new IntByReference(); - final byte[] error = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_ERROR_BYTES]; - final IntByReference errorLength = new IntByReference(); - LibGnarkEIP196.eip196_perform_operation(LibGnarkEIP196.EIP196_ADD_OPERATION_RAW_VALUE, input, - input.length, output, outputLength, error, errorLength); + int errorCode = LibGnarkEIP196.eip196_perform_operation(LibGnarkEIP196.EIP196_ADD_OPERATION_RAW_VALUE, input, + input.length, output); final Bytes expectedComputation = expectedResult == null ? null : Bytes.fromHexString(expectedResult); - if (errorLength.getValue() > 0) { + if (errorCode != LibGnarkEIP196.EIP196_ERR_CODE_SUCCESS) { assertThat(notes).isNotEmpty(); - assertThat(new String(error, 0, errorLength.getValue(), UTF_8)).isEqualTo(notes); - assertThat(outputLength.getValue()).isZero(); } else { - final Bytes actualComputation = Bytes.wrap(output, 0, outputLength.getValue()); + final Bytes actualComputation = Bytes.wrap(output, 0, LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES); assertThat(actualComputation).isEqualTo(expectedComputation); assertThat(notes).isEmpty(); } diff --git a/gnark/src/test/java/org/hyperledger/besu/nativelib/gnark/AltBN128G1MulPrecompiledContractTest.java b/gnark/src/test/java/org/hyperledger/besu/nativelib/gnark/AltBN128G1MulPrecompiledContractTest.java index 7b6d1f89..a0f4bc46 100644 --- a/gnark/src/test/java/org/hyperledger/besu/nativelib/gnark/AltBN128G1MulPrecompiledContractTest.java +++ b/gnark/src/test/java/org/hyperledger/besu/nativelib/gnark/AltBN128G1MulPrecompiledContractTest.java @@ -16,7 +16,6 @@ package org.hyperledger.besu.nativelib.gnark; import com.google.common.io.CharStreams; -import com.sun.jna.ptr.IntByReference; import org.apache.tuweni.bytes.Bytes; import org.junit.Test; import org.junit.runner.RunWith; @@ -60,19 +59,15 @@ public void shouldCalculate() { final byte[] input = Bytes.fromHexString(this.input).toArrayUnsafe(); final byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; - final IntByReference outputLength = new IntByReference(); - final byte[] error = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_ERROR_BYTES]; - final IntByReference errorLength = new IntByReference(); - LibGnarkEIP196.eip196_perform_operation(LibGnarkEIP196.EIP196_MUL_OPERATION_RAW_VALUE, input, - input.length, output, outputLength, error, errorLength); + int errorCode = LibGnarkEIP196.eip196_perform_operation(LibGnarkEIP196.EIP196_MUL_OPERATION_RAW_VALUE, input, + input.length, output); final Bytes expectedComputation = expectedResult == null ? null : Bytes.fromHexString(expectedResult); - if (errorLength.getValue() > 0) { - assertThat(new String(error, 0, errorLength.getValue(), UTF_8)).contains(notes); - assertThat(outputLength.getValue()).isZero(); + if (errorCode != LibGnarkEIP196.EIP196_ERR_CODE_SUCCESS) { + assertThat(notes).isNotEmpty(); } else { - final Bytes actualComputation = Bytes.wrap(output, 0, outputLength.getValue()); + final Bytes actualComputation = Bytes.wrap(output, 0, LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES); assertThat(actualComputation).isEqualTo(expectedComputation); assertThat(notes).isEmpty(); } diff --git a/gnark/src/test/java/org/hyperledger/besu/nativelib/gnark/AltBN128PairingPrecompiledContractLegacyTest.java b/gnark/src/test/java/org/hyperledger/besu/nativelib/gnark/AltBN128PairingPrecompiledContractLegacyTest.java index 17a6d7c5..3a8902c0 100644 --- a/gnark/src/test/java/org/hyperledger/besu/nativelib/gnark/AltBN128PairingPrecompiledContractLegacyTest.java +++ b/gnark/src/test/java/org/hyperledger/besu/nativelib/gnark/AltBN128PairingPrecompiledContractLegacyTest.java @@ -17,9 +17,7 @@ import static org.assertj.core.api.Assertions.assertThat; -import com.sun.jna.ptr.IntByReference; import org.apache.tuweni.bytes.Bytes; -import org.junit.Ignore; import org.junit.Test; public class AltBN128PairingPrecompiledContractLegacyTest { @@ -61,21 +59,15 @@ public void compute_validPoints() { final byte[] input = Bytes.concatenate(g1Point0, g2Point0, g1Point1, g2Point1).toArrayUnsafe(); final byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; - final IntByReference outputLength = new IntByReference(); - final byte[] error = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_ERROR_BYTES]; - final IntByReference errorLength = new IntByReference(); - int ret = LibGnarkEIP196.eip196_perform_operation( + int errorCode = LibGnarkEIP196.eip196_perform_operation( LibGnarkEIP196.EIP196_PAIR_OPERATION_RAW_VALUE, input, input.length, - output, - outputLength, - error, - errorLength); + output); - assertThat(ret).isEqualTo(0); - assertThat(output[outputLength.getValue() - 1]).isEqualTo((byte) 1); + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_SUCCESS); + assertThat(output[31]).isEqualTo((byte) 1); } @Test @@ -115,24 +107,14 @@ public void compute_invalidPointsOutsideSubgroupG2() { final byte[] input = Bytes.concatenate(g1Point0, g2Point0, g1Point1, g2Point1).toArrayUnsafe(); final byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; - final IntByReference outputLength = new IntByReference(output.length); - final byte[] error = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_ERROR_BYTES]; - final IntByReference errorLength = new IntByReference(); - LibGnarkEIP196.eip196_perform_operation( + int errorCode = LibGnarkEIP196.eip196_perform_operation( LibGnarkEIP196.EIP196_PAIR_OPERATION_RAW_VALUE, input, input.length, - output, - outputLength, - error, - errorLength); + output); // assert there is an error - assertThat(errorLength.getValue()).isNotEqualTo(0); - String errorStr = new String(error, 0, errorLength.getValue()); - assertThat(errorStr).isEqualTo("invalid input parameters, point is not in subgroup"); - // assert there is no output - assertThat(outputLength.getValue()).isEqualTo(0); + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_POINT_IN_SUBGROUP_CHECK_FAILED); } } diff --git a/gnark/src/test/java/org/hyperledger/besu/nativelib/gnark/AltBN128PairingPrecompiledContractTest.java b/gnark/src/test/java/org/hyperledger/besu/nativelib/gnark/AltBN128PairingPrecompiledContractTest.java index 9d445339..aac7e37e 100644 --- a/gnark/src/test/java/org/hyperledger/besu/nativelib/gnark/AltBN128PairingPrecompiledContractTest.java +++ b/gnark/src/test/java/org/hyperledger/besu/nativelib/gnark/AltBN128PairingPrecompiledContractTest.java @@ -16,7 +16,6 @@ package org.hyperledger.besu.nativelib.gnark; import com.google.common.io.CharStreams; -import com.sun.jna.ptr.IntByReference; import org.apache.tuweni.bytes.Bytes; import org.junit.Test; import org.junit.runner.RunWith; @@ -59,27 +58,20 @@ public void shouldCalculate() { } final byte[] input = Bytes.fromHexString(this.input).toArrayUnsafe(); - final byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; - final IntByReference outputLength = new IntByReference(); - final byte[] error = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_ERROR_BYTES]; - final IntByReference errorLength = new IntByReference(); + final byte[] output = new byte[LibGnarkEIP196.EIP196_PAIR_PREALLOCATE_FOR_RESULT_BYTES]; - LibGnarkEIP196.eip196_perform_operation( + int errorCode = LibGnarkEIP196.eip196_perform_operation( LibGnarkEIP196.EIP196_PAIR_OPERATION_RAW_VALUE, input, input.length, - output, - outputLength, - error, - errorLength); + output); final Bytes expectedComputation = expectedResult == null ? null : Bytes.fromHexString(expectedResult); - if (errorLength.getValue() > 0) { - assertThat(new String(error, 0, errorLength.getValue(), UTF_8)).isEqualTo(notes); - assertThat(outputLength.getValue()).isZero(); + if (errorCode != LibGnarkEIP196.EIP196_ERR_CODE_SUCCESS) { + assertThat(notes).isNotEmpty(); } else { - final Bytes actualComputation = Bytes.wrap(output, 0, outputLength.getValue()); + final Bytes actualComputation = Bytes.wrap(output, 0, 32); assertThat(actualComputation).isEqualTo(expectedComputation); assertThat(notes).isEmpty(); } diff --git a/gnark/src/test/java/org/hyperledger/besu/nativelib/gnark/LibGnarkEIP196ConcurrentTest.java b/gnark/src/test/java/org/hyperledger/besu/nativelib/gnark/LibGnarkEIP196ConcurrentTest.java new file mode 100644 index 00000000..6cd5107c --- /dev/null +++ b/gnark/src/test/java/org/hyperledger/besu/nativelib/gnark/LibGnarkEIP196ConcurrentTest.java @@ -0,0 +1,257 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ +package org.hyperledger.besu.nativelib.gnark; + +import org.apache.tuweni.bytes.Bytes; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +public class LibGnarkEIP196ConcurrentTest { + + // Valid G1 point for testing + private static final Bytes G1_POINT_1 = Bytes.concatenate( + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000001"), + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000002")); + + // Another valid G1 point + private static final Bytes G1_POINT_2 = Bytes.concatenate( + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000001"), + Bytes.fromHexString("0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd45")); + + // Valid G2 point for testing + private static final Bytes G2_POINT = Bytes.concatenate( + Bytes.fromHexString("0x198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2"), + Bytes.fromHexString("0x1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed"), + Bytes.fromHexString("0x090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b"), + Bytes.fromHexString("0x12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa")); + + // Scalar for multiplication + private static final Bytes SCALAR = Bytes.fromHexString( + "0x0000000000000000000000000000000000000000000000000000000000000009"); + + @Test + public void testConcurrentG1Add() throws Exception { + final int threadCount = 10; + final int operationsPerThread = 100; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + List> futures = new ArrayList<>(); + + byte[] inputBytes = Bytes.concatenate(G1_POINT_1, G1_POINT_2).toArrayUnsafe(); + + for (int i = 0; i < threadCount; i++) { + futures.add(executor.submit(() -> { + for (int j = 0; j < operationsPerThread; j++) { + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_ADD_OPERATION_RAW_VALUE, + inputBytes, + inputBytes.length, + output); + + if (errorCode != LibGnarkEIP196.EIP196_ERR_CODE_SUCCESS) { + return false; + } + } + return true; + })); + } + + executor.shutdown(); + assertThat(executor.awaitTermination(30, TimeUnit.SECONDS)).isTrue(); + + for (Future future : futures) { + assertThat(future.get()).isTrue(); + } + } + + @Test + public void testConcurrentG1Mul() throws Exception { + final int threadCount = 10; + final int operationsPerThread = 100; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + List> futures = new ArrayList<>(); + + byte[] inputBytes = Bytes.concatenate(G1_POINT_1, SCALAR).toArrayUnsafe(); + + for (int i = 0; i < threadCount; i++) { + futures.add(executor.submit(() -> { + for (int j = 0; j < operationsPerThread; j++) { + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_MUL_OPERATION_RAW_VALUE, + inputBytes, + inputBytes.length, + output); + + if (errorCode != LibGnarkEIP196.EIP196_ERR_CODE_SUCCESS) { + return false; + } + } + return true; + })); + } + + executor.shutdown(); + assertThat(executor.awaitTermination(30, TimeUnit.SECONDS)).isTrue(); + + for (Future future : futures) { + assertThat(future.get()).isTrue(); + } + } + + @Test + public void testConcurrentPairing() throws Exception { + final int threadCount = 10; + final int operationsPerThread = 50; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + List> futures = new ArrayList<>(); + + byte[] inputBytes = Bytes.concatenate(G1_POINT_1, G2_POINT).toArrayUnsafe(); + + for (int i = 0; i < threadCount; i++) { + futures.add(executor.submit(() -> { + for (int j = 0; j < operationsPerThread; j++) { + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_PAIR_OPERATION_RAW_VALUE, + inputBytes, + inputBytes.length, + output); + + if (errorCode != LibGnarkEIP196.EIP196_ERR_CODE_SUCCESS) { + return false; + } + } + return true; + })); + } + + executor.shutdown(); + assertThat(executor.awaitTermination(60, TimeUnit.SECONDS)).isTrue(); + + for (Future future : futures) { + assertThat(future.get()).isTrue(); + } + } + + @Test + public void testMixedConcurrentOperations() throws Exception { + final int threadCount = 15; + final int operationsPerThread = 50; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + List> futures = new ArrayList<>(); + + byte[] addInput = Bytes.concatenate(G1_POINT_1, G1_POINT_2).toArrayUnsafe(); + byte[] mulInput = Bytes.concatenate(G1_POINT_1, SCALAR).toArrayUnsafe(); + byte[] pairingInput = Bytes.concatenate(G1_POINT_1, G2_POINT).toArrayUnsafe(); + + // Mix of different operations + for (int i = 0; i < threadCount; i++) { + final int threadId = i; + futures.add(executor.submit(() -> { + for (int j = 0; j < operationsPerThread; j++) { + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + int errorCode; + + // Different threads do different operations + if (threadId % 3 == 0) { + errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_ADD_OPERATION_RAW_VALUE, + addInput, + addInput.length, + output); + } else if (threadId % 3 == 1) { + errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_MUL_OPERATION_RAW_VALUE, + mulInput, + mulInput.length, + output); + } else { + errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_PAIR_OPERATION_RAW_VALUE, + pairingInput, + pairingInput.length, + output); + } + + if (errorCode != LibGnarkEIP196.EIP196_ERR_CODE_SUCCESS) { + return false; + } + } + return true; + })); + } + + executor.shutdown(); + assertThat(executor.awaitTermination(60, TimeUnit.SECONDS)).isTrue(); + + for (Future future : futures) { + assertThat(future.get()).isTrue(); + } + } + + @Test + public void testConcurrentWithErrors() throws Exception { + final int threadCount = 10; + final int operationsPerThread = 50; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + List> futures = new ArrayList<>(); + + // Invalid point (not on curve) + byte[] invalidInput = Bytes.fromHexString("0x1234").toArrayUnsafe(); + byte[] validInput = Bytes.concatenate(G1_POINT_1, G1_POINT_2).toArrayUnsafe(); + + for (int i = 0; i < threadCount; i++) { + final int threadId = i; + futures.add(executor.submit(() -> { + for (int j = 0; j < operationsPerThread; j++) { + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + byte[] input = (threadId % 2 == 0) ? validInput : invalidInput; + int expectedError = (threadId % 2 == 0) ? + LibGnarkEIP196.EIP196_ERR_CODE_SUCCESS : + LibGnarkEIP196.EIP196_ERR_CODE_POINT_ON_CURVE_CHECK_FAILED; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_ADD_OPERATION_RAW_VALUE, + input, + input.length, + output); + + if (errorCode != expectedError) { + return false; + } + } + return true; + })); + } + + executor.shutdown(); + assertThat(executor.awaitTermination(30, TimeUnit.SECONDS)).isTrue(); + + for (Future future : futures) { + assertThat(future.get()).isTrue(); + } + } +} diff --git a/gnark/src/test/java/org/hyperledger/besu/nativelib/gnark/LibGnarkEIP196EdgeCaseTest.java b/gnark/src/test/java/org/hyperledger/besu/nativelib/gnark/LibGnarkEIP196EdgeCaseTest.java new file mode 100644 index 00000000..412201b0 --- /dev/null +++ b/gnark/src/test/java/org/hyperledger/besu/nativelib/gnark/LibGnarkEIP196EdgeCaseTest.java @@ -0,0 +1,464 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ +package org.hyperledger.besu.nativelib.gnark; + +import org.apache.tuweni.bytes.Bytes; +import org.junit.Test; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; + +public class LibGnarkEIP196EdgeCaseTest { + + private static final byte GARBAGE_BYTE = (byte) 0xFF; + + @Test + public void testG1AddEmptyInput() { + byte[] input = new byte[0]; + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_ADD_OPERATION_RAW_VALUE, + input, + input.length, + output); + + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_SUCCESS); + // Empty input should return zero point + assertThat(output).isEqualTo(new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]); + } + + @Test + public void testG1AddPartialFirstPoint() { + // Only 32 bytes (half a point) + byte[] input = new byte[32]; + Arrays.fill(input, (byte) 0); + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_ADD_OPERATION_RAW_VALUE, + input, + input.length, + output); + + // Should pad with zeros and succeed (zero point is valid) + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_SUCCESS); + } + + @Test + public void testG1AddPartialSecondPoint() { + // Valid first point, partial second point + Bytes firstPoint = Bytes.concatenate( + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000001"), + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000002")); + + // Only 96 bytes total (full first point + half second point) + byte[] input = new byte[96]; + firstPoint.toArrayUnsafe(); + System.arraycopy(firstPoint.toArrayUnsafe(), 0, input, 0, 64); + + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_ADD_OPERATION_RAW_VALUE, + input, + input.length, + output); + + // Should pad second point with zeros and return first point + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_SUCCESS); + assertThat(Bytes.wrap(output, 0, 64)).isEqualTo(firstPoint); + } + + @Test + public void testG1AddTruncatedInput() { + // Valid first point + partial second point (only X coordinate) + Bytes firstPoint = Bytes.concatenate( + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000001"), + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000002")); + Bytes secondPointX = Bytes.fromHexString( + "0x0000000000000000000000000000000000000000000000000000000000000001"); + + byte[] input = Bytes.concatenate(firstPoint, secondPointX).toArrayUnsafe(); + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_ADD_OPERATION_RAW_VALUE, + input, + input.length, + output); + + // Should pad Y coordinate with zeros - results in invalid point + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_POINT_ON_CURVE_CHECK_FAILED); + } + + @Test + public void testG1MulEmptyInput() { + byte[] input = new byte[0]; + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_MUL_OPERATION_RAW_VALUE, + input, + input.length, + output); + + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_SUCCESS); + // Empty input should return zero + assertThat(output).isEqualTo(new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]); + } + + @Test + public void testG1MulPointOnlyNoScalar() { + // Valid point but no scalar + byte[] input = Bytes.concatenate( + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000001"), + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000002") + ).toArrayUnsafe(); + + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_MUL_OPERATION_RAW_VALUE, + input, + input.length, + output); + + // No scalar means multiply by 0, should return zero point + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_SUCCESS); + assertThat(output).isEqualTo(new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]); + } + + @Test + public void testG1MulPartialScalar() { + // Valid point + partial scalar (only 16 bytes instead of 32) + Bytes point = Bytes.concatenate( + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000001"), + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000002")); + Bytes partialScalar = Bytes.fromHexString("0x00000000000000000000000000000009"); + + byte[] input = Bytes.concatenate(point, partialScalar).toArrayUnsafe(); + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_MUL_OPERATION_RAW_VALUE, + input, + input.length, + output); + + // Should pad scalar with zeros and succeed + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_SUCCESS); + } + + @Test + public void testG1MulInfinityPoint() { + // Zero point (point at infinity) with non-zero scalar + byte[] input = new byte[96]; + Arrays.fill(input, (byte) 0); + input[95] = 0x09; // scalar = 9 + + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_MUL_OPERATION_RAW_VALUE, + input, + input.length, + output); + + // Multiplying infinity by any scalar should return infinity (zero) + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_SUCCESS); + assertThat(output).isEqualTo(new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]); + } + + @Test + public void testG1MulByTwo() { + // Test special case optimization for scalar = 2 + Bytes point = Bytes.concatenate( + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000001"), + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000002")); + Bytes scalarTwo = Bytes.fromHexString( + "0x0000000000000000000000000000000000000000000000000000000000000002"); + + byte[] input = Bytes.concatenate(point, scalarTwo).toArrayUnsafe(); + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_MUL_OPERATION_RAW_VALUE, + input, + input.length, + output); + + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_SUCCESS); + // Result should be non-zero (doubled point) + assertThat(output).isNotEqualTo(new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]); + } + + @Test + public void testPairingEmptyInput() { + byte[] input = new byte[0]; + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_PAIR_OPERATION_RAW_VALUE, + input, + input.length, + output); + + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_SUCCESS); + // Empty input should return 1 (pairing succeeded) + assertThat(output[31]).isEqualTo((byte) 0x01); + } + + @Test + public void testPairingInvalidLength() { + // Not a multiple of 192 + byte[] input = new byte[100]; + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_PAIR_OPERATION_RAW_VALUE, + input, + input.length, + output); + + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_INVALID_INPUT_PAIRING_LENGTH); + } + + @Test + public void testPairingMultiplePairs() { + // Two pairs + Bytes g1Point = Bytes.concatenate( + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000001"), + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000002")); + + Bytes g2Point = Bytes.concatenate( + Bytes.fromHexString("0x198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2"), + Bytes.fromHexString("0x1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed"), + Bytes.fromHexString("0x090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b"), + Bytes.fromHexString("0x12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa")); + + byte[] input = Bytes.concatenate(g1Point, g2Point, g1Point, g2Point).toArrayUnsafe(); + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_PAIR_OPERATION_RAW_VALUE, + input, + input.length, + output); + + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_SUCCESS); + } + + @Test + public void testOutputBufferInitializationPairingWritesResult() { + // Test that output buffer is properly written by Go code (not just relying on Java initialization) + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + Arrays.fill(output, GARBAGE_BYTE); // Fill with garbage to ensure Go writes the result + + // Valid pairing from test data + Bytes g1Point = Bytes.concatenate( + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000001"), + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000002")); + + Bytes g2Point = Bytes.concatenate( + Bytes.fromHexString("0x198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2"), + Bytes.fromHexString("0x1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed"), + Bytes.fromHexString("0x090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b"), + Bytes.fromHexString("0x12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa")); + + Bytes g1Point2 = Bytes.concatenate( + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000001"), + Bytes.fromHexString("0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd45")); + + byte[] input = Bytes.concatenate(g1Point, g2Point, g1Point2, g2Point).toArrayUnsafe(); + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_PAIR_OPERATION_RAW_VALUE, + input, + input.length, + output); + + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_SUCCESS); + // The key test: byte 31 should have been written by Go code (either 0x00 or 0x01, not 0xFF) + assertThat(output[31]).isNotEqualTo(GARBAGE_BYTE); + assertThat(output[31]).isIn((byte) 0x00, (byte) 0x01); + // All other bytes should remain 0xFF + for (int i = 0; i < 31; i++) { + assertThat(output[i]).isEqualTo(GARBAGE_BYTE); + } + for (int i = 32; i < LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES; i++) { + assertThat(output[i]).isEqualTo(GARBAGE_BYTE); + } + } + + @Test + public void testPairingEmptyInputWritesOne() { + // Initialized with zeros + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_PAIR_OPERATION_RAW_VALUE, + new byte[0], + 0, + output); + + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_SUCCESS); + // Empty input should write 0x01 to byte 31 + assertThat(output[31]).isEqualTo((byte) 0x01); + // All other bytes should remain 0 + for (int i = 0; i < 31; i++) { + assertThat(output[i]).isEqualTo((byte) 0x00); + } + for (int i = 32; i < LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES; i++) { + assertThat(output[i]).isEqualTo((byte) 0x00); + } + } + + @Test + public void testPointNotInField() { + // Point with coordinates >= field modulus + byte[] input = Bytes.fromHexString( + "0xff00000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000" + ).toArrayUnsafe(); + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_ADD_OPERATION_RAW_VALUE, + input, + input.length, + output); + + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_POINT_NOT_IN_FIELD); + } + + @Test + public void testPointNotOnCurve() { + // Valid field elements but not on curve + byte[] input = Bytes.fromHexString("0x1234").toArrayUnsafe(); + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_ADD_OPERATION_RAW_VALUE, + input, + input.length, + output); + + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_POINT_ON_CURVE_CHECK_FAILED); + } + + @Test + public void testOutputBufferExactSize() { + byte[] input = Bytes.EMPTY.toArrayUnsafe(); + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_ADD_OPERATION_RAW_VALUE, + input, + input.length, + output); + + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_SUCCESS); + } + + @Test + public void testOutputBufferOversizedIsAllowed() { + final int oversizedOutputLength = 128; + byte[] input = Bytes.fromHexString( + "0x1b43c36e6eb9566ae50c78f79802a80a963cf4317079c9e201361b1afbd64d2b" + + "2796ffa55aaf5946e40b8038cda20238c53f328d5e4150287564eb08ce1efa59" + + "0d4d3c9a95606d838edb0b494a64d5ad5933335cd7acf5ee9409492135b44fed" + + "2d02a128bfb4cb61db495d5389feba0c4943e8c8c936acf7231fc8edefb619b5" + ).toArrayUnsafe(); + + byte[] output = new byte[oversizedOutputLength]; // Oversized - should work fine + Arrays.fill(output, GARBAGE_BYTE); // Fill with garbage + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_ADD_OPERATION_RAW_VALUE, + input, + input.length, + output); + + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_SUCCESS); + Bytes expectedResult = Bytes.fromHexString( + "0x29a99e2465491ddb57b131ea6d0469f4faaa4202bf870b3e82365b9c3a59cf82" + + "27d26a8780d9d0c30452f13d9673e2d9cae926a2158b62f8cf4804adaa112e98"); + assertThat(Bytes.wrap(output, 0, 64)).isEqualTo(expectedResult); + // Bytes 64+ remain untouched + for (int i = 64; i < oversizedOutputLength; i++) { + assertThat(output[i]).isEqualTo(GARBAGE_BYTE); + } + } + + @Test + public void testAddOutputBufferUndersizedReturnsError() { + int undersizedOutputLength = LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES - 1; + byte[] input = Bytes.EMPTY.toArrayUnsafe(); + byte[] output = new byte[undersizedOutputLength]; + + final int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_ADD_OPERATION_RAW_VALUE, + input, + input.length, + output); + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_INVALID_OUTPUT_LENGTH); + + // Output should remain all zeros + for (final byte b : output) { + assertThat(b).isEqualTo((byte) 0x00); + } + } + + @Test + public void testMulOutputBufferUndersizedReturnsError() { + int undersizedOutputLength = LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES - 1; + byte[] input = Bytes.EMPTY.toArrayUnsafe(); + byte[] output = new byte[undersizedOutputLength]; + + final int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_MUL_OPERATION_RAW_VALUE, + input, + input.length, + output); + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_INVALID_OUTPUT_LENGTH); + + // Output should remain all zeros + for (final byte b : output) { + assertThat(b).isEqualTo((byte) 0x00); + } + } + + @Test + public void testPairingOutputBufferUndersizedReturnsError() { + int undersizedOutputLength = LibGnarkEIP196.EIP196_PAIR_PREALLOCATE_FOR_RESULT_BYTES - 1; + byte[] input = Bytes.EMPTY.toArrayUnsafe(); + byte[] output = new byte[undersizedOutputLength]; + + final int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_PAIR_OPERATION_RAW_VALUE, + input, + input.length, + output); + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_INVALID_OUTPUT_LENGTH); + + // Output should remain all zeros + for (final byte b : output) { + assertThat(b).isEqualTo((byte) 0x00); + } + } +} diff --git a/gnark/src/test/java/org/hyperledger/besu/nativelib/gnark/LibGnarkEIP196ErrorCodeTest.java b/gnark/src/test/java/org/hyperledger/besu/nativelib/gnark/LibGnarkEIP196ErrorCodeTest.java new file mode 100644 index 00000000..fc74ec54 --- /dev/null +++ b/gnark/src/test/java/org/hyperledger/besu/nativelib/gnark/LibGnarkEIP196ErrorCodeTest.java @@ -0,0 +1,257 @@ +/* + * Copyright contributors to Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ +package org.hyperledger.besu.nativelib.gnark; + +import org.apache.tuweni.bytes.Bytes; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for verifying specific error codes returned by EIP-196 operations. + */ +public class LibGnarkEIP196ErrorCodeTest { + + @Test + public void testErrorCodeSuccess() { + Bytes g1Point1 = Bytes.concatenate( + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000001"), + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000002")); + Bytes g1Point2 = Bytes.concatenate( + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000001"), + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000002")); + + byte[] input = Bytes.concatenate(g1Point1, g1Point2).toArrayUnsafe(); + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_ADD_OPERATION_RAW_VALUE, + input, + input.length, + output); + + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_SUCCESS); + } + + @Test + public void testErrorCodePointNotInField() { + // Point with X coordinate >= field modulus + // bn254 field modulus is 21888242871839275222246405745257275088696311157297823662689037894645226208583 + // which is 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47 + // So 0xff00...00 is definitely out of field + byte[] input = Bytes.concatenate( + Bytes.fromHexString("0xff00000000000000000000000000000000000000000000000000000000000000"), + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000000") + ).toArrayUnsafe(); + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_ADD_OPERATION_RAW_VALUE, + input, + input.length, + output); + + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_POINT_NOT_IN_FIELD); + } + + @Test + public void testErrorCodePointNotInFieldYCoordinate() { + // Valid X coordinate but Y coordinate out of field + byte[] input = Bytes.concatenate( + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000001"), + Bytes.fromHexString("0xff00000000000000000000000000000000000000000000000000000000000000") + ).toArrayUnsafe(); + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_ADD_OPERATION_RAW_VALUE, + input, + input.length, + output); + + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_POINT_NOT_IN_FIELD); + } + + @Test + public void testErrorCodePointOnCurveCheckFailed() { + // Valid field elements but not satisfying curve equation y^2 = x^3 + 3 + byte[] input = Bytes.fromHexString("0x1234").toArrayUnsafe(); + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_ADD_OPERATION_RAW_VALUE, + input, + input.length, + output); + + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_POINT_ON_CURVE_CHECK_FAILED); + } + + @Test + public void testErrorCodePointOnCurveCheckFailedForMul() { + // Invalid point for multiplication + byte[] input = Bytes.concatenate( + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000001"), + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000000"), + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000009") + ).toArrayUnsafe(); + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_MUL_OPERATION_RAW_VALUE, + input, + input.length, + output); + + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_POINT_ON_CURVE_CHECK_FAILED); + } + + @Test + public void testErrorCodeInvalidInputPairingLength() { + // Input length not a multiple of 192 (size of G1 + G2 pair) + byte[] input = new byte[100]; + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_PAIR_OPERATION_RAW_VALUE, + input, + input.length, + output); + + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_INVALID_INPUT_PAIRING_LENGTH); + } + + @Test + public void testErrorCodeInvalidInputPairingLength191() { + // Just one byte short of a valid pair + byte[] input = new byte[191]; + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_PAIR_OPERATION_RAW_VALUE, + input, + input.length, + output); + + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_INVALID_INPUT_PAIRING_LENGTH); + } + + @Test + public void testErrorCodePointInSubgroupCheckFailed() { + // G2 point that is on the curve but not in the correct subgroup + // This is a known test vector + Bytes g1Point = Bytes.concatenate( + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000001"), + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000002")); + + // G2 point on curve but not in subgroup + Bytes g2PointNotInSubgroup = Bytes.concatenate( + Bytes.fromHexString("0x1382cd45e5674247f9c900b5c6f6cabbc189c2fabe2df0bf5acd84c97818f508"), + Bytes.fromHexString("0x1246178655ab8f2f26956b189894b7eb93cd4215b9937e7969e44305f80f521e"), + Bytes.fromHexString("0x08331c0a261a74e7e75db1232956663cbc88110f726159c5cba1857ecd03fa64"), + Bytes.fromHexString("0x1fbf8045ce3e79b5cde4112d38bcd0efbdb1295d2eefdf58151ae309d7ded7db")); + + byte[] input = Bytes.concatenate(g1Point, g2PointNotInSubgroup).toArrayUnsafe(); + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_PAIR_OPERATION_RAW_VALUE, + input, + input.length, + output); + + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_POINT_IN_SUBGROUP_CHECK_FAILED); + } + + @Test + public void testErrorCodePointOnCurveCheckFailedForG2() { + // Invalid G2 point in pairing + Bytes g1Point = Bytes.concatenate( + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000001"), + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000002")); + + // G2 point not on curve (using invalid coordinates) + Bytes invalidG2Point = Bytes.concatenate( + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000001"), + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000002"), + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000003"), + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000004")); + + byte[] input = Bytes.concatenate(g1Point, invalidG2Point).toArrayUnsafe(); + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_PAIR_OPERATION_RAW_VALUE, + input, + input.length, + output); + + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_POINT_ON_CURVE_CHECK_FAILED); + } + + @Test + public void testErrorCodeForInvalidG1InPairing() { + // Invalid G1 point in pairing (64 bytes but not on curve) + Bytes invalidG1Point = Bytes.concatenate( + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000001234"), + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000005678")); + + Bytes validG2Point = Bytes.concatenate( + Bytes.fromHexString("0x198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2"), + Bytes.fromHexString("0x1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed"), + Bytes.fromHexString("0x090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b"), + Bytes.fromHexString("0x12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa")); + + byte[] input = Bytes.concatenate(invalidG1Point, validG2Point).toArrayUnsafe(); + byte[] output = new byte[LibGnarkEIP196.EIP196_PREALLOCATE_FOR_RESULT_BYTES]; + + int errorCode = LibGnarkEIP196.eip196_perform_operation( + LibGnarkEIP196.EIP196_PAIR_OPERATION_RAW_VALUE, + input, + input.length, + output); + + assertThat(errorCode).isEqualTo(LibGnarkEIP196.EIP196_ERR_CODE_POINT_ON_CURVE_CHECK_FAILED); + } + + @Test + public void testAllErrorCodesAreDifferent() { + // Sanity check: ensure all error codes have unique values + int[] errorCodes = { + LibGnarkEIP196.EIP196_ERR_CODE_SUCCESS, + LibGnarkEIP196.EIP196_ERR_CODE_INVALID_INPUT_PAIRING_LENGTH, + LibGnarkEIP196.EIP196_ERR_CODE_POINT_NOT_IN_FIELD, + LibGnarkEIP196.EIP196_ERR_CODE_POINT_IN_SUBGROUP_CHECK_FAILED, + LibGnarkEIP196.EIP196_ERR_CODE_POINT_ON_CURVE_CHECK_FAILED, + LibGnarkEIP196.EIP196_ERR_CODE_PAIRING_CHECK_ERROR + }; + + // Check all values are unique + for (int i = 0; i < errorCodes.length; i++) { + for (int j = i + 1; j < errorCodes.length; j++) { + assertThat(errorCodes[i]).isNotEqualTo(errorCodes[j]); + } + } + + // Check values are in expected range + assertThat(LibGnarkEIP196.EIP196_ERR_CODE_SUCCESS).isEqualTo(0); + assertThat(LibGnarkEIP196.EIP196_ERR_CODE_INVALID_INPUT_PAIRING_LENGTH).isEqualTo(1); + assertThat(LibGnarkEIP196.EIP196_ERR_CODE_POINT_NOT_IN_FIELD).isEqualTo(2); + assertThat(LibGnarkEIP196.EIP196_ERR_CODE_POINT_IN_SUBGROUP_CHECK_FAILED).isEqualTo(3); + assertThat(LibGnarkEIP196.EIP196_ERR_CODE_POINT_ON_CURVE_CHECK_FAILED).isEqualTo(4); + assertThat(LibGnarkEIP196.EIP196_ERR_CODE_PAIRING_CHECK_ERROR).isEqualTo(5); + } +}