Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes me think we should create an alloy-bls package which also includes a tx signer for the AlphaNet payloads for the BatchContract -- cc @jxom

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