-
Notifications
You must be signed in to change notification settings - Fork 16
BLS example with Rust integration #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
a1288ae
bls multisig
klkvr f4e9932
use hashToCurve and remove unsafe
klkvr f257ddc
restructure + python example
klkvr f89b6e1
rename package
klkvr dc09964
Update src/BLSMultisig.sol
klkvr d86fe50
update doc
klkvr 69dd7cd
fix comments
klkvr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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