From 825d42ea61248154ba1b8d040dc2f012b52dd59d Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Thu, 26 Sep 2024 20:17:25 +0300 Subject: [PATCH 1/3] BLS and P256 solidity examples --- README.md | 30 ++------- lib/forge-std | 2 +- src/sign/BLS.sol | 154 +++++++++++++++++++++++++++++++++-------------- test/BLS.t.sol | 58 ++++++++++++++++++ test/P256.t.sol | 23 +++++++ 5 files changed, 196 insertions(+), 71 deletions(-) create mode 100644 test/BLS.t.sol create mode 100644 test/P256.t.sol diff --git a/README.md b/README.md index 845593a..fbef013 100644 --- a/README.md +++ b/README.md @@ -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] @@ -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]. diff --git a/lib/forge-std b/lib/forge-std index bf66061..8f24d6b 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit bf6606142994b1e47e2882ce0cd477c020d77623 +Subproject commit 8f24d6b04c92975e0795b5868aa0d783251cdeaa diff --git a/src/sign/BLS.sol b/src/sign/BLS.sol index e720e0a..f768f59 100644 --- a/src/sign/BLS.sol +++ b/src/sign/BLS.sol @@ -6,86 +6,148 @@ pragma solidity ^0.8.23; /// defined in EIP-2537, see . /// @dev Precompile addresses come from the BLS addresses submodule in AlphaNet, see /// -/// 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)); } } diff --git a/test/BLS.t.sol b/test/BLS.t.sol new file mode 100644 index 0000000..7f57a9d --- /dev/null +++ b/test/BLS.t.sol @@ -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)); + } +} diff --git a/test/P256.t.sol b/test/P256.t.sol new file mode 100644 index 0000000..21fd787 --- /dev/null +++ b/test/P256.t.sol @@ -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)); + } +} From 90e89918e326fd92d66e97c434e89aeaedb3a1c8 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Thu, 26 Sep 2024 20:35:09 +0300 Subject: [PATCH 2/3] add aggregated test --- src/sign/BLS.sol | 12 ++++++------ test/BLS.t.sol | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/sign/BLS.sol b/src/sign/BLS.sol index f768f59..c4a8f68 100644 --- a/src/sign/BLS.sol +++ b/src/sign/BLS.sol @@ -7,30 +7,30 @@ pragma solidity ^0.8.23; /// @dev Precompile addresses come from the BLS addresses submodule in AlphaNet, see /// 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, + /// @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. + /// @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 + /// @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 + /// @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; diff --git a/test/BLS.t.sol b/test/BLS.t.sol index 7f57a9d..1b4ce0a 100644 --- a/test/BLS.t.sol +++ b/test/BLS.t.sol @@ -30,6 +30,7 @@ contract BLSTest is Test { ) ); + /// @dev Demonstrates the signing and verification of a message. function test() public { // Obtain the private key as a random scalar. uint256 privateKey = vm.randomUint(); @@ -55,4 +56,39 @@ contract BLSTest is Test { 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)); + } } From 22c9a101b95bf4d37c3b2a73b0ffeaf68b93d624 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Mon, 30 Sep 2024 17:13:11 +0400 Subject: [PATCH 3/3] 7702 --- README.md | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fbef013..3510281 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,33 @@ 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. +## EIP-7702 support + +### cast + +`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`: + +```shell +# sign delegation via delegator-pk and broadcast via sender-pk +cast send $(cast az) --private-key --auth $(cast wallet sign-auth
--private-key ) +``` + +### forge + +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] @@ -116,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 \ No newline at end of file +[delegation designation]: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md#delegation-designation +[EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702 \ No newline at end of file