Skip to content

Commit

Permalink
Bring over BLS contract tests
Browse files Browse the repository at this point in the history
  • Loading branch information
forshtat committed Dec 24, 2024
1 parent 7ec3998 commit d8a5940
Show file tree
Hide file tree
Showing 5 changed files with 325 additions and 4 deletions.
90 changes: 90 additions & 0 deletions contracts/test/BrokenBlsAccount.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// SPDX-License-Identifier: GPL-3.0

/* solhint-disable one-contract-per-file */
pragma solidity ^0.8.23;

import "@openzeppelin/contracts/utils/Create2.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

import "@account-abstraction/contracts/core/Helpers.sol";

import "../SimpleAccount.sol";
import "../bls/IBLSAccount.sol";

/**
* for testing: a BLS account that fails to return its public-key (completely ignores its publickey)
* this is a copy of the normal bls account, but it returns a public-key unrelated to the one it is constructed with.
*/
contract BrokenBLSAccount is SimpleAccount, IBLSAccount {
address public immutable aggregator;

// The constructor is used only for the "implementation" and only sets immutable values.
// Mutable values slots for proxy accounts are set by the 'initialize' function.
constructor(IEntryPoint anEntryPoint, address anAggregator) SimpleAccount(anEntryPoint) {
aggregator = anAggregator;
}

function initialize(uint256[4] memory aPublicKey) public virtual initializer {
(aPublicKey);
super._initialize(address(0));
}

function _validateSignature(PackedUserOperation calldata userOp, bytes32 userOpHash)
internal override view returns (uint256 validationData) {

(userOp, userOpHash);
return _packValidationData(ValidationData(aggregator, 0,0));
}

function getBlsPublicKey() external override pure returns (uint256[4] memory) {
uint256[4] memory pubkey;
return pubkey;
}
}


/**
* Based n SimpleAccountFactory
* can't be a subclass, since both constructor and createAccount depend on the
* actual wallet contract constructor and initializer
*/
contract BrokenBLSAccountFactory {
BrokenBLSAccount public immutable accountImplementation;

constructor(IEntryPoint entryPoint, address aggregator){
accountImplementation = new BrokenBLSAccount(entryPoint, aggregator);
}

/**
* create an account, and return its address.
* returns the address even if the account is already deployed.
* Note that during UserOperation execution, this method is called only if the account is not deployed.
* This method returns an existing account address so that entryPoint.getSenderAddress() would work even after account creation
* Also note that out BLSSignatureAggregator requires that the public-key is the last parameter
*/
function createAccount(uint256 salt, uint256[4] memory aPublicKey) public returns (BrokenBLSAccount) {

address addr = getAddress(salt, aPublicKey);
uint256 codeSize = addr.code.length;
if (codeSize > 0) {
return BrokenBLSAccount(payable(addr));
}
return BrokenBLSAccount(payable(new ERC1967Proxy{salt : bytes32(salt)}(
address(accountImplementation),
abi.encodeCall(BrokenBLSAccount.initialize, aPublicKey)
)));
}

/**
* calculate the counterfactual address of this account as it would be returned by createAccount()
*/
function getAddress(uint256 salt, uint256[4] memory aPublicKey) public view returns (address) {
return Create2.computeAddress(bytes32(salt), keccak256(abi.encodePacked(
type(ERC1967Proxy).creationCode,
abi.encode(
address(accountImplementation),
abi.encodeCall(BrokenBLSAccount.initialize, (aPublicKey))
)
)));
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@nomicfoundation/hardhat-verify": "^2.0.0",
"@nomiclabs/hardhat-ethers": "^2.2.3",
"@nomiclabs/hardhat-waffle": "^2.0.6",
"@thehubbleproject/bls": "^0.5.1",
"@typechain/ethers-v5": "^10.1.0",
"@typechain/hardhat": "^2.3.0",
"@types/chai": "^4.2.0",
Expand Down
8 changes: 5 additions & 3 deletions test/UserOp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export function getUserOpHash (op: UserOperation, entryPoint: string, chainId: n
}

export const DefaultsForUserOp: UserOperation = {
sender: AddressZero,
sender: ethers.constants.AddressZero,
nonce: 0,
initCode: '0x',
callData: '0x',
Expand Down Expand Up @@ -155,9 +155,11 @@ export async function fillUserOp (op: Partial<UserOperation>, entryPoint?: Entry
}
if (op1.verificationGasLimit == null) {
if (provider == null) throw new Error('no entrypoint/provider')
const senderCreator = await entryPoint?.senderCreator()
// const senderCreator = await entryPoint?.senderCreator()
const initEstimate = await provider.estimateGas({
from: senderCreator,
// TODO: call from SenderCreator
// from: senderCreator,
from: entryPoint?.address,
to: initAddr,
data: initCallData,
gasLimit: 10e6
Expand Down
206 changes: 206 additions & 0 deletions test/y.bls.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import { aggregate, BlsSignerFactory, BlsVerifier } from '@thehubbleproject/bls/dist/signer'
import { arrayify, defaultAbiCoder, hexConcat } from 'ethers/lib/utils'
import {
BLSOpen__factory,
BLSSignatureAggregator,
BLSSignatureAggregator__factory,
BLSAccount,
BLSAccount__factory,
BLSAccountFactory,
BLSAccountFactory__factory,
BrokenBLSAccountFactory__factory,
EntryPoint
} from '../typechain'
import { ethers } from 'hardhat'
import { createAddress, deployEntryPoint, fund, ONE_ETH } from './testutils'
import { DefaultsForUserOp, fillAndPack, packUserOp, simulateValidation } from './UserOp'
import { expect } from 'chai'
import { keccak256 } from 'ethereumjs-util'
import { hashToPoint } from '@thehubbleproject/bls/dist/mcl'
import { BigNumber, Signer } from 'ethers'
import { BytesLike, hexValue } from '@ethersproject/bytes'

async function deployBlsAccount (ethersSigner: Signer, factoryAddr: string, blsSigner: any): Promise<BLSAccount> {
const factory = BLSAccountFactory__factory.connect(factoryAddr, ethersSigner)
const addr = await factory.callStatic.createAccount(0, blsSigner.pubkey)
await factory.createAccount(0, blsSigner.pubkey)
return BLSAccount__factory.connect(addr, ethersSigner)
}

describe('bls account', function () {
this.timeout(20000)
const BLS_DOMAIN = arrayify(keccak256(Buffer.from('eip4337.bls.domain')))
const etherSigner = ethers.provider.getSigner()
let fact: BlsSignerFactory
let signer1: any
let signer2: any
let blsAgg: BLSSignatureAggregator
let entrypoint: EntryPoint
let account1: BLSAccount
let account2: BLSAccount
let accountDeployer: BLSAccountFactory
before(async () => {
entrypoint = await deployEntryPoint()
const BLSOpenLib = await new BLSOpen__factory(ethers.provider.getSigner()).deploy()
blsAgg = await new BLSSignatureAggregator__factory({
'contracts/bls/lib/BLSOpen.sol:BLSOpen': BLSOpenLib.address
}, ethers.provider.getSigner()).deploy(entrypoint.address)

await blsAgg.addStake(2, { value: ONE_ETH })
fact = await BlsSignerFactory.new()
signer1 = fact.getSigner(arrayify(BLS_DOMAIN), '0x01')
signer2 = fact.getSigner(arrayify(BLS_DOMAIN), '0x02')

accountDeployer = await new BLSAccountFactory__factory(etherSigner).deploy(entrypoint.address, blsAgg.address)

account1 = await deployBlsAccount(etherSigner, accountDeployer.address, signer1)
account2 = await deployBlsAccount(etherSigner, accountDeployer.address, signer2)
})

it('#getTrailingPublicKey', async () => {
const data = defaultAbiCoder.encode(['uint[6]'], [[1, 2, 3, 4, 5, 6]])
const last4 = await blsAgg.getTrailingPublicKey(data)
expect(last4.map(x => x.toNumber())).to.eql([3, 4, 5, 6])
})
it('#aggregateSignatures', async () => {
const sig1 = signer1.sign('0x1234')
const sig2 = signer2.sign('0x5678')
const offChainSigResult = hexConcat(aggregate([sig1, sig2]))
const userOp1 = packUserOp({ ...DefaultsForUserOp, signature: hexConcat(sig1) })
const userOp2 = packUserOp({ ...DefaultsForUserOp, signature: hexConcat(sig2) })
const solidityAggResult = await blsAgg.aggregateSignatures([userOp1, userOp2])
expect(solidityAggResult).to.equal(offChainSigResult)
})

it('#userOpToMessage', async () => {
const userOp1 = await fillAndPack({
sender: account1.address
}, entrypoint)
const requestHash = await blsAgg.getUserOpHash(userOp1)
const solPoint: BigNumber[] = await blsAgg.userOpToMessage(userOp1)
const messagePoint = hashToPoint(requestHash, BLS_DOMAIN)
expect(`1 ${solPoint[0].toString()} ${solPoint[1].toString()}`).to.equal(messagePoint.getStr())
})

it('#validateUserOpSignature', async () => {
const userOp1 = await fillAndPack({
sender: account1.address
}, entrypoint)
const requestHash = await blsAgg.getUserOpHash(userOp1)

const sigParts = signer1.sign(requestHash)
userOp1.signature = hexConcat(sigParts)
expect(userOp1.signature.length).to.equal(130) // 64-byte hex value

const verifier = new BlsVerifier(BLS_DOMAIN)
expect(verifier.verify(sigParts, signer1.pubkey, requestHash)).to.equal(true)

const ret = await blsAgg.validateUserOpSignature(userOp1)
expect(ret).to.equal('0x')
})

it('aggregated sig validation must succeed if off-chain UserOp sig succeeds', async () => {
// regression AA-119: prevent off-chain signature success and on-chain revert.
// "broken account" uses different public-key during construction and runtime.
const brokenAccountFactory = await new BrokenBLSAccountFactory__factory(etherSigner).deploy(entrypoint.address, blsAgg.address)
// const brokenAccountFactory = await new BLSAccountFactory__factory(etherSigner).deploy(entrypoint.address, blsAgg.address)
const deployTx = await brokenAccountFactory.populateTransaction.createAccount(0, signer1.pubkey)
const res = await brokenAccountFactory.provider.call(deployTx)
const acc = brokenAccountFactory.interface.decodeFunctionResult('createAccount', res)[0]
await fund(acc)
const userOp = await fillAndPack({
sender: acc,
initCode: hexConcat([brokenAccountFactory.address, deployTx.data!])
}, entrypoint)
const requestHash = await blsAgg.getUserOpHash(userOp)
const signature = userOp.signature = hexConcat(signer1.sign(requestHash))

// and sig validation should fail:
const singleOpSigCheck = await blsAgg.validateUserOpSignature(userOp).then(() => 'ok', e => e.message) as string

// above account should fail on-chain:
const beneficiary = createAddress()
const handleRet = await entrypoint.callStatic.handleAggregatedOps([
{
userOps: [userOp],
aggregator: blsAgg.address,
signature
}
], beneficiary).then(() => 'ok', e => e.errorName) as string

expect(`${singleOpSigCheck},${handleRet}`)
.to.eq('ok,ok')
})

it('validateSignatures', async function () {
// yes, it does take long on hardhat, but quick on geth.
this.timeout(30000)
const userOp1 = await fillAndPack({
sender: account1.address
}, entrypoint)
const requestHash = await blsAgg.getUserOpHash(userOp1)
const sig1 = signer1.sign(requestHash)
userOp1.signature = hexConcat(sig1)

const userOp2 = await fillAndPack({
sender: account2.address
}, entrypoint)
const requestHash2 = await blsAgg.getUserOpHash(userOp2)
const sig2 = signer2.sign(requestHash2)
userOp2.signature = hexConcat(sig2)

const aggSig = aggregate([sig1, sig2])
const aggregatedSig = await blsAgg.aggregateSignatures([userOp1, userOp2])
expect(hexConcat(aggSig)).to.equal(aggregatedSig)

const pubkeys = [
signer1.pubkey,
signer2.pubkey
]
const v = new BlsVerifier(BLS_DOMAIN)
// off-chain check
const now = Date.now()
expect(v.verifyMultiple(aggSig, pubkeys, [requestHash, requestHash2])).to.equal(true)
console.log('verifyMultiple (mcl code)', Date.now() - now, 'ms')
const now2 = Date.now()
console.log('validateSignatures gas= ', await blsAgg.estimateGas.validateSignatures([userOp1, userOp2], aggregatedSig))
console.log('validateSignatures (on-chain)', Date.now() - now2, 'ms')
})

describe('#EntryPoint.simulateValidation with aggregator', () => {
let initCode: BytesLike
let signer3: any
before(async () => {
signer3 = fact.getSigner(arrayify(BLS_DOMAIN), '0x03')
initCode = hexConcat([
accountDeployer.address,
accountDeployer.interface.encodeFunctionData('createAccount', [0, signer3.pubkey])
])
})

it('validate after simulation returns ValidationResultWithAggregation', async () => {
const verifier = new BlsVerifier(BLS_DOMAIN)
const senderAddress = await entrypoint.callStatic.getSenderAddress(initCode).catch(e => e.errorArgs.sender)
await fund(senderAddress, '0.01')
const userOp = await fillAndPack({
sender: senderAddress,
initCode
}, entrypoint)
const requestHash = await blsAgg.getUserOpHash(userOp)
const sigParts = signer3.sign(requestHash)
userOp.signature = hexConcat(sigParts)

const { aggregatorInfo } = await simulateValidation(userOp, entrypoint.address)
expect(aggregatorInfo.aggregator).to.eq(blsAgg.address)
expect(aggregatorInfo.stakeInfo.stake).to.eq(ONE_ETH)
expect(aggregatorInfo.stakeInfo.unstakeDelaySec).to.eq(2)

const [signature] = defaultAbiCoder.decode(['bytes32[2]'], userOp.signature)
const pubkey = (await blsAgg.getUserOpPublicKey(userOp)).map(n => hexValue(n)) // TODO: returns uint256[4], verify needs bytes32[4]
const requestHash1 = await blsAgg.getUserOpHash(userOp)

// @ts-ignore
expect(verifier.verify(signature, pubkey, requestHash1)).to.equal(true)
})
})
})
24 changes: 23 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1060,6 +1060,14 @@
dependencies:
defer-to-connect "^2.0.0"

"@thehubbleproject/bls@^0.5.1":
version "0.5.1"
resolved "https://registry.yarnpkg.com/@thehubbleproject/bls/-/bls-0.5.1.tgz#6b0565f56fc9c8896dcf3c8f0e2214b69a06167f"
integrity sha512-g5zeMZ8js/yg6MjFoC+pt0eqfCL2jC46yLY1LbKNriyqftB1tE3jpG/FMMDIW3x9/yRg/AgUb8Nluqj15tQs+A==
dependencies:
ethers "^5.5.3"
mcl-wasm "^1.0.0"

"@tsconfig/node10@^1.0.7":
version "1.0.11"
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2"
Expand Down Expand Up @@ -1248,6 +1256,13 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240"
integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==

"@types/node@^20.2.5":
version "20.17.10"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.17.10.tgz#3f7166190aece19a0d1d364d75c8b0b5778c1e18"
integrity sha512-/jrvh5h6NXhEauFFexRin69nA0uHJ5gwk4iDivp/DeoEua3uwCUto6PC86IpRITBOs4+6i2I56K5x5b6WYGXHA==
dependencies:
undici-types "~6.19.2"

"@types/node@^8.0.0":
version "8.10.66"
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.66.tgz#dd035d409df322acc83dff62a602f12a5783bbb3"
Expand Down Expand Up @@ -4591,7 +4606,7 @@ [email protected]:
utf8 "^3.0.0"
uuid "^3.3.2"

ethers@^5.0.1, ethers@^5.0.2, ethers@^5.5.2, ethers@^5.7.2:
ethers@^5.0.1, ethers@^5.0.2, ethers@^5.5.2, ethers@^5.5.3, ethers@^5.7.2:
version "5.7.2"
resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e"
integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==
Expand Down Expand Up @@ -6830,6 +6845,13 @@ math-intrinsics@^1.0.0, math-intrinsics@^1.1.0:
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==

mcl-wasm@^1.0.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/mcl-wasm/-/mcl-wasm-1.8.0.tgz#f9292c465a50d86016aa0054cb025a538d3183dd"
integrity sha512-j6kekpd/i6XLHKgUPLPOqts3EUIw+lOFPdyQ4cqepONZ2R/dtfc3+DnYMJXKXw4JF8c6hfcBZ04gbYWOXurv+Q==
dependencies:
"@types/node" "^20.2.5"

md5.js@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
Expand Down

0 comments on commit d8a5940

Please sign in to comment.