import { CeloTxReceipt , TransactionResult } from '@celo/connect' ; import { CeloContract , ContractKit , newKitFromWeb3 } from '@celo/contractkit' ; import { GoldTokenWrapper } from '@celo/contractkit/lib/wrappers/GoldTokenWrapper' ; import { StableTokenWrapper } from '@celo/contractkit/lib/wrappers/StableTokenWrapper' ; import { waitForPortOpen } from '@celo/dev-utils/lib/network' ; import BigNumber from 'bignumber.js' ; import { spawn } from 'child_process' ; import { randomBytes } from 'crypto' ; import fs from 'fs' ; import { merge , range } from 'lodash' ; import fetch from 'node-fetch' ; import path from 'path' ; import sleep from 'sleep-promise' ; import Web3 from 'web3' ; import { Admin } from 'web3-eth-admin' ; import { spawnCmd , spawnCmdWithExitOnFailure } from './cmd-utils' ; import { convertToContractDecimals } from './contract-utils' ; import { envVar , fetchEnv , isVmBased } from './env-utils' ; import { AccountType , generateGenesis , generateGenesisWithMigrations , generatePrivateKey , privateKeyToPublicKey , Validator , } from './generate_utils' ; import { retrieveClusterIPAddress , retrieveIPAddress } from './helm_deploy' ; import { GethInstanceConfig } from './interfaces/geth-instance-config' ; import { GethRunConfig } from './interfaces/geth-run-config' ; import { ensure0x } from './utils' ; import { getTestnetOutputs } from './vm-testnet-utils' ; export async function unlockAccount ( web3 , duration , password , accountAddress = null ) { if ( accountAddress === null ) { const accounts = await web3 . eth . getAccounts ( ) ; accountAddress = accounts [ 0 ] ; } await web3 . eth . personal . unlockAccount ( accountAddress ! , password , duration ) ; return accountAddress ! ; } type HandleErrorCallback = ( isError : boolean , data : { location : string ; error : string ; } ) => void ; const DEFAULT_TRANSFER_AMOUNT = new BigNumber ( '0.00000000000001' ) ; const LOAD_TEST_TRANSFER_WEI = new BigNumber ( 10000 ) ; const GETH_IPC = 'geth.ipc' ; const DISCOVERY_PORT = 30303 ; const BOOTNODE_DISCOVERY_PORT = 30301 ; const BLOCKSCOUT_TIMEOUT = 12000 ; export const LOG_TAG_BLOCKSCOUT_TIMEOUT = 'blockscout_timeout' ; export const LOG_TAG_BLOCKSCOUT_TIME_MEASUREMENT = 'blockscout_time_measurement' ; export const LOG_TAG_BLOCKSCOUT_VALIDATION_ERROR = 'validate_blockscout_error' ; export const LOG_TAG_CONTRACT_ADDRESS_ERROR = 'contract_address_error' ; export const LOG_TAG_GETH_RPC_ERROR = 'geth_rpc_error' ; export const LOG_TAG_TRANSACTION_ERROR = 'transaction_error' ; export const LOG_TAG_TRANSACTION_HASH_RECEIVED = 'tx_hash_received' ; export const LOG_TAG_TRANSACTION_VALIDATION_ERROR = 'validate_transaction_error' ; export const LOG_TAG_TX_TIME_MEASUREMENT = 'tx_time_measurement' ; export const getEnodeAddress = ( nodeId , ipAddress , port ) => { return `enode://${ nodeId }@${ ipAddress }:${ port }` ; } ; export const getBootnodeEnode = async ( namespace ) => { const ip = await retrieveBootnodeIPAddress ( namespace ) ; const privateKey = generatePrivateKey ( fetchEnv ( envVar . MNEMONIC ) , AccountType . BOOTNODE , 0 ) ; const nodeId = privateKeyToPublicKey ( privateKey ) ; return [ getEnodeAddress ( nodeId , ip , BOOTNODE_DISCOVERY_PORT ) ] ; } ; export const retrieveBootnodeIPAddress = async ( namespace ) => { if ( isVmBased ( ) ) { const outputs = await getTestnetOutputs ( namespace ) ; return outputs . bootnode_ip_address . value ; } else { const resourceName = namespace === 'baklava' ? `${ namespace }-bootnode-address` : `${ namespace }-bootnode` ; if ( fetchEnv ( envVar . STATIC_IPS_FOR_GETH_NODES ) === 'true' ) { return retrieveIPAddress ( resourceName ) ; } else { return retrieveClusterIPAddress ( 'service' , resourceName , namespace ) ; } } } ; const retrieveTxNodeAddresses = async ( namespace , txNodesNum ) => { if ( isVmBased ( ) ) { const outputs = await getTestnetOutputs ( namespace ) ; return outputs . tx_node_ip_addresses . value ; } else { const txNodesRange = range ( 0 , txNodesNum ) ; return Promise . all ( txNodesRange . map ( ( i ) => retrieveIPAddress ( `${ namespace }-tx-nodes-${ i }` ) ) ) ; } } ; const getEnodesWithIpAddresses = async ( namespace , getExternalIP ) => { const txNodesNum = parseInt ( fetchEnv ( envVar . TX_NODES ) , 10 ) ; const txAddresses = await retrieveTxNodeAddresses ( namespace , txNodesNum ) ; const txNodesRange = range ( 0 , txNodesNum ) ; return Promise . all ( txNodesRange . map ( async ( index ) => { const privateKey = generatePrivateKey ( fetchEnv ( envVar . MNEMONIC ) , AccountType . TX_NODE , index ) ; const nodeId = privateKeyToPublicKey ( privateKey ) ; let address ; if ( getExternalIP ) { address = txAddresses [ index ] ; } else { address = await retrieveClusterIPAddress ( 'service' , `${ namespace }-service-${ index }` , namespace ) ; if ( address . length === 0 ) { console . error ( 'IP address is empty for transaction node' ) ; throw new Error ( 'IP address is empty for transaction node' ) ; } } return getEnodeAddress ( nodeId , address , DISCOVERY_PORT ) ; } ) ) ; } ; export const getEnodesAddresses = async ( namespace ) => { return getEnodesWithIpAddresses ( namespace , false ) ; } ; export const getEnodesWithExternalIPAddresses = async ( namespace ) => { return getEnodesWithIpAddresses ( namespace , true ) ; } ; export function getPrivateTxNodeClusterIP ( celoEnv ) { return retrieveClusterIPAddress ( 'service' , 'tx-nodes-private' , celoEnv ) ; } export const fetchPassword = ( passwordFile ) => { if ( ! fs . existsSync ( passwordFile ) ) { console . error ( `Password file at ${ passwordFile } does not exists!` ) ; process . exit ( 1 ) ; } return fs . readFileSync ( passwordFile ) . toString ( ) ; } ; export const writeStaticNodes = ( enodes , outputDirPath , outputFileName , spacing = 2 ) => { const encodedJSON = JSON . stringify ( enodes , null , spacing ) ; fs . writeFile ( path . join ( outputDirPath , outputFileName ) , encodedJSON , ( err ) => { if ( err ) { console . error ( err ) ; process . exit ( 1 ) ; } } ) ; } ; export const checkGethStarted = ( dataDir ) => { if ( ! fs . existsSync ( path . resolve ( dataDir , GETH_IPC ) ) ) { console . error ( `Looks like there are no local geth nodes running in ${ dataDir }` ) ; console . info ( `Please, make sure you specified correct data directory, you could also run the geth node by "celotooljs geth run"` ) ; process . exit ( 1 ) ; } } ; export const getWeb3AndTokensContracts = async ( ) => { const kit = newKitFromWeb3 ( new Web3 ( 'http://localhost:8545' ) ) ; const [ goldToken , stableToken ] = await Promise . all ( [ kit . contracts . getGoldToken ( ) , kit . contracts . getStableToken ( ) , ] ) ; return { kit , goldToken , stableToken , } ; } ; export const getRandomInt = ( from , to ) => { return Math . floor ( Math . random ( ) * ( to - from ) ) + from ; } ; const getRandomToken = ( goldToken , stableToken ) => { const tokenType = getRandomInt ( 0 , 2 ) ; if ( tokenType === 0 ) { return goldToken ; } else { return stableToken ; } } ; const validateGethRPC = async ( kit , txHash , from , handleError ) => { const transaction = await kit . connection . getTransaction ( txHash ) ; handleError ( ! transaction || ! transaction . from , { location : '[GethRPC]' , error : `Contractkit did not return a valid transaction` , } ) ; if ( transaction == null ) { return ; } const txFrom = transaction . from . toLowerCase ( ) ; const expectedFrom = from . toLowerCase ( ) ; handleError ( ! transaction . from || expectedFrom !== txFrom , { location : '[GethRPC]' , error : `Expected "from" to equal ${ expectedFrom }, but found ${ txFrom }` , } ) ; } ; const checkBlockscoutResponse = ( json , txHash , from , handleError ) => { const location = '[Blockscout]' ; handleError ( json . status !== '1' , { location , error : `Invalid status: expected '1', received` } ) ; handleError ( ! json . result , { location , error : `No result found: receive ${ json . status . result }` } ) ; const resultFrom = json . result . from . toLowerCase ( ) ; const expectedFrom = from . toLowerCase ( ) ; handleError ( resultFrom !== expectedFrom , { location , error : `Expected "from" to equal ${ expectedFrom }, but found ${ resultFrom }` , } ) ; handleError ( json . result . hash !== txHash , { location , error : `Expected "hash" to equal ${ txHash }, but found ${ json . result . hash }` , } ) ; } ; const fetchBlockscoutTxInfo = async ( url , txHash ) => { const response = await fetch ( `${ url }/api?module=transaction&action=gettxinfo&txhash=${ txHash }` ) ; return response . json ( ) ; } ; const validateBlockscout = async ( url , txHash , from , handleError ) => { const json = await fetchBlockscoutTxInfo ( url , txHash ) ; checkBlockscoutResponse ( json , txHash , from , handleError ) ; } ; const MAXIMAL_BLOCKSCOUT_TIMEOUT = 15000 ; const BLOCKSCOUT_FETCH_RETRY_TIME = 150 ; const getFirstValidBlockscoutResponse = async ( url , txHash ) => { const attempts = MAXIMAL_BLOCKSCOUT_TIMEOUT / BLOCKSCOUT_FETCH_RETRY_TIME ; for ( let attemptId = 0 ; attemptId < attempts ; attemptId ++ ) { const json = await fetchBlockscoutTxInfo ( url , txHash ) ; if ( json . status !== '1' ) { await sleep ( BLOCKSCOUT_FETCH_RETRY_TIME ) ; } else { return [ json , Date . now ( ) ] ; } } return [ null , null ] ; } ; const validateTransactionAndReceipt = ( from , txReceipt , handleError ) => { const location = '[TX & Receipt]' ; handleError ( ! txReceipt , { location , error : 'No transaction receipt received!' } ) ; handleError ( txReceipt . status !== true , { location , error : `Transaction receipt status (${ txReceipt . status }) is not true!` , } ) ; handleError ( txReceipt . from . toLowerCase ( ) !== from . toLowerCase ( ) , { location , error : `Transaction receipt from (${ txReceipt . from }) is not equal to sender address (${ from }).` , } ) ; } ; const tracerLog = ( logMessage ) => { console . log ( JSON . stringify ( logMessage ) ) ; } ; const exitTracerTool = ( logMessage ) => { tracerLog ( logMessage ) ; process . exit ( 1 ) ; } ; const transferAndTrace = async ( kit , goldToken , stableToken , from , to , password , blockscoutUrl ) => { console . info ( 'Transfer' ) ; const token = getRandomToken ( goldToken , stableToken ) ; const feeCurrencyToken = getRandomToken ( goldToken , stableToken ) ; const [ tokenName , feeCurrencySymbol ] = await Promise . all ( [ token . symbol ( ) , feeCurrencyToken . symbol ( ) , ] ) ; const logMessage = { severity : 'CRITICAL' , senderAddress : from , receiverAddress : to , blockscout : blockscoutUrl , token : tokenName , error : '' , location : '' , txHash : '' , } ; const txParams = { } ; if ( getRandomInt ( 0 , 2 ) === 3 ) { txParams . feeCurrency = feeCurrencyToken . address ; logMessage . feeCurrency = feeCurrencySymbol ; } const transferToken = new Promise ( async ( resolve ) => { await transferERC20Token ( kit , token , from , to , DEFAULT_TRANSFER_AMOUNT , password , txParams , undefined , ( receipt ) => { resolve ( receipt ) ; } , ( error ) => { logMessage . error = error ; exitTracerTool ( logMessage ) ; } ) ; } ) ; const txReceipt = await transferToken ; const txHash = txReceipt ? txReceipt . transactionHash : '' ; await sleep ( BLOCKSCOUT_TIMEOUT ) ; logMessage . txHash = txHash ; const handleError = ( isError , data ) => { if ( isError ) { exitTracerTool ( { ... logMessage , ... data } ) ; } } ; validateTransactionAndReceipt ( from , txReceipt ! , handleError ) ; await validateBlockscout ( blockscoutUrl , txHash , from , handleError ) ; await validateGethRPC ( kit , txHash , from , handleError ) ; } ; export const traceTransactions = async ( kit , goldToken , stableToken , addresses , blockscoutUrl ) => { console . info ( 'Starting simulation' ) ; await transferAndTrace ( kit , goldToken , stableToken , addresses [ 0 ] , addresses [ 1 ] , '' , blockscoutUrl ) ; await transferAndTrace ( kit , goldToken , stableToken , addresses [ 1 ] , addresses [ 0 ] , '' , blockscoutUrl ) ; console . info ( 'Simulation finished successully!' ) ; } ; const measureBlockscout = async ( blockscoutUrl , txHash , from , obtainReceiptTime , baseLogMessage ) => { const [ json , receivedTime ] = await getFirstValidBlockscoutResponse ( blockscoutUrl , txHash ) ; if ( receivedTime === null ) { tracerLog ( { tag : LOG_TAG_BLOCKSCOUT_TIMEOUT , ... baseLogMessage , } ) ; } else { tracerLog ( { tag : LOG_TAG_BLOCKSCOUT_TIME_MEASUREMENT , p_time : receivedTime - obtainReceiptTime , ... baseLogMessage , } ) ; checkBlockscoutResponse ( json , txHash , from , ( isError , data ) => { if ( isError ) { tracerLog ( { tag : LOG_TAG_BLOCKSCOUT_VALIDATION_ERROR , ... data , ... baseLogMessage , } ) ; } } ) ; } } ; export const transferCeloGold = async ( kit , fromAddress , toAddress , amount , txOptions = { } ) => { const kitGoldToken = await kit . contracts . getGoldToken ( ) ; return kitGoldToken . transfer ( toAddress , amount . toString ( ) ) . send ( { from : fromAddress , gas : txOptions . gas , gasPrice : txOptions . gasPrice , feeCurrency : txOptions . feeCurrency || undefined , gatewayFeeRecipient : txOptions . gatewayFeeRecipient , gatewayFee : txOptions . gatewayFee , nonce : txOptions . nonce , } ) ; } ; export const transferCeloDollars = async ( kit , fromAddress , toAddress , amount , txOptions = { } ) => { const kitStableToken = await kit . contracts . getStableToken ( ) ; return kitStableToken . transfer ( toAddress , amount . toString ( ) ) . send ( { from : fromAddress , gas : txOptions . gas , gasPrice : txOptions . gasPrice , feeCurrency : txOptions . feeCurrency || undefined , gatewayFeeRecipient : txOptions . gatewayFeeRecipient , gatewayFee : txOptions . gatewayFee , nonce : txOptions . nonce , } ) ; } ; export const unlock = async ( kit , address , password , unlockPeriod ) => { try { await kit . web3 . eth . personal . unlockAccount ( address , password , unlockPeriod ) ; } catch ( error ) { console . error ( `Unlock account ${ address } failed:` , error ) ; } } ; export const simulateClient = async ( senderAddress , recipientAddress , txPeriodMs , blockscoutUrl , blockscoutMeasurePercent , index , thread , web3Provider = 'http://localhost:8545' ) => { const kit = newKitFromWeb3 ( new Web3 ( web3Provider ) ) ; const password = fetchEnv ( 'PASSWORD' ) ; let lastNonce = - 1 ; let lastTx = '' ; let lastGasPriceMinimum = new BigNumber ( 0 ) ; let nonce = 0 ; let unlockNeeded = true ; let recipientAddressFinal = recipientAddress ; const useRandomRecipient = fetchEnv ( envVar . LOAD_TEST_USE_RANDOM_RECIPIENT ) ; const sleepTime = 5000 ; while ( await kit . connection . isSyncing ( ) ) { console . info ( `LoadTestId ${ index } waiting for web3Provider to be synced. Sleeping ${ sleepTime }ms` ) ; await sleep ( sleepTime ) ; } kit . defaultAccount = senderAddress ; const randomSleep = Math . random ( ) * txPeriodMs ; console . info ( `Sleeping for ${ randomSleep } ms` ) ; await sleep ( randomSleep ) ; const baseLogMessage = { loadTestID : index , threadID : thread , sender : senderAddress , recipient : recipientAddressFinal , feeCurrency : '' , txHash : '' , } ; while ( true ) { const sendTransactionTime = Date . now ( ) ; if ( unlockNeeded ) { await unlock ( kit , kit . defaultAccount , password , 9223372036 ) ; unlockNeeded = false ; } const transferGold = Boolean ( Math . round ( Math . random ( ) ) ) ; const transferFn = transferGold ? transferCeloGold : transferCeloDollars ; baseLogMessage . tokenName = transferGold ? 'cGLD' : 'cUSD' ; const feeCurrencyGold = Boolean ( Math . round ( Math . random ( ) ) ) ; if ( useRandomRecipient === 'true' ) { recipientAddressFinal = `0x${ randomBytes ( 20 ) . toString ( 'hex' ) }` ; baseLogMessage . recipient = recipientAddressFinal ; } let feeCurrency , txOptions ; try { feeCurrency = feeCurrencyGold ? '' : await kit . registry . addressFor ( CeloContract . StableToken ) ; } catch ( error ) { tracerLog ( { tag : LOG_TAG_CONTRACT_ADDRESS_ERROR , error : error . toString ( ) , ... baseLogMessage , } ) ; } try { baseLogMessage . feeCurrency = feeCurrency ; const gasPriceMinimum = await kit . contracts . getGasPriceMinimum ( ) ; const gasPriceBase = feeCurrency ? await gasPriceMinimum . getGasPriceMinimum ( feeCurrency ) : await gasPriceMinimum . gasPriceMinimum ( ) ; let gasPrice = new BigNumber ( gasPriceBase ) . times ( 2 ) . dp ( 0 ) ; if ( lastTx === '' || lastNonce === - 1 ) { nonce = await kit . web3 . eth . getTransactionCount ( kit . defaultAccount , 'latest' ) ; } else if ( ( await kit . connection . getTransactionReceipt ( lastTx ) ) ?. blockNumber ) { nonce = await kit . web3 . eth . getTransactionCount ( kit . defaultAccount , 'latest' ) ; } else { nonce = ( await kit . web3 . eth . getTransactionCount ( kit . defaultAccount , 'latest' ) ) - 1 ; gasPrice = BigNumber . max ( gasPrice . toNumber ( ) , lastGasPriceMinimum . times ( 1.15 ) ) . dp ( 0 ) ; console . warn ( `TX ${ lastTx } was not mined. Replacing tx reusing nonce ${ nonce } and gasPrice ${ gasPrice }` ) ; } lastGasPriceMinimum = gasPrice ; txOptions = { gasPrice : gasPrice . toString ( ) , feeCurrency , nonce , } ; } catch ( error ) { tracerLog ( { tag : LOG_TAG_CONTRACT_ADDRESS_ERROR , error : error . toString ( ) , ... baseLogMessage , } ) ; } await transferFn ( kit , senderAddress , recipientAddressFinal , LOAD_TEST_TRANSFER_WEI , txOptions ) . then ( async ( txResult ) => { lastTx = await txResult . getHash ( ) ; lastNonce = ( await kit . web3 . eth . getTransaction ( lastTx ) ) . nonce ; await onLoadTestTxResult ( kit , senderAddress , txResult , sendTransactionTime , baseLogMessage , blockscoutUrl , blockscoutMeasurePercent ) ; } ) . catch ( ( error ) => { if ( typeof error === 'string' && error . includes ( 'Error: authentication needed: password or unlock' ) ) { console . warn ( 'Load test transaction failed with locked account:' , error ) ; unlockNeeded = true ; } else { console . error ( 'Load test transaction failed with error:' , error ) ; tracerLog ( { tag : LOG_TAG_TRANSACTION_ERROR , error : error . toString ( ) , ... baseLogMessage , } ) ; } } ) ; if ( sendTransactionTime + txPeriodMs > Date . now ( ) ) { await sleep ( sendTransactionTime + txPeriodMs - Date . now ( ) ) ; } } } ; export const onLoadTestTxResult = async ( kit , senderAddress , txResult , sendTransactionTime , baseLogMessage , blockscoutUrl , blockscoutMeasurePercent ) => { const txReceipt = await txResult . waitReceipt ( ) ; const txHash = txReceipt . transactionHash ; baseLogMessage . txHash = txHash ; const receiptTime = Date . now ( ) ; tracerLog ( { tag : LOG_TAG_TX_TIME_MEASUREMENT , p_time : receiptTime - sendTransactionTime , ... baseLogMessage , } ) ; validateTransactionAndReceipt ( senderAddress , txReceipt , ( isError , data ) => { if ( isError ) { tracerLog ( { tag : LOG_TAG_TRANSACTION_VALIDATION_ERROR , ... baseLogMessage , ... data , } ) ; } } ) ; if ( Math . random ( ) * 100 < blockscoutMeasurePercent ) { await measureBlockscout ( blockscoutUrl , txReceipt . transactionHash , senderAddress , receiptTime , baseLogMessage ) ; } await validateGethRPC ( kit , txHash , senderAddress , ( isError , data ) => { if ( isError ) { tracerLog ( { tag : LOG_TAG_GETH_RPC_ERROR , ... data , ... baseLogMessage , } ) ; } } ) ; } ; param pod param thread export function getIndexForLoadTestThread ( pod , thread ) { const base = 10000 ; return pod * base + thread ; } param kit param token param from param to param amount param password param txParams param onTransactionHash param onReceipt param onError export const transferERC20Token = async ( kit , token , from , to , amount , password , txParams = { } , onTransactionHash ? , onReceipt ? , onError ? ) => { txParams . from = from ; await unlockAccount ( kit . connection . web3 , 0 , password , from ) ; const convertedAmount = await convertToContractDecimals ( amount , token ) ; try { const result = await token . transfer ( to , convertedAmount . toString ( ) ) . send ( ) ; if ( onTransactionHash ) { onTransactionHash ( await result . getHash ( ) ) ; } if ( onReceipt ) { const receipt = await result . waitReceipt ( ) ; onReceipt ( receipt ) ; } } catch ( error ) { if ( onError ) { onError ( error ) ; } } } ; export const runGethNodes = async ( { gethConfig , validators , verbose , } ) => { const gethBinaryPath = path . join ( ( gethConfig . repository && gethConfig . repository . path ) || '' , 'build/bin/geth' ) ; if ( ! fs . existsSync ( gethBinaryPath ) ) { console . error ( `Geth binary at ${ gethBinaryPath } not found!` ) ; return ; } if ( ! gethConfig . keepData && fs . existsSync ( gethConfig . runPath ) ) { await resetDataDir ( gethConfig . runPath , verbose ) ; } if ( ! fs . existsSync ( gethConfig . runPath ) ) { fs . mkdirSync ( gethConfig . runPath , { recursive : true } ) ; } await writeGenesis ( gethConfig , validators , verbose ) ; if ( verbose ) { const validatorAddresses = validators . map ( ( validator ) => validator . address ) ; console . log ( 'Validators' , JSON . stringify ( validatorAddresses , null , 2 ) ) ; } for ( const instance of gethConfig . instances ) { await initAndStartGeth ( gethConfig , gethBinaryPath , instance , verbose ) ; } await connectValidatorPeers ( gethConfig . instances ) ; } ; function getInstanceDir ( runPath , instance ) { return path . join ( runPath , instance . name ) ; } function getSnapshotdir ( runPath , instance ) { return path . join ( getInstanceDir ( runPath , instance ) , 'snapshot' ) ; } export function importGenesis ( genesisPath ) { return JSON . parse ( fs . readFileSync ( genesisPath ) . toString ( ) ) ; } export function getLogFilename ( runPath , instance ) { return path . join ( getDatadir ( runPath , instance ) , 'logs.txt' ) ; } function getDatadir ( runPath , instance ) { const dir = path . join ( getInstanceDir ( runPath , instance ) , 'datadir' ) ; fs . mkdirSync ( dir , { recursive : true } ) ; return dir ; } returns export async function initAndStartGeth ( gethConfig , gethBinaryPath , instance , verbose ) { await initGeth ( gethConfig , gethBinaryPath , instance , verbose ) ; return startGeth ( gethConfig , gethBinaryPath , instance , verbose ) ; } export async function initGeth ( gethConfig , gethBinaryPath , instance , verbose ) { const datadir = getDatadir ( gethConfig . runPath , instance ) ; const genesisPath = path . join ( gethConfig . runPath , 'genesis.json' ) ; if ( verbose ) { console . info ( `geth:${ instance . name }: init datadir ${ datadir }` ) ; console . log ( `init geth with genesis at ${ genesisPath }` ) ; } await spawnCmdWithExitOnFailure ( 'rm' , [ '-rf' , datadir ] , { silent : ! verbose } ) ; await spawnCmdWithExitOnFailure ( gethBinaryPath , [ '--datadir' , datadir , 'init' , genesisPath ] , { silent : ! verbose , } ) ; if ( instance . privateKey ) { await importPrivateKey ( gethConfig , gethBinaryPath , instance , verbose ) ; } } export async function importPrivateKey ( getConfig , gethBinaryPath , instance , verbose ) { const keyFile = path . join ( getDatadir ( getConfig . runPath , instance ) , 'key.txt' ) ; if ( ! instance . privateKey ) { throw new Error ( 'Unexpected empty private key' ) ; } fs . writeFileSync ( keyFile , instance . privateKey , { flag : 'a' } ) ; if ( verbose ) { console . info ( `geth:${ instance . name }: import account` ) ; } const args = [ 'account' , 'import' , '--datadir' , getDatadir ( getConfig . runPath , instance ) , '--password' , '/dev/null' , keyFile , ] ; if ( verbose ) { console . log ( gethBinaryPath , ... args ) ; } await spawnCmdWithExitOnFailure ( gethBinaryPath , args , { silent : true } ) ; } export async function getEnode ( peer , ws = false ) { if ( peer . toLowerCase ( ) . startsWith ( 'enode' ) ) { return peer ; } const p = ws ? 'ws' : 'http' ; const enodeRpcUrl = `${ p }://localhost:${ peer }` ; const admin = new Admin ( enodeRpcUrl ) ; let nodeInfo = { enode : null , } ; try { nodeInfo = await admin . getNodeInfo ( ) ; } catch { console . error ( `Unable to get node info from ${ enodeRpcUrl }` ) ; } return nodeInfo . enode ; } export async function addStaticPeers ( datadir , peers , verbose ) { const staticPeersPath = path . join ( datadir , 'static-nodes.json' ) ; if ( verbose ) { console . log ( `Writing static peers to ${ staticPeersPath }` ) ; } const enodes = await Promise . all ( peers . map ( ( peer ) => getEnode ( peer ) ) ) ; const enodesString = JSON . stringify ( enodes , null , 2 ) ; if ( verbose ) { console . log ( 'eNodes' , enodesString ) ; } fs . writeFileSync ( staticPeersPath , enodesString ) ; } export async function addProxyPeer ( runPath , gethBinaryPath , instance ) { if ( instance . proxies ) { await spawnCmdWithExitOnFailure ( gethBinaryPath , [ '--datadir' , getDatadir ( runPath , instance ) , 'attach' , '--exec' , `istanbul.addProxy('${ instance . proxies [ 0 ] ! }', '${ instance . proxies [ 1 ] ! }')` , ] ) ; } } export async function startGeth ( gethConfig , gethBinaryPath , instance , verbose ) { if ( verbose ) { console . log ( 'starting geth with config' , JSON . stringify ( instance , null , 2 ) ) ; } else { console . log ( `${ instance . name }: starting.` ) ; } const datadir = getDatadir ( gethConfig . runPath , instance ) ; const { syncmode , port , rpcport , wsport , validating , replica , validatingGasPrice , bootnodeEnode , isProxy , proxyAllowPrivateIp , isProxied , proxyport , ethstats , gatewayFee , } = instance ; const privateKey = instance . privateKey || '' ; const lightserv = instance . lightserv || false ; const minerValidator = instance . minerValidator ; if ( instance . validating && ! minerValidator ) { throw new Error ( 'miner.validator address from the instance is required' ) ; } const verbosity = gethConfig . verbosity ? gethConfig . verbosity : '3' ; instance . args = [ '--datadir' , datadir , '--syncmode' , syncmode , '--debug' , '--metrics' , '--port' , port . toString ( ) , '--networkid' , gethConfig . networkId . toString ( ) , `--verbosity=${ verbosity }` , '--consoleoutput=stdout' , '--consoleformat=term' , '--nat' , 'extip:127.0.0.1' , '--allow-insecure-unlock' , '--gcmode=archive' , ] ; if ( minerValidator ) { const txFeeRecipient = instance . txFeeRecipient || minerValidator ; instance . args . push ( '--miner.validator' , minerValidator , '--tx-fee-recipient' , txFeeRecipient ) ; } if ( rpcport ) { instance . args . push ( '--http' , '--http.port' , rpcport . toString ( ) , '--http.corsdomain=*' , '--http.vhosts=*' , '--http.api=eth,net,web3,debug,admin,personal,txpool,istanbul' ) ; } if ( wsport ) { instance . args . push ( '--ws' , '--ws.origins=*' , '--ws.port' , wsport . toString ( ) , '--ws.api=eth,net,web3,debug,admin,personal,txpool,istanbul' ) ; } if ( lightserv ) { instance . args . push ( '--light.serve=90' ) ; instance . args . push ( '--light.maxpeers=10' ) ; } else if ( syncmode === 'full' || syncmode === 'fast' ) { instance . args . push ( '--light.serve=0' ) ; } if ( instance . nodekey ) { instance . args . push ( `--nodekeyhex=${ instance . nodekey }` ) ; } else if ( ! validating || ! replica ) { instance . args . push ( `--nodekeyhex=${ privateKey }` ) ; } if ( gatewayFee ) { instance . args . push ( `--light.gatewayfee=${ gatewayFee . toString ( ) }` ) ; } if ( validating ) { instance . args . push ( '--mine' ) ; if ( validatingGasPrice ) { instance . args . push ( `--miner.gasprice=${ validatingGasPrice }` ) ; } if ( isProxied ) { instance . args . push ( '--proxy.proxied' ) ; } if ( replica ) { instance . args . push ( '--istanbul.replica' ) ; } } else if ( isProxy ) { instance . args . push ( '--proxy.proxy' ) ; if ( proxyport ) { instance . args . push ( `--proxy.internalendpoint=:${ proxyport . toString ( ) }` ) ; } instance . args . push ( `--proxy.proxiedvalidatoraddress=${ instance . proxiedValidatorAddress }` ) ; } if ( bootnodeEnode ) { instance . args . push ( `--bootnodes=${ bootnodeEnode }` ) ; } else { instance . args . push ( '--nodiscover' ) ; } if ( isProxied && instance . proxies ) { if ( proxyAllowPrivateIp ) { instance . args . push ( '--proxy.allowprivateip=true' ) ; } instance . args . push ( `--proxy.proxyenodeurlpairs=${ instance . proxies [ 0 ] ! };${ instance . proxies [ 1 ] ! }` ) ; } if ( privateKey || ethstats ) { instance . args . push ( '--password=/dev/null' , `--unlock=0` ) ; } if ( ethstats ) { instance . args . push ( `--ethstats=${ instance . name }@${ ethstats }` , '--etherbase=0' ) ; } const gethProcess = spawnWithLog ( gethBinaryPath , instance . args , `${ datadir }/logs.txt` , verbose ) ; instance . pid = gethProcess . pid ; gethProcess . on ( 'error' , ( err ) => { throw new Error ( `geth:${ instance . name } failed to start! ${ err }` ) ; } ) ; gethProcess . on ( 'exit' , ( code ) => { if ( code === 0 ) { console . info ( `geth:${ instance . name } exited` ) ; } else { console . error ( `geth:${ instance . name } exited with code ${ code }` ) ; } instance . pid = undefined ; } ) ; const secondsToWait = 30 ; if ( rpcport ) { const isOpen = await waitForPortOpen ( 'localhost' , rpcport , secondsToWait ) ; if ( ! isOpen ) { console . error ( `geth:${ instance . name }: jsonRPC port ${ rpcport } didn't open after ${ secondsToWait } seconds` ) ; process . exit ( 1 ) ; } else if ( verbose ) { console . info ( `geth:${ instance . name }: jsonRPC port open ${ rpcport }` ) ; } } if ( wsport ) { const isOpen = await waitForPortOpen ( 'localhost' , wsport , secondsToWait ) ; if ( ! isOpen ) { console . error ( `geth:${ instance . name }: ws port ${ wsport } didn't open after ${ secondsToWait } seconds` ) ; process . exit ( 1 ) ; } else if ( verbose ) { console . info ( `geth:${ instance . name }: ws port open ${ wsport }` ) ; } } await sleep ( 1000 ) ; console . log ( `${ instance . name }: running.` , rpcport ? `RPC: ${ rpcport }` : '' , wsport ? `WS: ${ wsport }` : '' , proxyport ? `PROXY: ${ proxyport }` : '' ) ; return instance ; } export function writeGenesis ( gethConfig , validators , verbose ) { const genesis = generateGenesis ( { validators , blockTime : 1 , epoch : 10 , lookbackwindow : 3 , requestTimeout : 3000 , chainId : gethConfig . networkId , ... gethConfig . genesisConfig , } ) ; const genesisPath = path . join ( gethConfig . runPath , 'genesis.json' ) ; if ( verbose ) { console . log ( 'writing genesis' ) ; } fs . writeFileSync ( genesisPath , genesis ) ; if ( verbose ) { console . log ( `wrote genesis to ${ genesisPath }` ) ; } } export async function writeGenesisWithMigrations ( gethConfig , gethRepoPath , mnemonic , numValidators , verbose = false ) { const genesis = await generateGenesisWithMigrations ( { gethRepoPath , mnemonic , numValidators , verbose , genesisConfig : { blockTime : 1 , epoch : 10 , lookbackwindow : 3 , requestTimeout : 3000 , chainId : gethConfig . networkId , ... gethConfig . genesisConfig , } , } ) ; const genesisPath = path . join ( gethConfig . runPath , 'genesis.json' ) ; if ( verbose ) { console . log ( 'writing genesis' ) ; } fs . writeFileSync ( genesisPath , genesis ) ; if ( verbose ) { console . log ( `wrote genesis to ${ genesisPath }` ) ; } } export async function snapshotDatadir ( runPath , instance , verbose ) { if ( verbose ) { console . log ( 'snapshotting data dir' ) ; } await spawnCmd ( 'rm' , [ `${ getDatadir ( runPath , instance ) }/geth.ipc` ] , { silent : true } ) ; await spawnCmdWithExitOnFailure ( 'cp' , [ '-r' , getDatadir ( runPath , instance ) , getSnapshotdir ( runPath , instance ) , ] ) ; } export async function restoreDatadir ( runPath , instance ) { const datadir = getDatadir ( runPath , instance ) ; const snapshotdir = getSnapshotdir ( runPath , instance ) ; console . info ( `geth:${ instance . name }: restore datadir: ${ datadir }` ) ; await spawnCmdWithExitOnFailure ( 'rm' , [ '-rf' , datadir ] , { silent : true } ) ; await spawnCmdWithExitOnFailure ( 'cp' , [ '-r' , snapshotdir , datadir ] , { silent : true } ) ; } export async function buildGeth ( gethPath ) { await spawnCmdWithExitOnFailure ( 'make' , [ 'geth' ] , { cwd : gethPath } ) ; } export async function buildGethAll ( gethPath ) { await spawnCmdWithExitOnFailure ( 'make' , [ 'all' ] , { cwd : gethPath } ) ; } export async function resetDataDir ( dataDir , verbose ) { await spawnCmd ( 'rm' , [ '-rf' , dataDir ] , { silent : ! verbose } ) ; await spawnCmd ( 'mkdir' , [ dataDir ] , { silent : ! verbose } ) ; } export async function checkoutGethRepo ( branch , gethPath ) { await spawnCmdWithExitOnFailure ( 'rm' , [ '-rf' , gethPath ] ) ; await spawnCmdWithExitOnFailure ( 'git' , [ 'clone' , '--depth' , '1' , 'https://github.com/celo-org/celo-blockchain.git' , gethPath , '-b' , branch , ] ) ; await spawnCmdWithExitOnFailure ( 'git' , [ 'checkout' , branch ] , { cwd : gethPath } ) ; } export function spawnWithLog ( cmd , args , logsFilepath , verbose ) { try { fs . unlinkSync ( logsFilepath ) ; } catch ( error ) { } const logStream = fs . createWriteStream ( logsFilepath , { flags : 'a' } ) ; if ( verbose ) { console . log ( cmd , ... args ) ; } const p = spawn ( cmd , args ) ; p . stdout . pipe ( logStream ) ; p . stderr . pipe ( logStream ) ; if ( verbose ) { p . stdout . pipe ( process . stdout ) ; p . stderr . pipe ( process . stderr ) ; } return p ; } export async function connectPeers ( instances , verbose = false ) { await connectBipartiteClique ( instances , instances , verbose ) ; } export async function connectBipartiteClique ( left , right , verbose = false ) { const admins = ( instances ) => instances . map ( ( { wsport , rpcport } ) => new Admin ( `${ rpcport ? 'http' : 'ws' }://localhost:${ rpcport || wsport }` ) ) ; const connect = async ( sources , targets ) => { const targetEnodes = await Promise . all ( admins ( targets ) . map ( async ( a ) => ( await a . getNodeInfo ( ) ) . enode ) ) ; await Promise . all ( admins ( sources ) . map ( async ( admin ) => { const sourceEnode = ( await admin . getNodeInfo ( ) ) . enode ; await Promise . all ( targetEnodes . map ( async ( enode ) => { if ( sourceEnode === enode ) { return ; } if ( verbose ) { console . log ( `connecting ${ sourceEnode } with ${ enode }` ) ; } const success = await admin . addPeer ( enode ) ; if ( ! success ) { throw new Error ( 'Connecting geth peers failed!' ) ; } } ) ) ; } ) ) ; } ; await connect ( left , right ) ; await connect ( right , left ) ; } export async function connectValidatorPeers ( instances ) { const validators = instances . filter ( ( node ) => ( node . validating && ! node . isProxied ) || node . isProxy ) ; const isolated = validators . filter ( ( node ) => ! node . bootnodeEnode ) ; if ( isolated . length <= 0 ) { return ; } const root = validators . find ( ( node ) => node . bootnodeEnode ) ?? validators [ 0 ] ; await connectBipartiteClique ( [ root ] , isolated ) ; } export async function migrateContracts ( monorepoRoot , validatorPrivateKeys , attestationKeys , validators , to = 1000 , overrides = { } , verbose = true ) { const migrationOverrides = merge ( { stableToken : { initialBalances : { addresses : validators . map ( ensure0x ) , values : validators . map ( ( ) => '10000000000000000000000' ) , } , oracles : validators . map ( ensure0x ) , } , validators : { validatorKeys : validatorPrivateKeys . map ( ensure0x ) , attestationKeys : attestationKeys . map ( ensure0x ) , } , blockchainParameters : { uptimeLookbackWindow : 3 , } , } , overrides ) ; const args = [ '--cwd' , `${ monorepoRoot }/packages/protocol` , 'init-network' , '-n' , 'testing' , '-m' , JSON . stringify ( migrationOverrides ) , '-t' , to . toString ( ) , ] ; await spawnCmdWithExitOnFailure ( 'yarn' , args , { silent : ! verbose } ) ; }