❓ C++ BN254 Pairing Check Fails, Equivalent Solidity Precompile Succeeds - Why?

by ADMIN 80 views

🔍 Introduction

In the world of cryptography and blockchain development, pairing checks are a crucial component of many cryptographic protocols. These checks involve verifying the relationship between two points on an elliptic curve, which is essential for various cryptographic applications. In this article, we will delve into a specific issue where a C++ implementation of a pairing check fails, while an equivalent Solidity precompile succeeds. We will explore the possible reasons behind this discrepancy and provide insights into debugging and resolving such issues.

🔍 C++ Unit Test (gtest)

The C++ unit test in question uses the Google Test framework to verify the correctness of the pairing_check() function. This function takes a vector of pairs of points as input and returns a boolean value indicating whether the pairing check was successful. The test case in question uses two pairs of points, p1 and q1, as well as p2 and q2, and passes them to the pairing_check() function.

#include "evmone_precompiles/bn254.hpp" 
#include <intx/intx.hpp>
#include <vector>

using namespace evmmax::bn254;
using namespace intx;
using namespace intx::literals;

TEST(evmmax_bn254, SpecificVectorCheck_MatchesSolidity) {
    // Pair 1
    const auto p1 = Point{
        0x2cf44499d5d27bb186308b7af7af02ac5bc9eeb6a3d147c186b21fb1b76e18da_u256,
        0x2c0f001f52110ccfe69108924926e45f0b0c868df0e7bde1fe16d3242dc715f6_u256 };
    const auto q1 = ExtPoint{
        {0x1fb19bb476f6b9e44e2a32234da8212f61cd63919354bc06aef31e3cfaff3ebc_u256,
         0x22606845ff186793914e03e21df544c34ffe2f2f3504de8a79d9159eca2d98d9_u256},
        {0x2bd368e28381e8eccb5fa81fc26cf3f048eea9abfdd85d7ed3ab3698d63e4f90_u256,
         0x2fe02e47887507adf0ff1743cbac6ba291e66f59be6bd763950bb16041a0a85e_u256} };

    // Pair 2
    const auto p2 = Point{
        0x0000000000000000000000000000000000000000000000000000000000000001_u256,
        0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd45_u256 };
    const auto q2 = ExtPoint{
        {0x1971ff0471b09fa93caaf13cbf443c1aede09cc4328f5a62aad45f40ec133eb4_u256,
         0x091058a3141822985733cbdddfed0fd8d6c104e9eeff40bf5abfef9ab163bc7_u256},
        {0x2a23af9a5ce2ba2796c1f4e453a370eb0af8c212d9dc9acd8fc02c2e907baea2_u256,
         0x23a8eb0b0996252cb548a4487da97b02422ebc0e834613f954de6c7e0afdc1fc_u256} };

    const std::vector<std::pair<Point, ExtPoint>> pairs = {{p1, q1}, {p2, q2}};
    const auto result = pairing_check(pairs);

    ASSERT_TRUE(result.has_value()) << "Pairing check should return a value.";
    EXPECT_EQ(result.value(), true) << "Pairing result should be true, matching Solidity.";
}

✅ Solidity Equivalent (Returns 1)

The Solidity code in question uses the call() function to invoke the BN254 pairing precompile at address 0x08. The input data is passed as a memory array, and the success variable is used to store the result of the pairing check.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

contract PairingExpample {
    function run() public returns (uint) {
        uint256[12] memory input;
        input[0] = 0x2cf44499d5d27bb186308b7af7af02ac5bc9eeb6a3d147c186b21fb1b76e18da;
        input[1] = 0x2c0f001f52110ccfe69108924926e45f0b0c868df0e7bde1fe16d3242dc715f6;
        input[2] = 0x1fb19bb476f6b9e44e2a32234da8212f61cd63919354bc06aef31e3cfaff3ebc;
        input[3] = 0x22606845ff186793914e03e21df544c34ffe2f2f3504de8a79d9159eca2d98d9;
        input[4] = 0x2bd368e28381e8eccb5fa81fc26cf3f048eea9abfdd85d7ed3ab3698d63e4f90;
        input[5] = 0x2fe02e47887507adf0ff1743cbac6ba291e66f59be6bd763950bb16041a0a85e;
        input[6] = 0x0000000000000000000000000000000000000000000000000000000000000001;
        input[7] = 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd45;
        input[8] = 0x1971ff0471b09fa93caaf13cbf443c1aede09cc4328f5a62aad45f40ec133eb4;
        input[9] = 0x091058a3141822985733cbdddfed0fd8d6c104e9e9eff40bf5abfef9ab163bc7;
        input[10] = 0x2a23af9a5ce2ba6c1f4e453a370eb0af8c212d9dc9acd8fc02c2e907baea2;
        input[11] = 0x23a8eb0b0996252cb548a4487da97b02422ebc0e834613f954de6c7e0afdc1fc;

        uint success;
        assembly {
            success := call(gas(), 0x08, 0, input, 0x180, input, 0x20)
            if iszero(success) { revert(0, 0) }
        }

        return input[0];
    }
}

❓ Question

Why does the Solidity pairing check return true, while the C++ pairing_check() implementation returns false for the same input?

What could be causing this discrepancy?

  • Are the input points encoded or ordered differently?

Any help debugging this would be much appreciated 🙏

🔍 Possible Reasons for the Discrepancy

There are several possible reasons why the C++ pairing_check() implementation returns false while the Solidity pairing check returns true for the same input:

  1. Encoding differences: The input points may be encoded differently in the C++ and Solidity implementations. This could be due to differences in the encoding schemes used or the order in which the points are encoded.
  2. Ordering differences: The order in which the input points are passed to the pairing_check() function may be different in the C++ and Solidity implementations. This could affect the result of the pairing check.
  3. Data type differences: The data types used to represent the input points may be different in the C++ and Solidity implementations. This could affect the result of the pairing check.
  4. Implementation differences: The implementation of the pairing_check() function may be different in the C++ and Solidity implementations. This could affect the result of the pairing check.

🔍 Debugging the Issue

To debug this issue, we can follow these steps:

  1. Verify the input points: Verify that the input points are encoded correctly and in the correct order in both the C++ and Solidity implementations.
  2. Check the data types: Check that the data types used to represent the input points are the same in both the C++ and Solidity implementations.
  3. Compare the implementations: Compare the implementations of the pairing_check() function in both the C++ and Solidity implementations to identify any differences.
  4. Test the implementation: Test the implementation of

🔍 Q&A

Q: What is the BN254 pairing check?

A: The BN254 pairing check is a cryptographic operation that involves verifying the relationship between two points on an elliptic curve. It is a crucial component of many cryptographic protocols, including pairing-based cryptography.

Q: What is the difference between the C++ and Solidity implementations of the pairing check?

A: The C++ implementation of the pairing check uses the pairing_check() function, while the Solidity implementation uses the call() function to invoke the BN254 pairing precompile at address 0x08. The input data is passed as a memory array in both implementations.

Q: Why does the Solidity pairing check return true, while the C++ pairing_check() implementation returns false for the same input?

A: There are several possible reasons for this discrepancy, including:

  • Encoding differences: The input points may be encoded differently in the C++ and Solidity implementations.
  • Ordering differences: The order in which the input points are passed to the pairing_check() function may be different in the C++ and Solidity implementations.
  • Data type differences: The data types used to represent the input points may be different in the C++ and Solidity implementations.
  • Implementation differences: The implementation of the pairing_check() function may be different in the C++ and Solidity implementations.

Q: How can I debug this issue?

A: To debug this issue, you can follow these steps:

  • Verify the input points: Verify that the input points are encoded correctly and in the correct order in both the C++ and Solidity implementations.
  • Check the data types: Check that the data types used to represent the input points are the same in both the C++ and Solidity implementations.
  • Compare the implementations: Compare the implementations of the pairing_check() function in both the C++ and Solidity implementations to identify any differences.
  • Test the implementation: Test the implementation of the pairing_check() function in both the C++ and Solidity implementations to ensure that it is working correctly.

Q: What are some common pitfalls to avoid when implementing the pairing check?

A: Some common pitfalls to avoid when implementing the pairing check include:

  • Incorrect encoding of the input points
  • Incorrect ordering of the input points
  • Use of incorrect data types to represent the input points
  • Implementation differences between the C++ and Solidity implementations

Q: How can I ensure that my implementation of the pairing check is correct?

A: To ensure that your implementation of the pairing check is correct, you can:

  • Verify the input points: Verify that the input points are encoded correctly and in the correct order.
  • Check the data types: Check that the data types used to represent the input points are the same in both the C++ and Solidity implementations.
  • Compare the implementations: Compare the implementations of the pairing_check() function in both the C++ and Solidity implementations to identify any differences.
  • Test the implementation: Test the implementation of the pairing_check() function in both the C++ and Solidity implementations to ensure that it is working correctly.

🔍 Conclusion

In conclusion, the discrepancy between the C++ and Solidity implementations of the pairing check is likely due to differences in the encoding, ordering, or data types used to represent the input points. To debug this issue, you can follow the steps outlined above and ensure that your implementation of the pairing check is correct.