Skip to content

Commit

Permalink
Merge pull request #9 from klkvr/klkvr/more-examples
Browse files Browse the repository at this point in the history
BLS example with Rust integration
  • Loading branch information
klkvr authored Oct 4, 2024
2 parents 9096028 + 69dd7cd commit cc6edd5
Show file tree
Hide file tree
Showing 8 changed files with 491 additions and 0 deletions.
1 change: 1 addition & 0 deletions examples/bls-multisig/python/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.venv/
19 changes: 19 additions & 0 deletions examples/bls-multisig/python/README.md
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.
105 changes: 105 additions & 0 deletions examples/bls-multisig/python/multisig.py
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()
2 changes: 2 additions & 0 deletions examples/bls-multisig/rust/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target/
Cargo.lock
15 changes: 15 additions & 0 deletions examples/bls-multisig/rust/Cargo.toml
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"
100 changes: 100 additions & 0 deletions examples/bls-multisig/rust/src/main.rs
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());
}
112 changes: 112 additions & 0 deletions src/BLSMultisig.sol
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 {}
}
Loading

0 comments on commit cc6edd5

Please sign in to comment.