-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #9 from klkvr/klkvr/more-examples
BLS example with Rust integration
- Loading branch information
Showing
8 changed files
with
491 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
.venv/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Python Alphanet Multisig | ||
|
||
This example demonstrates an integration of [BlsMultisig](../../../src/BLSMultisig.sol) with Python. | ||
|
||
## Running the example | ||
|
||
To run the example, you will need to install the required dependencies: | ||
|
||
```shell | ||
pip install web3 py_ecc | ||
``` | ||
|
||
Then, you can run the example by executing the following command: | ||
|
||
```shell | ||
python multisig.py | ||
``` | ||
|
||
This will spin up an Anvil instance in Alphanet mode, deploy the multisig contract and execute a simple operation signed by random BLS keys. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
from web3 import AsyncWeb3 | ||
import pathlib | ||
import asyncio | ||
import json | ||
import subprocess | ||
import random | ||
from py_ecc.bls import G2Basic | ||
from py_ecc.bls import g2_primitives | ||
import eth_abi | ||
|
||
Fp = tuple[int, int] | ||
Fp2 = tuple[Fp, Fp] | ||
G1Point = tuple[Fp, Fp] | ||
G2Point = tuple[Fp2, Fp2] | ||
Operation = tuple[str, str, int, int] | ||
|
||
|
||
def fp_from_int(x: int) -> Fp: | ||
b = x.to_bytes(64, "big") | ||
return (int.from_bytes(b[:32], "big"), int.from_bytes(b[32:], "big")) | ||
|
||
|
||
def generate_keys(num: int) -> list[tuple[G1Point, int]]: | ||
keypairs = [] | ||
for _ in range(num): | ||
sk = random.randint(0, 10**30) | ||
pk_point = g2_primitives.pubkey_to_G1(G2Basic.SkToPk(sk)) | ||
|
||
pk = (fp_from_int(int(pk_point[0])), fp_from_int(int(pk_point[1]))) | ||
|
||
keypairs.append((pk, sk)) | ||
|
||
keypairs.sort() | ||
|
||
return keypairs | ||
|
||
|
||
def sign_operation(sks: list[int], operation: Operation) -> G2Point: | ||
encoded = eth_abi.encode(["(address,bytes,uint256,uint256)"], [operation]) | ||
|
||
signatures = [] | ||
for sk in sks: | ||
signatures.append(G2Basic.Sign(sk, encoded)) | ||
|
||
aggregated = g2_primitives.signature_to_G2(G2Basic.Aggregate(signatures)) | ||
|
||
signature = ( | ||
(fp_from_int(aggregated[0].coeffs[0]), fp_from_int(aggregated[0].coeffs[1])), | ||
(fp_from_int(aggregated[1].coeffs[0]), fp_from_int(aggregated[1].coeffs[1])), | ||
) | ||
|
||
return signature | ||
|
||
|
||
async def main(): | ||
bls_multisig_artifact = json.load( | ||
open(pathlib.Path(__file__).parent.parent.parent.parent / "out/BLSMultisig.sol/BLSMultisig.json") | ||
) | ||
|
||
web3 = AsyncWeb3(AsyncWeb3.AsyncHTTPProvider("http://localhost:8545")) | ||
|
||
bytecode = bls_multisig_artifact["bytecode"]["object"] | ||
abi = bls_multisig_artifact["abi"] | ||
BlsMultisig = web3.eth.contract(abi=abi, bytecode=bytecode) | ||
|
||
signer = (await web3.eth.accounts)[0] | ||
|
||
# generate 100 BLS keys | ||
keypairs = generate_keys(100) | ||
pks = list(map(lambda x: x[0], keypairs)) | ||
|
||
# deploy the multisig contract with generated signers and threshold of 50 | ||
tx = await BlsMultisig.constructor(pks, 50).transact({"from": signer}) | ||
receipt = await web3.eth.wait_for_transaction_receipt(tx) | ||
multisig = BlsMultisig(receipt.contractAddress) | ||
|
||
# fund the multisig | ||
hash = await web3.eth.send_transaction({"from": signer, "to": multisig.address, "value": 10**18}) | ||
await web3.eth.wait_for_transaction_receipt(hash) | ||
|
||
# create an operation transferring 1 eth to zero address | ||
operation: Operation = ("0x0000000000000000000000000000000000000000", bytes(), 10**18, 0) | ||
|
||
# choose 50 random signers that will sign the operation | ||
signers_subset = sorted(random.sample(keypairs, 50)) | ||
|
||
pks = list(map(lambda x: x[0], signers_subset)) | ||
sks = list(map(lambda x: x[1], signers_subset)) | ||
|
||
# create aggregated signature for operation | ||
signature = sign_operation(sks, operation) | ||
|
||
# execute the operation | ||
tx = await multisig.functions.verifyAndExecute((operation, pks, signature)).transact({"from": signer}) | ||
receipt = await web3.eth.wait_for_transaction_receipt(tx) | ||
|
||
assert receipt.status == 1 | ||
|
||
|
||
if __name__ == "__main__": | ||
try: | ||
anvil = subprocess.Popen(["anvil", "--alphanet"], stdout=subprocess.PIPE) | ||
asyncio.run(main()) | ||
finally: | ||
anvil.terminate() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
target/ | ||
Cargo.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
[package] | ||
name = "alphanet-bls-multisig" | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
[dependencies] | ||
alloy = { version = "0.4", features = [ | ||
"providers", | ||
"contract", | ||
"sol-types", | ||
"node-bindings", | ||
] } | ||
tokio = { version = "1", features = ["full"] } | ||
blst = "0.3" | ||
rand = "0.8" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
use alloy::{primitives::U256, providers::ProviderBuilder, sol, sol_types::SolValue}; | ||
use blst::min_pk::{AggregateSignature, SecretKey, Signature}; | ||
use rand::RngCore; | ||
use BLS::G2Point; | ||
|
||
sol! { | ||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord)] | ||
#[sol(rpc)] | ||
BLSMultisig, | ||
"../../../out/BLSMultisig.sol/BLSMultisig.json" | ||
} | ||
|
||
impl From<[u8; 96]> for BLS::G1Point { | ||
fn from(value: [u8; 96]) -> Self { | ||
let mut data = [0u8; 128]; | ||
data[16..64].copy_from_slice(&value[0..48]); | ||
data[80..128].copy_from_slice(&value[48..96]); | ||
|
||
BLS::G1Point::abi_decode(&data, false).unwrap() | ||
} | ||
} | ||
|
||
impl From<[u8; 192]> for BLS::G2Point { | ||
fn from(value: [u8; 192]) -> Self { | ||
let mut data = [0u8; 256]; | ||
data[16..64].copy_from_slice(&value[48..96]); | ||
data[80..128].copy_from_slice(&value[0..48]); | ||
data[144..192].copy_from_slice(&value[144..192]); | ||
data[208..256].copy_from_slice(&value[96..144]); | ||
|
||
BLS::G2Point::abi_decode(&data, false).unwrap() | ||
} | ||
} | ||
|
||
/// Generates `num` BLS keys and returns them as a tuple of secret keys and public keys, sorted by public key. | ||
fn generate_keys(num: usize) -> (Vec<SecretKey>, Vec<BLS::G1Point>) { | ||
let mut rng = rand::thread_rng(); | ||
let mut keys = Vec::with_capacity(num); | ||
|
||
for _ in 0..num { | ||
let mut ikm = [0u8; 32]; | ||
rng.fill_bytes(&mut ikm); | ||
|
||
let sk = SecretKey::key_gen(&ikm, &[]).unwrap(); | ||
let pk: BLS::G1Point = sk.sk_to_pk().serialize().into(); | ||
|
||
keys.push((sk, pk)); | ||
} | ||
|
||
keys.sort_by(|(_, pk1), (_, pk2)| pk1.cmp(pk2)); | ||
|
||
keys.into_iter().unzip() | ||
} | ||
|
||
/// Signs a message with the provided keys and returns the aggregated signature. | ||
fn sign_message(keys: &[SecretKey], msg: &[u8]) -> G2Point { | ||
let mut sigs = Vec::new(); | ||
|
||
// create individual signatures | ||
for key in keys { | ||
let sig = key.sign(msg, b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_", &[]); | ||
sigs.push(sig); | ||
} | ||
|
||
let agg_sig = Signature::from_aggregate( | ||
&AggregateSignature::aggregate(sigs.iter().collect::<Vec<_>>().as_slice(), false).unwrap(), | ||
); | ||
|
||
agg_sig.serialize().into() | ||
} | ||
|
||
#[tokio::main] | ||
pub async fn main() { | ||
let provider = ProviderBuilder::new().on_anvil_with_config(|config| config.arg("--alphanet")); | ||
|
||
let (keys, signers) = generate_keys(100); | ||
|
||
let multisig = BLSMultisig::deploy(provider, signers.clone(), U256::from(1)) | ||
.await | ||
.unwrap(); | ||
|
||
let operation = BLSMultisig::Operation::default(); | ||
|
||
let signature = sign_message(&keys, &operation.abi_encode()); | ||
|
||
let receipt = multisig | ||
.verifyAndExecute(BLSMultisig::SignedOperation { | ||
operation: operation.clone(), | ||
signers, | ||
signature, | ||
}) | ||
.send() | ||
.await | ||
.unwrap() | ||
.get_receipt() | ||
.await | ||
.unwrap(); | ||
|
||
assert!(receipt.status()); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.23; | ||
|
||
import {BLS} from "./sign/BLS.sol"; | ||
|
||
/// @notice BLS-powered multisignature wallet, demonstrating the use of | ||
/// aggregated BLS signatures for verification | ||
/// @dev This is for demonstration purposes only, do not use in production. This contract does | ||
/// not include protection from rogue public-key attacks. | ||
contract BLSMultisig { | ||
/// @notice Public keys of signers. This may contain a pre-aggregated | ||
/// public keys for common sets of signers as well. | ||
mapping(bytes32 => bool) public signers; | ||
|
||
struct Operation { | ||
address to; | ||
bytes data; | ||
uint256 value; | ||
uint256 nonce; | ||
} | ||
|
||
struct SignedOperation { | ||
Operation operation; | ||
BLS.G1Point[] signers; | ||
BLS.G2Point signature; | ||
} | ||
|
||
/// @notice The negated generator point in G1 (-P1). Used during pairing as a first G1 point. | ||
BLS.G1Point NEGATED_G1_GENERATOR = BLS.G1Point( | ||
BLS.Fp( | ||
31827880280837800241567138048534752271, | ||
88385725958748408079899006800036250932223001591707578097800747617502997169851 | ||
), | ||
BLS.Fp( | ||
22997279242622214937712647648895181298, | ||
46816884707101390882112958134453447585552332943769894357249934112654335001290 | ||
) | ||
); | ||
|
||
/// @notice The number of signatures required to execute an operation. | ||
uint256 public threshold; | ||
|
||
/// @notice Nonce used for replay protection. | ||
uint256 public nonce; | ||
|
||
constructor(BLS.G1Point[] memory _signers, uint256 _threshold) { | ||
for (uint256 i = 0; i < _signers.length; i++) { | ||
signers[keccak256(abi.encode(_signers[i]))] = true; | ||
} | ||
threshold = _threshold; | ||
} | ||
|
||
/// @notice Maps an operation to a point on G2 which needs to be signed. | ||
function getOperationPoint(Operation memory op) public view returns (BLS.G2Point memory) { | ||
return BLS.hashToCurveG2(abi.encode(op)); | ||
} | ||
|
||
/// @notice Accepts an operation signed by a subset of the signers and executes it | ||
function verifyAndExecute(SignedOperation memory operation) public { | ||
require(operation.operation.nonce == nonce++, "invalid nonce"); | ||
require(operation.signers.length >= threshold, "not enough signers"); | ||
|
||
BLS.G1Point memory aggregatedSigner; | ||
|
||
for (uint256 i = 0; i < operation.signers.length; i++) { | ||
BLS.G1Point memory signer = operation.signers[i]; | ||
require(signers[keccak256(abi.encode(signer))], "invalid signer"); | ||
|
||
if (i == 0) { | ||
aggregatedSigner = signer; | ||
} else { | ||
aggregatedSigner = BLS.G1Add(aggregatedSigner, signer); | ||
require(_comparePoints(operation.signers[i - 1], signer), "signers not sorted"); | ||
} | ||
} | ||
|
||
BLS.G1Point[] memory g1Points = new BLS.G1Point[](2); | ||
BLS.G2Point[] memory g2Points = new BLS.G2Point[](2); | ||
|
||
g1Points[0] = NEGATED_G1_GENERATOR; | ||
g1Points[1] = aggregatedSigner; | ||
|
||
g2Points[0] = operation.signature; | ||
g2Points[1] = getOperationPoint(operation.operation); | ||
|
||
// verify signature | ||
require(BLS.Pairing(g1Points, g2Points), "invalid signature"); | ||
|
||
// execute operation | ||
Operation memory op = operation.operation; | ||
(bool success,) = op.to.call{value: op.value}(op.data); | ||
require(success, "execution failed"); | ||
} | ||
|
||
/// @notice Returns true if X coordinate of the first point is lower than the X coordinate of the second point. | ||
function _comparePoints(BLS.G1Point memory a, BLS.G1Point memory b) internal pure returns (bool) { | ||
BLS.Fp memory aX = a.x; | ||
BLS.Fp memory bX = b.x; | ||
|
||
if (aX.a < bX.a) { | ||
return true; | ||
} else if (aX.a > bX.a) { | ||
return false; | ||
} else if (aX.b < bX.b) { | ||
return true; | ||
} else { | ||
return false; | ||
} | ||
} | ||
|
||
receive() external payable {} | ||
} |
Oops, something went wrong.