├── .github └── workflows │ ├── examples.yml │ └── test.yml ├── .gitignore ├── .gitmodules ├── README.md ├── examples ├── bls-multisig │ ├── python │ │ ├── .gitignore │ │ ├── README.md │ │ └── multisig.py │ └── rust │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ └── src │ │ └── main.rs └── p256.py ├── foundry.toml ├── src ├── BLSMultisig.sol ├── P256Delegation.sol └── sign │ ├── BLS.sol │ └── Secp256r1.sol └── test ├── BLS.t.sol └── P256.t.sol /.github/workflows/examples.yml: -------------------------------------------------------------------------------- 1 | name: examples 2 | 3 | on: 4 | pull_request: 5 | merge_group: 6 | push: 7 | branches: [main] 8 | 9 | env: 10 | FOUNDRY_PROFILE: ci 11 | 12 | jobs: 13 | check: 14 | strategy: 15 | fail-fast: true 16 | 17 | name: Foundry project 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | submodules: recursive 23 | 24 | - uses: actions/setup-python@v5 25 | with: 26 | python-version: '3.12' 27 | 28 | - uses: dtolnay/rust-toolchain@stable 29 | 30 | - name: Install Foundry 31 | uses: foundry-rs/foundry-toolchain@v1 32 | 33 | - name: Run Forge build 34 | id: build 35 | run: forge build 36 | 37 | - name: Rust BLS multisig example 38 | run: cargo run --manifest-path ./examples/bls-multisig/rust/Cargo.toml 39 | 40 | - name: Python BLS multisig example 41 | run: | 42 | pip install web3 py_ecc 43 | python ./examples/bls-multisig/python/multisig.py 44 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | pull_request: 5 | merge_group: 6 | push: 7 | branches: [main] 8 | 9 | env: 10 | FOUNDRY_PROFILE: ci 11 | 12 | jobs: 13 | check: 14 | strategy: 15 | fail-fast: true 16 | 17 | name: Foundry project 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | submodules: recursive 23 | 24 | - name: Install Foundry 25 | uses: foundry-rs/foundry-toolchain@v1 26 | 27 | - name: Run Forge build 28 | id: build 29 | run: forge build 30 | 31 | - name: Run Forge test 32 | id: test 33 | run: forge test 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | 16 | # Key files 17 | private.pem 18 | public.pem -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # forge-alphanet 2 | 3 | Set of solidity utilities to ease deployment and usage of applications on 4 | [AlphaNet]. 5 | 6 | ## EOF support 7 | 8 | Forge has built-in support for [EOF]. This is done by using solc binary from [forge-eof] repository distributed as a docker image. To be able to compile contracts you will need to have [Docker] installed. Once it's installed, and forge version is up to date (run `foundryup` if needed), you can add `--eof` flag to any forge command to try out EOF compilation. 9 | 10 | This repository is configured to compile contracts for [EOF] by default by setting `eof = true` in the `foundry.toml` file. 11 | 12 | ## EIP-7702 support 13 | 14 | ### cast 15 | 16 | `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`: 17 | 18 | ```shell 19 | # sign delegation via delegator-pk and broadcast via sender-pk 20 | cast send $(cast az) --private-key --auth $(cast wallet sign-auth
--private-key ) 21 | ``` 22 | 23 | ### forge 24 | 25 | To test EIP-7702 features in forge tests, you can use `vm.etch` cheatcode: 26 | ```solidity 27 | import {Test} from "forge-std/Test.sol"; 28 | import {P256Delegation} from "../src/P256Delegation.sol"; 29 | 30 | contract DelegationTest is Test { 31 | function test() public { 32 | P256Delegation delegation = new P256Delegation(); 33 | // this sets ALICE's EOA code to the deployed contract code 34 | vm.etch(ALICE, address(delegation).code); 35 | } 36 | } 37 | ``` 38 | 39 | ## BLS library 40 | 41 | Functions and data structures to allow calling each of the BLS precompiles defined in [EIP-2537] 42 | without the low level details. 43 | 44 | We've prepared a simple test demonstrating BLS signing and verification in [test/BLS.t.sol](test/BLS.t.sol). 45 | 46 | ## Secp256r1 library 47 | 48 | Provides functionality to call the `P256VERIFY` precompile defined in [EIP-7212] 49 | to verify Secp256r1 signatures. 50 | 51 | It can be used in a solidity smart contract like this: 52 | ```solidity 53 | // SPDX-License-Identifier: MIT 54 | pragma solidity ^0.8.25; 55 | 56 | import {Secp256r1} from "/path/to/forge-alphanet/src/sign/Secp256r1.sol"; 57 | 58 | contract Secp256r1Example { 59 | event OperationResult(bool success); 60 | 61 | // Function to perform a Secp256r1 signature verification with error handling 62 | function performP256Verify(bytes32 digest, bytes32 r, bytes32 s, uint256 publicKeyX, uint256 publicKeyY) public { 63 | bool result = Secp256r1.verify(digest, r, s, publicKeyX, publicKeyY); 64 | emit OperationResult(result); 65 | } 66 | } 67 | ``` 68 | 69 | See an example of how to test secp256r1 signatures with foundry cheatcodes in [test/P256.t.sol](test/P256.t.sol). 70 | 71 | ## Account controlled by a P256 key 72 | 73 | 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]. 74 | 75 | We are demonstrating a simple implementation of an account that can be controlled by a P256 key. EOAs can delegate to this contract and configure an authorized P256 key, which can then be used to perform actions on behalf of the EOA. 76 | 77 | To run the commands below, you will need to have [Python] and `openssl` CLI tool installed. 78 | 79 | 1. Run anvil in Alphanet mode to enable support for EIP-7702 and P256 precompile: 80 | ```shell 81 | anvil --alphanet 82 | ``` 83 | We will be using dev account with address `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266` and private key `0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80`. 84 | 85 | 2. Generate a P256 private and public key pair: 86 | ```shell 87 | python examples/p256.py gen 88 | ``` 89 | 90 | This command will generate a private and public key pair, save them to `private.pem` and `public.pem` respectively, and print the public key in hex format. 91 | 92 | 3. Deploy [P256Delegation](src/P256Delegation.sol) contract which we will be delegating to: 93 | ```shell 94 | forge create P256Delegation --private-key "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" --rpc-url "http://127.0.0.1:8545" 95 | ``` 96 | 97 | 4. Configure delegation contract: 98 | Send EIP-7702 transaction, delegating to our newly deployed contract. 99 | This transaction will both authorize the delegation and set it to use our P256 public key: 100 | ```shell 101 | cast send 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 'authorize(uint256,uint256)' '' '' --auth "
" --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 102 | ``` 103 | Note that we are transacting with our EOA account which already includes the updated code. 104 | 105 | Verify that new code at our EOA account contains the [delegation designation]: 106 | ```shell 107 | $ cast code 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 108 | 0xef0100... 109 | ``` 110 | 111 | 5. After that, you should be able to transact on behalf of the EOA account by using the `transact` function of the delegation contract. 112 | Let's generate a signature for sending 1 ether to zero address by using our P256 private key: 113 | ```shell 114 | python examples/p256.py sign $(cast abi-encode 'f(uint256,address,bytes,uint256)' $(cast call 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 'nonce()(uint256)') '0x0000000000000000000000000000000000000000' '0x' '1000000000000000000') 115 | ``` 116 | 117 | Note that it uses `cast call` to get internal nonce of our EOA used to protect against replay attacks. 118 | It also abi-encodes the payload expected by the `P256Delegation` contract, and passes it to our Python script to sign with openssl. 119 | 120 | Command output will contain the signature r and s values, which we then should pass to the `transact` function of the delegation contract: 121 | ```shell 122 | cast send 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 'transact(address to,bytes data,uint256 value,bytes32 r,bytes32 s)' '0x0000000000000000000000000000000000000000' '0x' '1000000000000000000' '' '' --private-key 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d 123 | ``` 124 | 125 | Note that we are using a different private key here, this transaction can be sent by anyone as it was authorized by our P256 key. 126 | 127 | 128 | [AlphaNet]: https://github.com/paradigmxyz/alphanet 129 | [EOF]: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-3540.md 130 | [forge-eof]: https://github.com/paradigmxyz/forge-eof 131 | [Docker]: https://docs.docker.com/ 132 | [EIP-2537]: https://eips.ethereum.org/EIPS/eip-2537 133 | [EIP-7212]: https://eips.ethereum.org/EIPS/eip-7212 134 | [EIP-3074]: https://eips.ethereum.org/EIPS/eip-3074 135 | [foundry-alphanet]: https://github.com/paradigmxyz/foundry-alphanet 136 | [Apple Secure Enclave]: https://support.apple.com/guide/security/secure-enclave-sec59b0b31ff/web 137 | [WebAuthn]: https://webauthn.io/ 138 | [Python]: https://www.python.org/ 139 | [delegation designation]: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md#delegation-designation 140 | [EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702 141 | -------------------------------------------------------------------------------- /examples/bls-multisig/python/.gitignore: -------------------------------------------------------------------------------- 1 | .venv/ 2 | -------------------------------------------------------------------------------- /examples/bls-multisig/python/README.md: -------------------------------------------------------------------------------- 1 | # Python Alphanet Multisig 2 | 3 | This example demonstrates an integration of [BlsMultisig](../../../src/BLSMultisig.sol) with Python. 4 | 5 | ## Running the example 6 | 7 | To run the example, you will need to install the required dependencies: 8 | 9 | ```shell 10 | pip install web3 py_ecc 11 | ``` 12 | 13 | Then, you can run the example by executing the following command: 14 | 15 | ```shell 16 | python multisig.py 17 | ``` 18 | 19 | This will spin up an Anvil instance in Alphanet mode, deploy the multisig contract and execute a simple operation signed by random BLS keys. -------------------------------------------------------------------------------- /examples/bls-multisig/python/multisig.py: -------------------------------------------------------------------------------- 1 | from web3 import AsyncWeb3 2 | import pathlib 3 | import asyncio 4 | import json 5 | import subprocess 6 | import random 7 | from py_ecc.bls import G2Basic 8 | from py_ecc.bls import g2_primitives 9 | import eth_abi 10 | 11 | Fp = tuple[int, int] 12 | Fp2 = tuple[Fp, Fp] 13 | G1Point = tuple[Fp, Fp] 14 | G2Point = tuple[Fp2, Fp2] 15 | Operation = tuple[str, str, int, int] 16 | 17 | 18 | def fp_from_int(x: int) -> Fp: 19 | b = x.to_bytes(64, "big") 20 | return (int.from_bytes(b[:32], "big"), int.from_bytes(b[32:], "big")) 21 | 22 | 23 | def generate_keys(num: int) -> list[tuple[G1Point, int]]: 24 | keypairs = [] 25 | for _ in range(num): 26 | sk = random.randint(0, 10**30) 27 | pk_point = g2_primitives.pubkey_to_G1(G2Basic.SkToPk(sk)) 28 | 29 | pk = (fp_from_int(int(pk_point[0])), fp_from_int(int(pk_point[1]))) 30 | 31 | keypairs.append((pk, sk)) 32 | 33 | keypairs.sort() 34 | 35 | return keypairs 36 | 37 | 38 | def sign_operation(sks: list[int], operation: Operation) -> G2Point: 39 | encoded = eth_abi.encode(["(address,bytes,uint256,uint256)"], [operation]) 40 | 41 | signatures = [] 42 | for sk in sks: 43 | signatures.append(G2Basic.Sign(sk, encoded)) 44 | 45 | aggregated = g2_primitives.signature_to_G2(G2Basic.Aggregate(signatures)) 46 | 47 | signature = ( 48 | (fp_from_int(aggregated[0].coeffs[0]), fp_from_int(aggregated[0].coeffs[1])), 49 | (fp_from_int(aggregated[1].coeffs[0]), fp_from_int(aggregated[1].coeffs[1])), 50 | ) 51 | 52 | return signature 53 | 54 | 55 | async def main(): 56 | bls_multisig_artifact = json.load( 57 | open(pathlib.Path(__file__).parent.parent.parent.parent / "out/BLSMultisig.sol/BLSMultisig.json") 58 | ) 59 | 60 | web3 = AsyncWeb3(AsyncWeb3.AsyncHTTPProvider("http://localhost:8545")) 61 | 62 | bytecode = bls_multisig_artifact["bytecode"]["object"] 63 | abi = bls_multisig_artifact["abi"] 64 | BlsMultisig = web3.eth.contract(abi=abi, bytecode=bytecode) 65 | 66 | signer = (await web3.eth.accounts)[0] 67 | 68 | # generate 100 BLS keys 69 | keypairs = generate_keys(100) 70 | pks = list(map(lambda x: x[0], keypairs)) 71 | 72 | # deploy the multisig contract with generated signers and threshold of 50 73 | tx = await BlsMultisig.constructor(pks, 50).transact({"from": signer}) 74 | receipt = await web3.eth.wait_for_transaction_receipt(tx) 75 | multisig = BlsMultisig(receipt.contractAddress) 76 | 77 | # fund the multisig 78 | hash = await web3.eth.send_transaction({"from": signer, "to": multisig.address, "value": 10**18}) 79 | await web3.eth.wait_for_transaction_receipt(hash) 80 | 81 | # create an operation transferring 1 eth to zero address 82 | operation: Operation = ("0x0000000000000000000000000000000000000000", bytes(), 10**18, 0) 83 | 84 | # choose 50 random signers that will sign the operation 85 | signers_subset = sorted(random.sample(keypairs, 50)) 86 | 87 | pks = list(map(lambda x: x[0], signers_subset)) 88 | sks = list(map(lambda x: x[1], signers_subset)) 89 | 90 | # create aggregated signature for operation 91 | signature = sign_operation(sks, operation) 92 | 93 | # execute the operation 94 | tx = await multisig.functions.verifyAndExecute((operation, pks, signature)).transact({"from": signer}) 95 | receipt = await web3.eth.wait_for_transaction_receipt(tx) 96 | 97 | assert receipt.status == 1 98 | 99 | 100 | if __name__ == "__main__": 101 | try: 102 | anvil = subprocess.Popen(["anvil", "--alphanet"], stdout=subprocess.PIPE) 103 | asyncio.run(main()) 104 | finally: 105 | anvil.terminate() 106 | -------------------------------------------------------------------------------- /examples/bls-multisig/rust/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /examples/bls-multisig/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "alphanet-bls-multisig" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | alloy = { version = "0.4", features = [ 8 | "providers", 9 | "contract", 10 | "sol-types", 11 | "node-bindings", 12 | "rpc-types", 13 | "getrandom", 14 | ] } 15 | tokio = { version = "1", features = ["full"] } 16 | blst = "0.3" 17 | rand = "0.8" 18 | -------------------------------------------------------------------------------- /examples/bls-multisig/rust/src/main.rs: -------------------------------------------------------------------------------- 1 | use alloy::{ 2 | network::TransactionBuilder, 3 | primitives::{Address, U256}, 4 | providers::{Provider, ProviderBuilder}, 5 | rpc::types::TransactionRequest, 6 | sol, 7 | sol_types::SolValue, 8 | }; 9 | use blst::min_pk::{AggregateSignature, PublicKey, SecretKey, Signature}; 10 | use rand::{seq::IteratorRandom, RngCore}; 11 | 12 | sol! { 13 | #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord)] 14 | #[sol(rpc)] 15 | BLSMultisig, 16 | "../../../out/BLSMultisig.sol/BLSMultisig.json" 17 | } 18 | 19 | /// Generates `num` BLS keys and returns them as a tuple of private and public keys 20 | fn generate_keys(num: usize) -> (Vec, Vec) { 21 | let mut rng = rand::thread_rng(); 22 | 23 | let mut public = Vec::with_capacity(num); 24 | let mut private = Vec::with_capacity(num); 25 | 26 | for _ in 0..num { 27 | let mut ikm = [0u8; 32]; 28 | rng.fill_bytes(&mut ikm); 29 | 30 | let sk = SecretKey::key_gen(&ikm, &[]).unwrap(); 31 | let pk = BLS::G1Point::from(sk.sk_to_pk()); 32 | 33 | public.push(pk); 34 | private.push(sk); 35 | } 36 | 37 | (private, public) 38 | } 39 | 40 | /// Signs a message with the provided keys and returns the aggregated signature. 41 | fn sign_message(keys: &[&SecretKey], msg: &[u8]) -> BLS::G2Point { 42 | let mut sigs = Vec::new(); 43 | 44 | // create individual signatures 45 | for key in keys { 46 | let sig = key.sign(msg, b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_", &[]); 47 | sigs.push(sig); 48 | } 49 | 50 | // aggregate 51 | Signature::from_aggregate( 52 | &AggregateSignature::aggregate(sigs.iter().collect::>().as_slice(), false).unwrap(), 53 | ) 54 | .into() 55 | } 56 | 57 | #[tokio::main] 58 | pub async fn main() -> Result<(), Box> { 59 | // Spawn Anvil node and connect to it 60 | let provider = ProviderBuilder::new().on_anvil_with_config(|config| config.arg("--alphanet")); 61 | 62 | // Generate 100 BLS keys 63 | let (private_keys, public_keys) = generate_keys(100); 64 | 65 | // Deploy multisig contract, configuring generated keys as signers and requiring threshold of 50 66 | let multisig = BLSMultisig::deploy(&provider, public_keys.clone(), U256::from(50)).await?; 67 | 68 | // Fund multisig with some ETH 69 | provider 70 | .send_transaction( 71 | TransactionRequest::default() 72 | .to(*multisig.address()) 73 | .with_value(U256::from(1_000_000_000_000_000_000u128)), 74 | ) 75 | .await? 76 | .watch() 77 | .await?; 78 | 79 | // Operation which will transfer 1 ETH to a random address 80 | let operation = BLSMultisig::Operation { 81 | to: Address::random(), 82 | value: U256::from(1_000_000_000_000_000_000u128), 83 | nonce: multisig.nonce().call().await?._0, 84 | data: Default::default(), 85 | }; 86 | 87 | // Choose 50 random signers to sign the operation 88 | let (keys, signers): (Vec<_>, Vec<_>) = { 89 | let mut pairs = private_keys 90 | .iter() 91 | .zip(public_keys.clone()) 92 | .choose_multiple(&mut rand::thread_rng(), 50); 93 | 94 | // contract requires signers to be sorted by public key 95 | pairs.sort_by(|(_, pk1), (_, pk2)| pk1.cmp(pk2)); 96 | 97 | pairs.into_iter().unzip() 98 | }; 99 | 100 | // Sign abi-encoded operation with the chosen keys 101 | let signature = sign_message(&keys, &operation.abi_encode()); 102 | 103 | // Send the signed operation to the contract along with the list of signers 104 | let receipt = multisig 105 | .verifyAndExecute(BLSMultisig::SignedOperation { 106 | operation: operation.clone(), 107 | signers, 108 | signature, 109 | }) 110 | .send() 111 | .await? 112 | .get_receipt() 113 | .await?; 114 | 115 | // Assert that the transaction was successful and that recipient has received the funds 116 | assert!(receipt.status()); 117 | assert!(provider.get_balance(operation.to).await? > U256::ZERO); 118 | 119 | Ok(()) 120 | } 121 | 122 | /// Converts a blst [`PublicKey`] to a [`BLS::G1Point`] which can be passed to the contract 123 | impl From for BLS::G1Point { 124 | fn from(value: PublicKey) -> Self { 125 | let serialized = value.serialize(); 126 | 127 | let mut data = [0u8; 128]; 128 | data[16..64].copy_from_slice(&serialized[0..48]); 129 | data[80..128].copy_from_slice(&serialized[48..96]); 130 | 131 | BLS::G1Point::abi_decode(&data, false).unwrap() 132 | } 133 | } 134 | 135 | /// Converts a blst [`Signature`] to a [`BLS::G2Point`] which can be passed to the contract 136 | impl From for BLS::G2Point { 137 | fn from(value: Signature) -> Self { 138 | let serialized = value.serialize(); 139 | 140 | let mut data = [0u8; 256]; 141 | data[16..64].copy_from_slice(&serialized[48..96]); 142 | data[80..128].copy_from_slice(&serialized[0..48]); 143 | data[144..192].copy_from_slice(&serialized[144..192]); 144 | data[208..256].copy_from_slice(&serialized[96..144]); 145 | 146 | BLS::G2Point::abi_decode(&data, false).unwrap() 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /examples/p256.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import subprocess 3 | 4 | args = sys.argv[1:] 5 | if args[0] == "gen": 6 | subprocess.run( 7 | "openssl ecparam -name prime256v1 -genkey -noout -out private.pem", 8 | shell=True, 9 | stdout=subprocess.DEVNULL, 10 | stderr=subprocess.DEVNULL, 11 | ) 12 | subprocess.run( 13 | "openssl ec -in private.pem -pubout -out public.pem", 14 | shell=True, 15 | stdout=subprocess.DEVNULL, 16 | stderr=subprocess.DEVNULL, 17 | ) 18 | data = ( 19 | subprocess.check_output( 20 | "openssl ec -in private.pem -text", shell=True, stderr=subprocess.DEVNULL 21 | ) 22 | .decode() 23 | .rstrip() 24 | .replace("\n", "") 25 | .replace(" ", "") 26 | ) 27 | priv = data.split("priv:")[1].split("pub:")[0].replace(":", "") 28 | pub = data.split("pub:")[1].split("ASN1")[0].replace(":", "")[2:] 29 | pub_x, pub_y = pub[:64], pub[64:] 30 | 31 | print(f"Private key: 0x{priv}") 32 | print(f"Public key: 0x{pub_x}, 0x{pub_y}") 33 | elif args[0] == "sign": 34 | payload = bytearray.fromhex(args[1].replace("0x", "")) 35 | 36 | proc = subprocess.Popen( 37 | ["openssl", "dgst", "-keccak-256", "-sign", "private.pem"], 38 | stdin=subprocess.PIPE, 39 | stdout=subprocess.PIPE, 40 | ) 41 | output = proc.communicate(payload)[0] 42 | proc = subprocess.Popen( 43 | ["openssl", "asn1parse", "-inform", "DER"], 44 | stdin=subprocess.PIPE, 45 | stdout=subprocess.PIPE, 46 | ) 47 | output = proc.communicate(output)[0].decode().replace(" ", "").replace("\n", "") 48 | 49 | sig_r = output.split("INTEGER:")[1][:64] 50 | sig_s = output.split("INTEGER:")[2][:64] 51 | 52 | print(f"Signature r: 0x{sig_r}") 53 | print(f"Signature s: 0x{sig_s}") 54 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | 6 | eof = true 7 | alphanet = true 8 | -------------------------------------------------------------------------------- /src/BLSMultisig.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import {BLS} from "./sign/BLS.sol"; 5 | 6 | /// @notice BLS-powered multisignature wallet, demonstrating the use of 7 | /// aggregated BLS signatures for verification 8 | /// @dev This is for demonstration purposes only, do not use in production. This contract does 9 | /// not include protection from rogue public-key attacks. 10 | contract BLSMultisig { 11 | /// @notice Public keys of signers. This may contain a pre-aggregated 12 | /// public keys for common sets of signers as well. 13 | mapping(bytes32 => bool) public signers; 14 | 15 | struct Operation { 16 | address to; 17 | bytes data; 18 | uint256 value; 19 | uint256 nonce; 20 | } 21 | 22 | struct SignedOperation { 23 | Operation operation; 24 | BLS.G1Point[] signers; 25 | BLS.G2Point signature; 26 | } 27 | 28 | /// @notice The negated generator point in G1 (-P1). Used during pairing as a first G1 point. 29 | BLS.G1Point NEGATED_G1_GENERATOR = BLS.G1Point( 30 | BLS.Fp( 31 | 31827880280837800241567138048534752271, 32 | 88385725958748408079899006800036250932223001591707578097800747617502997169851 33 | ), 34 | BLS.Fp( 35 | 22997279242622214937712647648895181298, 36 | 46816884707101390882112958134453447585552332943769894357249934112654335001290 37 | ) 38 | ); 39 | 40 | /// @notice The number of signatures required to execute an operation. 41 | uint256 public threshold; 42 | 43 | /// @notice Nonce used for replay protection. 44 | uint256 public nonce; 45 | 46 | constructor(BLS.G1Point[] memory _signers, uint256 _threshold) { 47 | for (uint256 i = 0; i < _signers.length; i++) { 48 | signers[keccak256(abi.encode(_signers[i]))] = true; 49 | } 50 | threshold = _threshold; 51 | } 52 | 53 | /// @notice Maps an operation to a point on G2 which needs to be signed. 54 | function getOperationPoint(Operation memory op) public view returns (BLS.G2Point memory) { 55 | return BLS.hashToCurveG2(abi.encode(op)); 56 | } 57 | 58 | /// @notice Accepts an operation signed by a subset of the signers and executes it 59 | function verifyAndExecute(SignedOperation memory operation) public { 60 | require(operation.operation.nonce == nonce++, "invalid nonce"); 61 | require(operation.signers.length >= threshold, "not enough signers"); 62 | 63 | BLS.G1Point memory aggregatedSigner; 64 | 65 | for (uint256 i = 0; i < operation.signers.length; i++) { 66 | BLS.G1Point memory signer = operation.signers[i]; 67 | require(signers[keccak256(abi.encode(signer))], "invalid signer"); 68 | 69 | if (i == 0) { 70 | aggregatedSigner = signer; 71 | } else { 72 | aggregatedSigner = BLS.G1Add(aggregatedSigner, signer); 73 | require(_comparePoints(operation.signers[i - 1], signer), "signers not sorted"); 74 | } 75 | } 76 | 77 | BLS.G1Point[] memory g1Points = new BLS.G1Point[](2); 78 | BLS.G2Point[] memory g2Points = new BLS.G2Point[](2); 79 | 80 | g1Points[0] = NEGATED_G1_GENERATOR; 81 | g1Points[1] = aggregatedSigner; 82 | 83 | g2Points[0] = operation.signature; 84 | g2Points[1] = getOperationPoint(operation.operation); 85 | 86 | // verify signature 87 | require(BLS.Pairing(g1Points, g2Points), "invalid signature"); 88 | 89 | // execute operation 90 | Operation memory op = operation.operation; 91 | (bool success,) = op.to.call{value: op.value}(op.data); 92 | require(success, "execution failed"); 93 | } 94 | 95 | /// @notice Returns true if X coordinate of the first point is lower than the X coordinate of the second point. 96 | function _comparePoints(BLS.G1Point memory a, BLS.G1Point memory b) internal pure returns (bool) { 97 | BLS.Fp memory aX = a.x; 98 | BLS.Fp memory bX = b.x; 99 | 100 | if (aX.a < bX.a) { 101 | return true; 102 | } else if (aX.a > bX.a) { 103 | return false; 104 | } else if (aX.b < bX.b) { 105 | return true; 106 | } else { 107 | return false; 108 | } 109 | } 110 | 111 | receive() external payable {} 112 | } 113 | -------------------------------------------------------------------------------- /src/P256Delegation.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import {Secp256r1} from "./sign/Secp256r1.sol"; 5 | 6 | /// @notice Contract designed for being delegated to by EOAs to authorize a secp256r1 key to transact on their behalf. 7 | contract P256Delegation { 8 | /// @notice The x coordinate of the authorized public key 9 | uint256 authorizedPublicKeyX; 10 | /// @notice The y coordinate of the authorized public key 11 | uint256 authorizedPublicKeyY; 12 | 13 | /// @notice Internal nonce used for replay protection, must be tracked and included into prehashed message. 14 | uint256 public nonce; 15 | 16 | /// @notice Authorizes provided public key to transact on behalf of this account. Only callable by EOA itself. 17 | function authorize(uint256 publicKeyX, uint256 publicKeyY) public { 18 | require(msg.sender == address(this)); 19 | 20 | authorizedPublicKeyX = publicKeyX; 21 | authorizedPublicKeyY = publicKeyY; 22 | } 23 | 24 | /// @notice Main entrypoint for authorized transactions. Accepts transaction parameters (to, data, value) and a secp256r1 signature. 25 | function transact(address to, bytes memory data, uint256 value, bytes32 r, bytes32 s) public { 26 | bytes32 digest = keccak256(abi.encode(nonce++, to, data, value)); 27 | require(Secp256r1.verify(digest, r, s, authorizedPublicKeyX, authorizedPublicKeyY), "Invalid signature"); 28 | 29 | (bool success,) = to.call{value: value}(data); 30 | require(success); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/sign/BLS.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | /// @title BLS 5 | /// @notice Wrapper functions to abstract low level details of calls to BLS precompiles 6 | /// defined in EIP-2537, see . 7 | /// @dev Precompile addresses come from the BLS addresses submodule in AlphaNet, see 8 | /// 9 | /// @notice `hashToCurve` logic is based on 10 | /// with small modifications including: 11 | /// - Removal of low-level assembly in _modexp to ensure compatibility with EOF which does not support low-level staticcall 12 | /// - Usage of Fp2/G2Point structs defined here for better compatibility with existing methods 13 | library BLS { 14 | /// @dev A base field element (Fp) is encoded as 64 bytes by performing the 15 | /// BigEndian encoding of the corresponding (unsigned) integer. Due to the size of p, 16 | /// the top 16 bytes are always zeroes. 17 | struct Fp { 18 | uint256 a; 19 | uint256 b; 20 | } 21 | 22 | /// @dev For elements of the quadratic extension field (Fp2), encoding is byte concatenation of 23 | /// individual encoding of the coefficients totaling in 128 bytes for a total encoding. 24 | /// c0 + c1 * v 25 | struct Fp2 { 26 | Fp c0; 27 | Fp c1; 28 | } 29 | 30 | /// @dev Points of G1 and G2 are encoded as byte concatenation of the respective 31 | /// encodings of the x and y coordinates. 32 | struct G1Point { 33 | Fp x; 34 | Fp y; 35 | } 36 | 37 | /// @dev Points of G1 and G2 are encoded as byte concatenation of the respective 38 | /// encodings of the x and y coordinates. 39 | struct G2Point { 40 | Fp2 x; 41 | Fp2 y; 42 | } 43 | 44 | /// @notice G1ADD operation 45 | /// @param a First G1 point 46 | /// @param b Second G1 point 47 | /// @return result Resulted G1 point 48 | function G1Add(G1Point memory a, G1Point memory b) internal view returns (G1Point memory result) { 49 | // G1ADD address is 0x0b 50 | (bool success, bytes memory output) = address(0x0b).staticcall(abi.encode(a, b)); 51 | require(success, "G1ADD failed"); 52 | return abi.decode(output, (G1Point)); 53 | } 54 | 55 | /// @notice G1MUL operation 56 | /// @param point G1 point 57 | /// @param scalar Scalar to multiply the point by 58 | /// @return result Resulted G1 point 59 | function G1Mul(G1Point memory point, uint256 scalar) internal view returns (G1Point memory result) { 60 | // G1MUL address is 0x0c 61 | (bool success, bytes memory output) = address(0x0c).staticcall(abi.encode(point, scalar)); 62 | require(success, "G1MUL failed"); 63 | return abi.decode(output, (G1Point)); 64 | } 65 | 66 | /// @notice G1MSM operation 67 | /// @param points Array of G1 points 68 | /// @param scalars Array of scalars to multiply the points by 69 | /// @return result Resulted G1 point 70 | function G1MSM(G1Point[] memory points, uint256[] memory scalars) internal view returns (G1Point memory result) { 71 | bytes memory input; 72 | 73 | for (uint256 i = 0; i < points.length; i++) { 74 | input = bytes.concat(input, abi.encode(points[i], scalars[i])); 75 | } 76 | 77 | // G1MSM address is 0x0d 78 | (bool success, bytes memory output) = address(0x0d).staticcall(input); 79 | require(success, "G1MSM failed"); 80 | return abi.decode(output, (G1Point)); 81 | } 82 | 83 | /// @notice G2ADD operation 84 | /// @param a First G2 point 85 | /// @param b Second G2 point 86 | /// @return result Resulted G2 point 87 | function G2Add(G2Point memory a, G2Point memory b) internal view returns (G2Point memory result) { 88 | // G2ADD address is 0x0e 89 | (bool success, bytes memory output) = address(0x0e).staticcall(abi.encode(a, b)); 90 | require(success, "G2ADD failed"); 91 | return abi.decode(output, (G2Point)); 92 | } 93 | 94 | /// @notice G2MUL operation 95 | /// @param point G2 point 96 | /// @param scalar Scalar to multiply the point by 97 | /// @return result Resulted G2 point 98 | function G2Mul(G2Point memory point, uint256 scalar) internal view returns (G2Point memory result) { 99 | // G2MUL address is 0x0f 100 | (bool success, bytes memory output) = address(0x0f).staticcall(abi.encode(point, scalar)); 101 | require(success, "G2MUL failed"); 102 | return abi.decode(output, (G2Point)); 103 | } 104 | 105 | /// @notice G2MSM operation 106 | /// @param points Array of G2 points 107 | /// @param scalars Array of scalars to multiply the points by 108 | /// @return result Resulted G2 point 109 | function G2MSM(G2Point[] memory points, uint256[] memory scalars) internal view returns (G2Point memory result) { 110 | bytes memory input; 111 | 112 | for (uint256 i = 0; i < points.length; i++) { 113 | input = bytes.concat(input, abi.encode(points[i], scalars[i])); 114 | } 115 | 116 | // G2MSM address is 0x10 117 | (bool success, bytes memory output) = address(0x10).staticcall(input); 118 | require(success, "G2MSM failed"); 119 | return abi.decode(output, (G2Point)); 120 | } 121 | 122 | /// @notice PAIRING operation 123 | /// @param g1Points Array of G1 points 124 | /// @param g2Points Array of G2 points 125 | /// @return result Returns whether pairing result is equal to the multiplicative identity (1). 126 | function Pairing(G1Point[] memory g1Points, G2Point[] memory g2Points) internal view returns (bool result) { 127 | bytes memory input; 128 | for (uint256 i = 0; i < g1Points.length; i++) { 129 | input = bytes.concat(input, abi.encode(g1Points[i], g2Points[i])); 130 | } 131 | 132 | // PAIRING address is 0x11 133 | (bool success, bytes memory output) = address(0x11).staticcall(input); 134 | require(success, "Pairing failed"); 135 | return abi.decode(output, (bool)); 136 | } 137 | 138 | /// @notice MAP_FP_TO_G1 operation 139 | /// @param element Fp element 140 | /// @return result Resulted G1 point 141 | function MapFpToG1(Fp memory element) internal view returns (G1Point memory result) { 142 | // MAP_FP_TO_G1 address is 0x12 143 | (bool success, bytes memory output) = address(0x12).staticcall(abi.encode(element)); 144 | require(success, "MAP_FP_TO_G1 failed"); 145 | return abi.decode(output, (G1Point)); 146 | } 147 | 148 | /// @notice MAP_FP2_TO_G2 operation 149 | /// @param element Fp2 element 150 | /// @return result Resulted G2 point 151 | function MapFp2ToG2(Fp2 memory element) internal view returns (G2Point memory result) { 152 | // MAP_FP2_TO_G2 address is 0x13 153 | (bool success, bytes memory output) = address(0x13).staticcall(abi.encode(element)); 154 | require(success, "MAP_FP2_TO_G2 failed"); 155 | return abi.decode(output, (G2Point)); 156 | } 157 | 158 | /// @notice Computes a point in G2 from a message 159 | /// @dev Uses the eip-2537 precompiles 160 | /// @param message Arbitrarylength byte string to be hashed 161 | /// @return A point in G2 162 | function hashToCurveG2(bytes memory message) internal view returns (G2Point memory) { 163 | // 1. u = hash_to_field(msg, 2) 164 | Fp2[2] memory u = hashToFieldFp2(message, bytes("BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_")); 165 | // 2. Q0 = map_to_curve(u[0]) 166 | G2Point memory q0 = MapFp2ToG2(u[0]); 167 | // 3. Q1 = map_to_curve(u[1]) 168 | G2Point memory q1 = MapFp2ToG2(u[1]); 169 | // 4. R = Q0 + Q1 170 | return G2Add(q0, q1); 171 | } 172 | 173 | /// @notice Computes a field point from a message 174 | /// @dev Follows https://datatracker.ietf.org/doc/html/rfc9380#section-5.2 175 | /// @param message Arbitrarylength byte string to be hashed 176 | /// @param dst The domain separation tag 177 | /// @return Two field points 178 | function hashToFieldFp2(bytes memory message, bytes memory dst) private view returns (Fp2[2] memory) { 179 | // 1. len_in_bytes = count * m * L 180 | // so always 2 * 2 * 64 = 256 181 | uint16 lenInBytes = 256; 182 | // 2. uniform_bytes = expand_message(msg, DST, len_in_bytes) 183 | bytes32[] memory pseudoRandomBytes = expandMsgXmd(message, dst, lenInBytes); 184 | Fp2[2] memory u; 185 | // No loop here saves 800 gas hardcoding offset an additional 300 186 | // 3. for i in (0, ..., count - 1): 187 | // 4. for j in (0, ..., m - 1): 188 | // 5. elm_offset = L * (j + i * m) 189 | // 6. tv = substr(uniform_bytes, elm_offset, HTF_L) 190 | // uint8 HTF_L = 64; 191 | // bytes memory tv = new bytes(64); 192 | // 7. e_j = OS2IP(tv) mod p 193 | // 8. u_i = (e_0, ..., e_(m - 1)) 194 | // tv = bytes.concat(pseudo_random_bytes[0], pseudo_random_bytes[1]); 195 | u[0].c0 = _modfield(pseudoRandomBytes[0], pseudoRandomBytes[1]); 196 | u[0].c1 = _modfield(pseudoRandomBytes[2], pseudoRandomBytes[3]); 197 | u[1].c0 = _modfield(pseudoRandomBytes[4], pseudoRandomBytes[5]); 198 | u[1].c1 = _modfield(pseudoRandomBytes[6], pseudoRandomBytes[7]); 199 | // 9. return (u_0, ..., u_(count - 1)) 200 | return u; 201 | } 202 | 203 | /// @notice Computes a field point from a message 204 | /// @dev Follows https://datatracker.ietf.org/doc/html/rfc9380#section-5.3 205 | /// @dev bytes32[] because len_in_bytes is always a multiple of 32 in our case even 128 206 | /// @param message Arbitrarylength byte string to be hashed 207 | /// @param dst The domain separation tag of at most 255 bytes 208 | /// @param lenInBytes The length of the requested output in bytes 209 | /// @return A field point 210 | function expandMsgXmd(bytes memory message, bytes memory dst, uint16 lenInBytes) 211 | private 212 | pure 213 | returns (bytes32[] memory) 214 | { 215 | // 1. ell = ceil(len_in_bytes / b_in_bytes) 216 | // b_in_bytes seems to be 32 for sha256 217 | // ceil the division 218 | uint256 ell = (lenInBytes - 1) / 32 + 1; 219 | 220 | // 2. ABORT if ell > 255 or len_in_bytes > 65535 or len(DST) > 255 221 | require(ell <= 255, "len_in_bytes too large for sha256"); 222 | // Not really needed because of parameter type 223 | // require(lenInBytes <= 65535, "len_in_bytes too large"); 224 | // no length normalizing via hashing 225 | require(dst.length <= 255, "dst too long"); 226 | 227 | bytes memory dstPrime = bytes.concat(dst, bytes1(uint8(dst.length))); 228 | 229 | // 4. Z_pad = I2OSP(0, s_in_bytes) 230 | // this should be sha256 blocksize so 64 bytes 231 | bytes memory zPad = new bytes(64); 232 | 233 | // 5. l_i_b_str = I2OSP(len_in_bytes, 2) 234 | // length in byte string? 235 | bytes2 libStr = bytes2(lenInBytes); 236 | 237 | // 6. msg_prime = Z_pad || msg || l_i_b_str || I2OSP(0, 1) || DST_prime 238 | bytes memory msgPrime = bytes.concat(zPad, message, libStr, hex"00", dstPrime); 239 | 240 | // 7. b_0 = H(msg_prime) 241 | bytes32 b_0 = sha256(msgPrime); 242 | 243 | bytes32[] memory b = new bytes32[](ell); 244 | 245 | // 8. b_1 = H(b_0 || I2OSP(1, 1) || DST_prime) 246 | b[0] = sha256(bytes.concat(b_0, hex"01", dstPrime)); 247 | 248 | // 9. for i in (2, ..., ell): 249 | for (uint8 i = 2; i <= ell; i++) { 250 | // 10. b_i = H(strxor(b_0, b_(i - 1)) || I2OSP(i, 1) || DST_prime) 251 | bytes memory tmp = abi.encodePacked(b_0 ^ b[i - 2], i, dstPrime); 252 | b[i - 1] = sha256(tmp); 253 | } 254 | // 11. uniform_bytes = b_1 || ... || b_ell 255 | // 12. return substr(uniform_bytes, 0, len_in_bytes) 256 | // Here we don't need the uniform_bytes because b is already properly formed 257 | return b; 258 | } 259 | 260 | // passing two bytes32 instead of bytes memory saves approx 700 gas per call 261 | // Computes the mod against the bls12-381 field modulus 262 | function _modfield(bytes32 _b1, bytes32 _b2) private view returns (Fp memory r) { 263 | (bool success, bytes memory output) = address(0x5).staticcall( 264 | abi.encode( 265 | // arg[0] = base.length 266 | 0x40, 267 | // arg[1] = exp.length 268 | 0x20, 269 | // arg[2] = mod.length 270 | 0x40, 271 | // arg[3] = base.bits 272 | // places the first 32 bytes of _b1 and the last 32 bytes of _b2 273 | _b1, 274 | _b2, 275 | // arg[4] = exp 276 | // exponent always 1 277 | 1, 278 | // arg[5] = mod 279 | // this field_modulus as hex 4002409555221667393417789825735904156556882819939007885332058136124031650490837864442687629129015664037894272559787 280 | // we add the 0 prefix so that the result will be exactly 64 bytes 281 | // saves 300 gas per call instead of sending it along every time 282 | // places the first 32 bytes and the last 32 bytes of the field modulus 283 | 0x000000000000000000000000000000001a0111ea397fe69a4b1ba7b6434bacd7, 284 | 0x64774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab 285 | ) 286 | ); 287 | require(success, "MODEXP failed"); 288 | return abi.decode(output, (Fp)); 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/sign/Secp256r1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | //( @title Secp256r1 5 | /// @notice Wrapper function to abstract low level details of call to the Secp256r1 6 | /// signature verification precompile as defined in EIP-7212, see 7 | /// . 8 | library Secp256r1 { 9 | /// @notice P256VERIFY operation 10 | /// @param digest 32 bytes of the signed data hash 11 | /// @param r 32 bytes of the r component of the signature 12 | /// @param s 32 bytes of the s component of the signature 13 | /// @param publicKeyX 32 bytes of the x coordinate of the public key 14 | /// @param publicKeyY 32 bytes of the y coordinate of the public key 15 | /// @return success Represents if the operation was successful 16 | function verify(bytes32 digest, bytes32 r, bytes32 s, uint256 publicKeyX, uint256 publicKeyY) 17 | internal 18 | view 19 | returns (bool) 20 | { 21 | // P256VERIFY address is 0x14 from 22 | (bool success, bytes memory output) = address(0x14).staticcall(abi.encode(digest, r, s, publicKeyX, publicKeyY)); 23 | success = success && output.length == 32 && output[31] == 0x01; 24 | 25 | return success; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/BLS.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import {Test, console} from "forge-std/Test.sol"; 5 | import {BLS} from "../src/sign/BLS.sol"; 6 | 7 | /// @notice A simple test demonstrating BLS signature verification. 8 | contract BLSTest is Test { 9 | /// @notice The generator point in G1 (P1). 10 | BLS.G1Point G1_GENERATOR = BLS.G1Point( 11 | BLS.Fp( 12 | 31827880280837800241567138048534752271, 13 | 88385725958748408079899006800036250932223001591707578097800747617502997169851 14 | ), 15 | BLS.Fp( 16 | 11568204302792691131076548377920244452, 17 | 114417265404584670498511149331300188430316142484413708742216858159411894806497 18 | ) 19 | ); 20 | 21 | /// @notice The negated generator point in G1 (-P1). 22 | BLS.G1Point NEGATED_G1_GENERATOR = BLS.G1Point( 23 | BLS.Fp( 24 | 31827880280837800241567138048534752271, 25 | 88385725958748408079899006800036250932223001591707578097800747617502997169851 26 | ), 27 | BLS.Fp( 28 | 22997279242622214937712647648895181298, 29 | 46816884707101390882112958134453447585552332943769894357249934112654335001290 30 | ) 31 | ); 32 | 33 | /// @dev Demonstrates the signing and verification of a message. 34 | function test() public { 35 | // Obtain the private key as a random scalar. 36 | uint256 privateKey = vm.randomUint(); 37 | 38 | // Public key is the generator point multiplied by the private key. 39 | BLS.G1Point memory publicKey = BLS.G1Mul(G1_GENERATOR, privateKey); 40 | 41 | // Compute the message point by mapping message's keccak256 hash to a point in G2. 42 | bytes memory message = "hello world"; 43 | BLS.G2Point memory messagePoint = BLS.MapFp2ToG2(BLS.Fp2(BLS.Fp(0, 0), BLS.Fp(0, uint256(keccak256(message))))); 44 | 45 | // Obtain the signature by multiplying the message point by the private key. 46 | BLS.G2Point memory signature = BLS.G2Mul(messagePoint, privateKey); 47 | 48 | // Invoke the pairing check to verify the signature. 49 | BLS.G1Point[] memory g1Points = new BLS.G1Point[](2); 50 | g1Points[0] = NEGATED_G1_GENERATOR; 51 | g1Points[1] = publicKey; 52 | 53 | BLS.G2Point[] memory g2Points = new BLS.G2Point[](2); 54 | g2Points[0] = signature; 55 | g2Points[1] = messagePoint; 56 | 57 | assertTrue(BLS.Pairing(g1Points, g2Points)); 58 | } 59 | 60 | /// @dev Demonstrates the aggregation and verification of two signatures. 61 | function testAggregated() public { 62 | // private keys 63 | uint256 sk1 = vm.randomUint(); 64 | uint256 sk2 = vm.randomUint(); 65 | 66 | // public keys 67 | BLS.G1Point memory pk1 = BLS.G1Mul(G1_GENERATOR, sk1); 68 | BLS.G1Point memory pk2 = BLS.G1Mul(G1_GENERATOR, sk2); 69 | 70 | // Compute the message point by mapping message's keccak256 hash to a point in G2. 71 | bytes memory message = "hello world"; 72 | BLS.G2Point memory messagePoint = BLS.MapFp2ToG2(BLS.Fp2(BLS.Fp(0, 0), BLS.Fp(0, uint256(keccak256(message))))); 73 | 74 | // signatures 75 | BLS.G2Point memory sig1 = BLS.G2Mul(messagePoint, sk1); 76 | BLS.G2Point memory sig2 = BLS.G2Mul(messagePoint, sk2); 77 | 78 | // aggregated signature 79 | BLS.G2Point memory sig = BLS.G2Add(sig1, sig2); 80 | 81 | // Invoke the pairing check to verify the signature. 82 | BLS.G1Point[] memory g1Points = new BLS.G1Point[](3); 83 | g1Points[0] = NEGATED_G1_GENERATOR; 84 | g1Points[1] = pk1; 85 | g1Points[2] = pk2; 86 | 87 | BLS.G2Point[] memory g2Points = new BLS.G2Point[](3); 88 | g2Points[0] = sig; 89 | g2Points[1] = messagePoint; 90 | g2Points[2] = messagePoint; 91 | 92 | assertTrue(BLS.Pairing(g1Points, g2Points)); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /test/P256.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.23; 3 | 4 | import {Test, console} from "forge-std/Test.sol"; 5 | import {Secp256r1} from "../src/sign/Secp256r1.sol"; 6 | 7 | /// @notice A simple test demonstrating P256 signature verification. 8 | contract BLSTest is Test { 9 | function test() public { 10 | // Obtain the private key and derive the public key. 11 | uint256 privateKey = vm.randomUint(); 12 | (uint256 publicKeyX, uint256 publicKeyY) = vm.publicKeyP256(privateKey); 13 | 14 | bytes memory message = "hello world"; 15 | bytes32 digest = keccak256(message); 16 | 17 | // Sign the hashed message. 18 | (bytes32 r, bytes32 s) = vm.signP256(privateKey, digest); 19 | 20 | // Verify the signature. 21 | assertTrue(Secp256r1.verify(digest, r, s, publicKeyX, publicKeyY)); 22 | } 23 | } 24 | --------------------------------------------------------------------------------