Skip to content

Commit

Permalink
BLS example (#3)
Browse files Browse the repository at this point in the history
* init

* update context

* note on Python

* fix

* move instructions

* alphanet -> odyssey

* EOF (#4)

* wip

* fixes
  • Loading branch information
klkvr authored Oct 9, 2024
1 parent 965aa1c commit 787564f
Show file tree
Hide file tree
Showing 14 changed files with 1,113 additions and 3 deletions.
3 changes: 1 addition & 2 deletions chapter1/.gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
cache/
out/
lib/
*.pem
*.pem
228 changes: 228 additions & 0 deletions chapter1/bls-multisig/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
# BLS Multisig

## Context
Imagine a DAO consisting of 100 members (signers in total), where at least 50 members need to approve any transfer of funds from the treasury to ensure that funds cannot be misused without sufficient consensus from the key holders.

EIP-2537 introduces a set of precompiled contracts enabling elliptic curve operations directly on the Ethereum Virtual Machine (EVM). This makes it feasible to use BLS signatures natively on Ethereum and dramatically reduces the cost and complexity of these operations, unlocking efficient use of BLS-based schemes like multisig.

## Implementation

### Contract
We demonstrate a simple multisignature contract [BLSMultisig](../contracts/BLSMultisig.sol) which keeps a list of signers public keys and allows executing arbitrary operations which are signed by a subset of signers. Both stored public keys and signatures can be aggregated, thus allowing for much better scalability for large numbers of signers vs ECDSA. Let's walk through the contract's code.

BLS signing operates on two curves: G1 and G2. In our case we will store public keys on G1 while signatures and messages will be on G2. To sign or verify a message consisting of arbitrary bytes, we need to firstly map the message to a point on G2. There is a commonly used [algorithm](https://datatracker.ietf.org/doc/html/rfc9380#name-hashing-to-a-finite-field) for this mapping, we are using its implementation in Solidity:

```solidity
/// @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));
}
```

Second contract's method `verifyAndExecute` contains core logic for signature verification, let's walk through it.

We start with aggregating the signers public keys into a single point on G1. This is done by simply invoking G1ADD precompile with all public keys. After this step, we will have a single point on G1 which represents the aggregated public key of all signers. We require signers to be sorted to ensure that all signers are unique and valid.

```solidity
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");
}
}
```

After that, we perform signature verification, by invoking the PAIRING precompile with the aggregated public key and the signature. Notice that we are invoking `getOperationPoint` method we've defined earlier to map the operation to a point on G2 which we can verify against the signature.
```solidity
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");
```

If all of those steps are successful, we can execute the operation.

### Integration

We've prepated 2 code snippets demonstrating integration of the above contract to aggregate and submit signatures obtained off-chain. Examples are written in [Rust](./rust) and [Python](./python). We will walk through the Rust code, but the Python code is very similar.

We will use [blst](https://github.com/supranational/blst) library for BLS operations.

Let's start with generating bindings for our contract.
```rust
alloy::sol! {
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
#[sol(rpc)]
BLSMultisig,
"../out/BLSMultisig.sol/BLSMultisig.json"
}
```

This will generate bindings for `BLSMultisig` and `BLS`, allowing us to reuse the same G1/G2 structures in Rust code.

Now, let's define helpers for converting between our contract's structures and `blst` types. `blst` provides serialization methods for both G1 and G2 points which are a bit different from the format defined in [EIP-2537](https://eips.ethereum.org/EIPS/eip-2537). Converting between the two requires some bit manipulation.
```rust
use blst::min_pk::{PublicKey, Signature};

/// Converts a blst [`PublicKey`] to a [`BLS::G1Point`] which can be passed to the contract
impl From<PublicKey> for BLS::G1Point {
fn from(value: PublicKey) -> Self {
let serialized = value.serialize();

let mut data = [0u8; 128];
data[16..64].copy_from_slice(&serialized[0..48]);
data[80..128].copy_from_slice(&serialized[48..96]);

BLS::G1Point::abi_decode(&data, false).unwrap()
}
}

/// Converts a blst [`Signature`] to a [`BLS::G2Point`] which can be passed to the contract
impl From<Signature> for BLS::G2Point {
fn from(value: Signature) -> Self {
let serialized = value.serialize();

let mut data = [0u8; 256];
data[16..64].copy_from_slice(&serialized[48..96]);
data[80..128].copy_from_slice(&serialized[0..48]);
data[144..192].copy_from_slice(&serialized[144..192]);
data[208..256].copy_from_slice(&serialized[96..144]);

BLS::G2Point::abi_decode(&data, false).unwrap()
}
}

```

Next, let's define helpers for generating BLS keys and signing messages.
```rust
use blst::min_pk::{AggregateSignature, PublicKey, SecretKey, Signature};

/// Generates `num` BLS keys and returns them as a tuple of private and public keys
fn generate_keys(num: usize) -> (Vec<SecretKey>, Vec<BLS::G1Point>) {
let mut rng = rand::thread_rng();

let mut public = Vec::with_capacity(num);
let mut private = 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::from(sk.sk_to_pk());

public.push(pk);
private.push(sk);
}

(private, public)
}

/// Signs a message with the provided keys and returns the aggregated signature.
fn sign_message(keys: &[&SecretKey], msg: &[u8]) -> BLS::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);
}

// aggregate
Signature::from_aggregate(
&AggregateSignature::aggregate(sigs.iter().collect::<Vec<_>>().as_slice(), false).unwrap(),
)
.into()
}
```

We now have all the pieces we need to interact with our contract. Let's try to send a simple operation.

Firstly, we need to launch Anvil node and connect to it.
```rust
// Spawn Anvil node in --odyssey mode
let provider = ProviderBuilder::new().on_anvil_with_config(|config| config.arg("--odyssey"));
```

Let's now setup our multisig contract.
```rust
// Generate 100 BLS keys
let (private_keys, public_keys) = generate_keys(100);

// Deploy multisig contract, configuring generated keys as signers and requiring threshold of 50
let multisig = BLSMultisig::deploy(&provider, public_keys.clone(), U256::from(50)).await?;

// Fund multisig with some ETH
provider
.send_transaction(
TransactionRequest::default()
.to(*multisig.address())
.with_value(U256::from(1_000_000_000_000_000_000u128)),
)
.await?
.watch()
.await?;
```

At this point we should be able to sign any operation with at least 50 signers, and execute it on behalf of the multisig contract. Let's transfer 1 ETH to a random address:
```rust
let operation = BLSMultisig::Operation {
to: Address::random(),
value: U256::from(1_000_000_000_000_000_000u128),
nonce: multisig.nonce().call().await?._0,
data: Default::default(),
};
```

Firstly, we choose 50 random signers from our set of 100 keys to sign the operation.
```rust
let (keys, signers): (Vec<_>, Vec<_>) = {
let mut pairs = private_keys
.iter()
.zip(public_keys.clone())
.choose_multiple(&mut rand::thread_rng(), 50);

// contract requires signers to be sorted by public key
pairs.sort_by(|(_, pk1), (_, pk2)| pk1.cmp(pk2));

pairs.into_iter().unzip()
};
```

Then, we sign the operation with the chosen keys.
```rust
let signature = sign_message(&keys, &operation.abi_encode());
```

Finally, we send the signed operation to the contract along with the list of signers.
```rust
let receipt = multisig
.verifyAndExecute(BLSMultisig::SignedOperation {
operation: operation.clone(),
signers,
signature,
})
.send()
.await?
.get_receipt()
.await?;

// Assert that the transaction was successful and that recipient has received the funds
assert!(receipt.status());
assert!(provider.get_balance(operation.to).await? > U256::ZERO);
```
1 change: 1 addition & 0 deletions chapter1/bls-multisig/python/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.venv/
19 changes: 19 additions & 0 deletions chapter1/bls-multisig/python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Python BLS Multisig

This example demonstrates an integration of [BlsMultisig](../../contracts/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 with Odyssey features enabled, deploy the multisig contract and execute a simple operation signed by random BLS keys.
105 changes: 105 additions & 0 deletions chapter1/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", "--odyssey"], stdout=subprocess.PIPE)
asyncio.run(main())
finally:
anvil.terminate()
2 changes: 2 additions & 0 deletions chapter1/bls-multisig/rust/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
target/
Cargo.lock
17 changes: 17 additions & 0 deletions chapter1/bls-multisig/rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "odyssey-bls-multisig"
version = "0.1.0"
edition = "2021"

[dependencies]
alloy = { version = "0.4", features = [
"providers",
"contract",
"sol-types",
"node-bindings",
"rpc-types",
"getrandom",
] }
tokio = { version = "1", features = ["full"] }
blst = "0.3"
rand = "0.8"
13 changes: 13 additions & 0 deletions chapter1/bls-multisig/rust/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Rust BLS Multisig

This example demonstrates an integration of [BlsMultisig](../../contracts/BLSMultisig.sol) with Rust.

## Running the example

To run the example, just run the following command:

```shell
cargo run
```

This will spin up an Anvil instance with Odyssey features enabled, deploy the multisig contract and execute a simple operation signed by random BLS keys.
Loading

0 comments on commit 787564f

Please sign in to comment.