Skip to content

Commit

Permalink
BLS and P256 solidity examples
Browse files Browse the repository at this point in the history
  • Loading branch information
klkvr committed Sep 26, 2024
1 parent df04fc0 commit 825d42e
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 71 deletions.
30 changes: 6 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,11 @@ After that, make sure that your forge version is up to data (run `foundryup` if

## BLS library

Functions to allow calling each of the BLS precompiles defined in [EIP-2537]
Functions and data structures to allow calling each of the BLS precompiles defined in [EIP-2537]
without the low level details.

For example, this is how the library can be used from a solidity smart contract:
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import {BLS} from "/path/to/forge-alphanet/src/sign/BLS.sol";
contract BLSExample {
event OperationResult(bool success, bytes result);
We've prepared a simple test demonstrating BLS signing and verification in [test/BLS.t.sol](test/BLS.t.sol).

// Function to perform a BLS12-381 G1 addition with error handling
function performG1Add(bytes memory input) public {
(bool success, bytes memory output) = BLS.G1Add(input);
if (!success) {
emit OperationResult(false, "");
} else {
emit OperationResult(true, output);
}
}
}
```
## Secp256r1 library

Provides functionality to call the `P256VERIFY` precompile defined in [EIP-7212]
Expand All @@ -59,13 +39,15 @@ contract Secp256r1Example {
event OperationResult(bool success);
// Function to perform a Secp256r1 signature verification with error handling
function performP256Verify(bytes memory input) public {
bool result = Secp256r1.verify(input);
function performP256Verify(bytes32 digest, bytes32 r, bytes32 s, uint256 publicKeyX, uint256 publicKeyY) public {
bool result = Secp256r1.verify(digest, r, s, publicKeyX, publicKeyY);
emit OperationResult(result);
}
}
```

See an example of how to test secp256r1 signatures with foundry cheatcodes in [test/P256.t.sol](test/P256.t.sol).

## Account controlled by a P256 key

With EIP-7702 and EIP-7212 it is possible to delegate control over an EOA to a P256 key. This has large potential for UX improvement as P256 keys are adopted by commonly used protocols like [Apple Secure Enclave] and [WebAuthn].
Expand Down
154 changes: 108 additions & 46 deletions src/sign/BLS.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,86 +6,148 @@ pragma solidity ^0.8.23;
/// defined in EIP-2537, see <https://eips.ethereum.org/EIPS/eip-2537>.
/// @dev Precompile addresses come from the BLS addresses submodule in AlphaNet, see
/// <https://github.com/paradigmxyz/alphanet/blob/main/crates/precompile/src/addresses.rs>
/// Being addresses we can't use them as constants in inline assembly
library BLS {
/// @dev A base field element (Fp) is encoded as 64 bytes by performing the
/// BigEndian encoding of the corresponding (unsigned) integer. Due to the size of p,
/// the top 16 bytes are always zeroes.
struct Fp {
uint256 a;
uint256 b;
}

/// @dev For elements of the quadratic extension field (Fp2), encoding is byte concatenation of
/// individual encoding of the coefficients totaling in 128 bytes for a total encoding.
/// c0 + c1 * v
struct Fp2 {
Fp c0;
Fp c1;
}

/// @dev Points of G1 and G2 are encoded as byte concatenation of the respective
/// encodings of the x and y coordinates.
struct G1Point {
Fp x;
Fp y;
}

/// @dev Points of G1 and G2 are encoded as byte concatenation of the respective
/// encodings of the x and y coordinates.
struct G2Point {
Fp2 x;
Fp2 y;
}

/// @notice G1ADD operation
/// @param input Slice of bytes representing the input for the precompile operation
/// @return success Represents if the operation was successful
/// @return output Result bytes of the operation
function G1Add(bytes memory input) internal view returns (bool success, bytes memory output) {
/// @param a First G1 point
/// @param b Second G1 point
/// @return result Resulted G1 point
function G1Add(G1Point memory a, G1Point memory b) internal view returns (G1Point memory result) {
// G1ADD address is 0x0b
(success, output) = address(0x0b).staticcall(input);
(bool success, bytes memory output) = address(0x0b).staticcall(abi.encode(a, b));
require(success, "G1ADD failed");
return abi.decode(output, (G1Point));
}

/// @notice G1MUL operation
/// @param input Slice of bytes representing the input for the precompile operation
/// @return success Represents if the operation was successful
/// @return output Result bytes of the operation
function G1Mul(bytes memory input) internal view returns (bool success, bytes memory output) {
/// @param point G1 point
/// @param scalar Scalar to multiply the point by
/// @return result Resulted G1 point
function G1Mul(G1Point memory point, uint256 scalar) internal view returns (G1Point memory result) {
// G1MUL address is 0x0c
(success, output) = address(0x0c).staticcall(input);
(bool success, bytes memory output) = address(0x0c).staticcall(abi.encode(point, scalar));
require(success, "G1MUL failed");
return abi.decode(output, (G1Point));
}

/// @notice G1MSM operation
/// @param input Slice of bytes representing the input for the precompile operation
/// @return success Represents if the operation was successful
/// @return output Result bytes of the operation
function G1MSM(bytes memory input) internal view returns (bool success, bytes memory output) {
/// @param points Array of G1 points
/// @param scalars Array of scalars to multiply the points by
/// @return result Resulted G1 point
function G1MSM(G1Point[] memory points, uint256[] memory scalars) internal view returns (G1Point memory result) {
bytes memory input;

for (uint256 i = 0; i < points.length; i++) {
input = bytes.concat(input, abi.encode(points[i], scalars[i]));
}

// G1MSM address is 0x0d
(success, output) = address(0x0d).staticcall(input);
(bool success, bytes memory output) = address(0x0d).staticcall(input);
require(success, "G1MSM failed");
return abi.decode(output, (G1Point));
}

/// @notice G2ADD operation
/// @param input Slice of bytes representing the input for the precompile operation
/// @return success Represents if the operation was successful
/// @return output Result bytes of the operation
function G2Add(bytes memory input) internal view returns (bool success, bytes memory output) {
/// @param a First G2 point
/// @param b Second G2 point
/// @return result Resulted G2 point
function G2Add(G2Point memory a, G2Point memory b) internal view returns (G2Point memory result) {
// G2ADD address is 0x0e
(success, output) = address(0x0e).staticcall(input);
(bool success, bytes memory output) = address(0x0e).staticcall(abi.encode(a, b));
require(success, "G2ADD failed");
return abi.decode(output, (G2Point));
}

/// @notice G2MUL operation
/// @param input Slice of bytes representing the input for the precompile operation
/// @return success Represents if the operation was successful
/// @return output Result bytes of the operation
function G2Mul(bytes memory input) internal view returns (bool success, bytes memory output) {
/// @param point G2 point
/// @param scalar Scalar to multiply the point by
/// @return result Resulted G2 point
function G2Mul(G2Point memory point, uint256 scalar) internal view returns (G2Point memory result) {
// G2MUL address is 0x0f
(success, output) = address(0x0f).staticcall(input);
(bool success, bytes memory output) = address(0x0f).staticcall(abi.encode(point, scalar));
require(success, "G2MUL failed");
return abi.decode(output, (G2Point));
}

/// @notice G2MSM operation
/// @param input Slice of bytes representing the input for the precompile operation
/// @return success Represents if the operation was successful
/// @return output Result bytes of the operation
function G2MSM(bytes memory input) internal view returns (bool success, bytes memory output) {
/// @param points Array of G2 points
/// @param scalars Array of scalars to multiply the points by
/// @return result Resulted G2 point
function G2MSM(G2Point[] memory points, uint256[] memory scalars) internal view returns (G2Point memory result) {
bytes memory input;

for (uint256 i = 0; i < points.length; i++) {
input = bytes.concat(input, abi.encode(points[i], scalars[i]));
}

// G2MSM address is 0x10
(success, output) = address(0x10).staticcall(input);
(bool success, bytes memory output) = address(0x10).staticcall(input);
require(success, "G2MSM failed");
return abi.decode(output, (G2Point));
}

/// @notice PAIRING operation
/// @param input Slice of bytes representing the input for the precompile operation
/// @return success Represents if the operation was successful
/// @return output Result bytes of the operation
function Pairing(bytes memory input) internal view returns (bool success, bytes memory output) {
/// @param g1Points Array of G1 points
/// @param g2Points Array of G2 points
/// @return result Returns whether pairing result is equal to the multiplicative identity (1).
function Pairing(G1Point[] memory g1Points, G2Point[] memory g2Points) internal view returns (bool result) {
bytes memory input;
for (uint256 i = 0; i < g1Points.length; i++) {
input = bytes.concat(input, abi.encode(g1Points[i], g2Points[i]));
}

// PAIRING address is 0x11
(success, output) = address(0x11).staticcall(input);
(bool success, bytes memory output) = address(0x11).staticcall(input);
require(success, "Pairing failed");
return abi.decode(output, (bool));
}

/// @notice MAP_FP_TO_G1 operation
/// @param input Slice of bytes representing the input for the precompile operation
/// @return success Represents if the operation was successful
/// @return output Result bytes of the operation
function MapFpToG1(bytes memory input) internal view returns (bool success, bytes memory output) {
/// @param element Fp element
/// @return result Resulted G1 point
function MapFpToG1(Fp memory element) internal view returns (G1Point memory result) {
// MAP_FP_TO_G1 address is 0x12
(success, output) = address(0x12).staticcall(input);
(bool success, bytes memory output) = address(0x12).staticcall(abi.encode(element));
require(success, "MAP_FP_TO_G1 failed");
return abi.decode(output, (G1Point));
}

/// @notice MAP_FP2_TO_G2 operation
/// @param input Slice of bytes representing the input for the precompile operation
/// @return success Represents if the operation was successful
/// @return output Result bytes of the operation
function MapFp2ToG2(bytes memory input) internal view returns (bool success, bytes memory output) {
/// @param element Fp2 element
/// @return result Resulted G2 point
function MapFp2ToG2(Fp2 memory element) internal view returns (G2Point memory result) {
// MAP_FP2_TO_G2 address is 0x13
(success, output) = address(0x13).staticcall(input);
(bool success, bytes memory output) = address(0x13).staticcall(abi.encode(element));
require(success, "MAP_FP2_TO_G2 failed");
return abi.decode(output, (G2Point));
}
}
58 changes: 58 additions & 0 deletions test/BLS.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {Test, console} from "forge-std/Test.sol";
import {BLS} from "../src/sign/BLS.sol";

/// @notice A simple test demonstrating BLS signature verification.
contract BLSTest is Test {
/// @notice The generator point in G1 (P1).
BLS.G1Point G1_GENERATOR = BLS.G1Point(
BLS.Fp(
31827880280837800241567138048534752271,
88385725958748408079899006800036250932223001591707578097800747617502997169851
),
BLS.Fp(
11568204302792691131076548377920244452,
114417265404584670498511149331300188430316142484413708742216858159411894806497
)
);

/// @notice The negated generator point in G1 (-P1).
BLS.G1Point NEGATED_G1_GENERATOR = BLS.G1Point(
BLS.Fp(
31827880280837800241567138048534752271,
88385725958748408079899006800036250932223001591707578097800747617502997169851
),
BLS.Fp(
22997279242622214937712647648895181298,
46816884707101390882112958134453447585552332943769894357249934112654335001290
)
);

function test() public {
// Obtain the private key as a random scalar.
uint256 privateKey = vm.randomUint();

// Public key is the generator point multiplied by the private key.
BLS.G1Point memory publicKey = BLS.G1Mul(G1_GENERATOR, privateKey);

// Compute the message point by mapping message's keccak256 hash to a point in G2.
bytes memory message = "hello world";
BLS.G2Point memory messagePoint = BLS.MapFp2ToG2(BLS.Fp2(BLS.Fp(0, 0), BLS.Fp(0, uint256(keccak256(message)))));

// Obtain the signature by multiplying the message point by the private key.
BLS.G2Point memory signature = BLS.G2Mul(messagePoint, privateKey);

// Invoke the pairing check to verify the signature.
BLS.G1Point[] memory g1Points = new BLS.G1Point[](2);
g1Points[0] = NEGATED_G1_GENERATOR;
g1Points[1] = publicKey;

BLS.G2Point[] memory g2Points = new BLS.G2Point[](2);
g2Points[0] = signature;
g2Points[1] = messagePoint;

assertTrue(BLS.Pairing(g1Points, g2Points));
}
}
23 changes: 23 additions & 0 deletions test/P256.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {Test, console} from "forge-std/Test.sol";
import {Secp256r1} from "../src/sign/Secp256r1.sol";

/// @notice A simple test demonstrating P256 signature verification.
contract BLSTest is Test {
function test() public {
// Obtain the private key and derive the public key.
uint256 privateKey = vm.randomUint();
(uint256 publicKeyX, uint256 publicKeyY) = vm.publicKeyP256(privateKey);

bytes memory message = "hello world";
bytes32 digest = keccak256(message);

// Sign the hashed message.
(bytes32 r, bytes32 s) = vm.signP256(privateKey, digest);

// Verify the signature.
assertTrue(Secp256r1.verify(digest, r, s, publicKeyX, publicKeyY));
}
}

0 comments on commit 825d42e

Please sign in to comment.