Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BLS and P256 solidity examples #8

Merged
merged 3 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 32 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,33 +16,40 @@ It will pull the docker image on a first run and should print the version of the

After that, make sure that your forge version is up to data (run `foundryup` if needed), and then you should be able to use all usual forge commands —— all contracts will get compiled for EOF.

## BLS library

Functions to allow calling each of the BLS precompiles defined in [EIP-2537]
without the low level details.
## EIP-7702 support

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;
### cast

import {BLS} from "/path/to/forge-alphanet/src/sign/BLS.sol";
`cast send` accepts a `--auth` argument which can accept either an address or an encoded authorization which can be obtained through `cast wallet sign-auth`:

contract BLSExample {
event OperationResult(bool success, bytes result);
```shell
# sign delegation via delegator-pk and broadcast via sender-pk
cast send $(cast az) --private-key <sender-pk> --auth $(cast wallet sign-auth <address> --private-key <delegator-pk>)
```

// 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);
### forge

if (!success) {
emit OperationResult(false, "");
} else {
emit OperationResult(true, output);
}
To test EIP-7702 features in forge tests, you can use `vm.etch` cheatcode:
```solidity
import {Test} from "forge-std/Test.sol";
import {P256Delegation} from "../src/P256Delegation.sol";

contract DelegationTest is Test {
function test() public {
P256Delegation delegation = new P256Delegation();
// this sets ALICE's EOA code to the deployed contract code
vm.etch(ALICE, address(delegation).code);
}
}
```

## BLS library

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

We've prepared a simple test demonstrating BLS signing and verification in [test/BLS.t.sol](test/BLS.t.sol).

## Secp256r1 library

Provides functionality to call the `P256VERIFY` precompile defined in [EIP-7212]
Expand All @@ -59,13 +66,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 Expand Up @@ -134,4 +143,5 @@ Note that we are using a different private key here, this transaction can be sen
[Apple Secure Enclave]: https://support.apple.com/guide/security/secure-enclave-sec59b0b31ff/web
[WebAuthn]: https://webauthn.io/
[Python]: https://www.python.org/
[delegation designation]: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md#delegation-designation
[delegation designation]: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md#delegation-designation
[EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702
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));
}
}
94 changes: 94 additions & 0 deletions test/BLS.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// 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
)
);

/// @dev Demonstrates the signing and verification of a message.
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));
}

/// @dev Demonstrates the aggregation and verification of two signatures.
function testAggregated() public {
// private keys
uint256 sk1 = vm.randomUint();
uint256 sk2 = vm.randomUint();

// public keys
BLS.G1Point memory pk1 = BLS.G1Mul(G1_GENERATOR, sk1);
BLS.G1Point memory pk2 = BLS.G1Mul(G1_GENERATOR, sk2);

// 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)))));

// signatures
BLS.G2Point memory sig1 = BLS.G2Mul(messagePoint, sk1);
BLS.G2Point memory sig2 = BLS.G2Mul(messagePoint, sk2);

// aggregated signature
BLS.G2Point memory sig = BLS.G2Add(sig1, sig2);

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

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

assertTrue(BLS.Pairing(g1Points, g2Points));
}
}
Loading