import './aa.init' ; import { describe } from 'mocha' ; import { Wallet } from "ethers" ; import { expect } from "chai" ; import { SimpleWallet , SimpleWallet__factory , EntryPoint , TestCounter , TestCounter__factory , TestUtil , TestUtil__factory , } from "../typechain" ; import { AddressZero , createWalletOwner , fund , checkForGeth , rethrow , WalletConstructor , tonumber , deployEntryPoint , callDataCost } from "./testutils" ; import { fillAndSign } from "./UserOp" ; import { UserOperation } from "./UserOperation" ; import { PopulatedTransaction } from "ethers/lib/ethers" ; import { ethers } from 'hardhat' ; import { toBuffer } from "ethereumjs-util" ; import { defaultAbiCoder } from "ethers/lib/utils" ; describe ( "Batch gas testing" , function ( ) { return ; let once = true ; let ethersSigner = ethers . provider . getSigner ( ) ; let entryPoint ; let entryPointView ; let testUtil ; let walletOwner ; let wallet ; let results = [ ] ; before ( async function ( ) { this . skip ( ) ; return ; await checkForGeth ( ) ; testUtil = await new TestUtil__factory ( ethersSigner ) . deploy ( ) ; entryPoint = await deployEntryPoint ( 0 , 0 ) ; entryPointView = entryPoint . connect ( ethers . provider . getSigner ( AddressZero ) ) ; walletOwner = createWalletOwner ( ) ; wallet = await new SimpleWallet__factory ( ethersSigner ) . deploy ( entryPoint . address , await walletOwner . getAddress ( ) ) ; await fund ( wallet ) ; } ) ; after ( async ( ) => { if ( results . length == 0 ) { return ; } console . log ( '== Summary' ) ; console . log ( 'note: negative "overpaid" means the client should compensate the relayer with higher priority fee' ) ; for ( let result of results ) { await result ( ) ; } } ) ; [ 1 , 10 ] . forEach ( maxCount => { describe ( 'test batches maxCount=' + maxCount , ( ) => { /**
             * attempt big batch.
             */ let counter ; let walletExecCounterFromEntryPoint ; let execCounterCount ; const beneficiaryAddress = Wallet . createRandom ( ) . address ; before ( async ( ) => { counter = await new TestCounter__factory ( ethersSigner ) . deploy ( ) ; const count = await counter . populateTransaction . count ( ) ; execCounterCount = await wallet . populateTransaction . exec ( counter . address , 0 , count . data ! ) ; walletExecCounterFromEntryPoint = await wallet . populateTransaction . execFromEntryPoint ( counter . address , 0 , count . data ! ) ; } ) ; let wallets = [ ] ; it ( 'batch of create' , async ( ) => { let ops = [ ] ; let count = 0 ; const maxTxGas = 12e6 ; let opsGasCollected = 0 ; while ( ++ count ) { const walletOwner1 = createWalletOwner ( ) ; const wallet1 = await entryPoint . getSenderAddress ( WalletConstructor ( entryPoint . address , walletOwner1 . address ) , 0 ) ; await fund ( wallet1 , '0.5' ) ; const op1 = await fillAndSign ( { initCode : WalletConstructor ( entryPoint . address , walletOwner1 . address ) , nonce : 0 , maxPriorityFeePerGas : 1e9 , } , walletOwner1 , entryPoint ) ; const { preOpGas } = await entryPointView . callStatic . simulateValidation ( op1 , { gasPrice : 1e9 } ) ; const txgas = preOpGas . add ( op1 . callGas ) . toNumber ( ) ; if ( opsGasCollected + txgas > maxTxGas ) { break ; } opsGasCollected += txgas ; ops . push ( op1 ) ; wallets . push ( { owner : walletOwner1 , w : wallet1 } ) ; if ( wallets . length >= maxCount ) break ; } await call_handleOps_and_stats ( 'Create' , ops , count ) ; } ) ; it ( 'batch of tx' , async function ( ) { this . timeout ( 30000 ) ; if ( ! wallets . length ) { this . skip ( ) ; } let ops = [ ] ; for ( let { w , owner } of wallets ) { const op1 = await fillAndSign ( { sender : w , callData : walletExecCounterFromEntryPoint . data , maxPriorityFeePerGas : 1e9 , verificationGas : 1.3e6 } , owner , entryPoint ) ; ops . push ( op1 ) ; if ( once ) { once = false ; console . log ( 'direct call:' , await counter . estimateGas . count ( ) ) ; console . log ( 'through wallet:' , await ethers . provider . estimateGas ( { from : walletOwner . address , to : wallet . address , data : execCounterCount . data ! } ) , 'datacost=' , callDataCost ( execCounterCount . data ! ) ) ; console . log ( 'through handleOps:' , await entryPoint . estimateGas . handleOps ( [ op1 ] , beneficiaryAddress ) ) ; console . log ( 'through single handleOp:' , await entryPoint . estimateGas . handleOp ( op1 , beneficiaryAddress ) ) ; } } await call_handleOps_and_stats ( "Simple Ops" , ops , ops . length ) ; } ) ; it ( 'batch of expensive ops' , async function ( ) { this . timeout ( 30000 ) ; if ( ! wallets . length ) { this . skip ( ) ; } let walletExecFromEntryPoint_waster ; const waster = await counter . populateTransaction . gasWaster ( 40 , "" ) ; walletExecFromEntryPoint_waster = await wallet . populateTransaction . execFromEntryPoint ( counter . address , 0 , waster . data ! ) ; let ops = [ ] ; for ( let { w , owner } of wallets ) { const op1 = await fillAndSign ( { sender : w , callData : walletExecFromEntryPoint_waster . data , maxPriorityFeePerGas : 1e9 , verificationGas : 1.3e6 } , owner , entryPoint ) ; ops . push ( op1 ) ; } await call_handleOps_and_stats ( "Expensive Ops" , ops , ops . length ) ; } ) ; it ( 'batch of large ops' , async function ( ) { this . timeout ( 30000 ) ; if ( ! wallets . length ) { this . skip ( ) ; } let walletExecFromEntryPoint_waster ; const waster = await counter . populateTransaction . gasWaster ( 0 , '1' . repeat ( 16384 ) ) ; walletExecFromEntryPoint_waster = await wallet . populateTransaction . execFromEntryPoint ( counter . address , 0 , waster . data ! ) ; let ops = [ ] ; for ( let { w , owner } of wallets ) { const op1 = await fillAndSign ( { sender : w , callData : walletExecFromEntryPoint_waster . data , maxPriorityFeePerGas : 1e9 , verificationGas : 1.3e6 } , owner , entryPoint ) ; ops . push ( op1 ) ; } await call_handleOps_and_stats ( 'Large (16k) Ops' , ops , ops . length ) ; } ) ; } ) ; } ) ; async function call_handleOps_and_stats ( title , ops , count ) { const beneficiaryAddress = createWalletOwner ( ) . address ; const sender = ethersSigner ; const senderPrebalance = await ethers . provider . getBalance ( await sender . getAddress ( ) ) ; const entireTxEncoded = toBuffer ( await entryPoint . populateTransaction . handleOps ( ops , beneficiaryAddress ) . then ( tx => tx . data ) ) ; function callDataCost ( data ) { if ( typeof data == 'string' ) { data = toBuffer ( data ) ; } return data . map ( b => b == 0 ? 4 : 16 ) . reduce ( ( sum , b ) => sum + b ) ; } const entireTxDataCost = callDataCost ( entireTxEncoded ) ; const handleOpFunc = Object . values ( entryPoint . interface . functions ) . find ( func => func . name == 'handleOp' ) ! ; const opEncoding = handleOpFunc . inputs [ 0 ] ; const opEncoded = defaultAbiCoder . encode ( [ opEncoding ] , [ ops [ 0 ] ] ) ; const opDataCost = callDataCost ( opEncoded ) ; console . log ( '== calldataoverhead=' , entireTxDataCost , 'len=' , entireTxEncoded . length / 2 , 'opcost=' , opDataCost , opEncoded . length / 2 ) ; console . log ( '== per-op overhead:' , entireTxDataCost - ( opDataCost * count ) , 'count=' , count ) ; const { maxPriorityFeePerGas } = ops [ 0 ] ; const ret = await entryPoint . connect ( sender ) . handleOps ( ops , beneficiaryAddress , { gasLimit : 13e6 , maxPriorityFeePerGas } ) . catch ( ( rethrow ( ) ) ) . then ( r => r ! . wait ( ) ) ; const events1 = ret . events ! . filter ( e => e . event == 'UserOperationEvent' ) ! ; if ( events1 . length != ret . events ! . length ) { console . log ( '== reverted: ' , ret . events ! . length - events1 . length ) ; } const { actualGasPrice } = events1 [ 0 ] ! . args ! ; const totalEventsGasCost = parseInt ( events1 . map ( x => x . args ! . actualGasCost ) . reduce ( ( sum , x ) => sum . add ( x ) ) . toString ( ) ) ; const senderPaid = parseInt ( senderPrebalance . sub ( await ethers . provider . getBalance ( await sender . getAddress ( ) ) ) . toString ( ) ) ; let senderRedeemed = await ethers . provider . getBalance ( beneficiaryAddress ) . then ( tonumber ) ; expect ( senderRedeemed ) . to . equal ( totalEventsGasCost ) ; senderRedeemed += entireTxDataCost * actualGasPrice ; console . log ( 'provider gasprice:' , await ethers . provider . getGasPrice ( ) ) ; console . log ( 'userop   gasPrice:' , actualGasPrice ) ; const opGasUsed = Math . floor ( senderPaid / actualGasPrice / count ) ; const opGasPaid = Math . floor ( senderRedeemed / actualGasPrice / count ) ; console . log ( 'senderPaid= ' , senderPaid , '(wei)\t' , ( senderPaid / actualGasPrice ) . toFixed ( 0 ) , '(gas)' , opGasUsed , '(gas/op)' , count ) ; console . log ( 'redeemed=   ' , senderRedeemed , '(wei)\t' , ( senderRedeemed / actualGasPrice ) . toFixed ( 0 ) , '(gas)' , opGasPaid , '(gas/op)' ) ; const dumpResult = async ( ) => { console . log ( '==>' , `${ title } (count=${ count }) : ` . padEnd ( 30 ) , 'per-op gas overpaid:' , opGasPaid - opGasUsed ) ; } ; await dumpResult ( ) ; results . push ( dumpResult ) ; } } ) ;