-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
325 additions
and
4 deletions.
There are no files selected for viewing
This file contains 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,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)) | ||
) | ||
))); | ||
} | ||
} |
This file contains 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
This file contains 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
This file contains 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,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) | ||
}) | ||
}) | ||
}) |
This file contains 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 |
---|---|---|
|
@@ -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" | ||
|
@@ -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" | ||
|
@@ -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== | ||
|
@@ -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" | ||
|