aboutsummaryrefslogblamecommitdiffstats
path: root/packages/sol-tracing-utils/src/trace_collection_subprovider.ts
blob: d79c5ca22d98659ce3832f69d1f36f8716d0f3c8 (plain) (tree)
1
2
3
4
5
6
7
8
9
                                                    
                                     

                                                                                      
                                                                         



                                             
                                            












                                                   

















                                                                          





                                                                                                                       

                                                                                                                   
   
                                                                      
                                         
                                                                                                                                 

                                                 
                              
                                                               









                                                                                       
















                                                                                         
                                                                                                          

                                                                        
                                                                                                                        






                                                                       
                                                                                              







                                                                
                                                                                                          







                                                                       
                                                                                                                 















                                                                             
                                                                    




                                                    




                                                 





                                          













                                                                                                                   

                                                                                                                
                                                                                               

             





                                                                                   
     
                                                    
                                       

                            

                      

                                                                
     
                                                                                                     



                                                                                                            
                                               

                                                                                             




                                                                                                                         

                                                                                
                                                                          
                       
                                                                                        
                                                                                   
                     
         


                                                
 
import { BlockchainLifecycle } from '@0x/dev-utils';
import { logUtils } from '@0x/utils';
import { Callback, ErrorCallback, NextCallback, Subprovider } from '@0x/subproviders';
import { CallDataRPC, marshaller, Web3Wrapper } from '@0x/web3-wrapper';
import { JSONRPCRequestPayload, Provider, TxData } from 'ethereum-types';
import * as _ from 'lodash';
import { Lock } from 'semaphore-async-await';

import { constants } from './constants';
import { BlockParamLiteral } from './types';

interface MaybeFakeTxData extends TxData {
    isFakeTransaction?: boolean;
}

const BLOCK_GAS_LIMIT = 6000000;

export interface TraceCollectionSubproviderConfig {
    shouldCollectTransactionTraces: boolean;
    shouldCollectCallTraces: boolean;
    shouldCollectGasEstimateTraces: boolean;
}

type AsyncFunc = (...args: any[]) => Promise<void>;

// This wrapper outputs errors to console even if the promise gets ignored
// we need this because web3-provider-engine does not handle promises in
// the after function of next(after).
function logAsyncErrors(fn: AsyncFunc): AsyncFunc {
    async function wrappedAsync(...args: any[]): Promise<void> {
        try {
            await fn(...args);
        } catch (err) {
            // tslint:disable-next-line no-console
            logUtils.error(err);
            throw err;
        }
    }
    return wrappedAsync;
}

// Because there is no notion of a call trace in the Ethereum rpc - we collect them in a rather non-obvious/hacky way.
// On each call - we create a snapshot, execute the call as a transaction, get the trace, revert the snapshot.
// That allows us to avoid influencing test behaviour.

/**
 * This class implements the [web3-provider-engine](https://github.com/MetaMask/provider-engine) subprovider interface.
 * It collects traces of all transactions that were sent and all calls that were executed through JSON RPC. It must
 * be extended by implementing the _recordTxTraceAsync method which is called for every transaction.
 */
export abstract class TraceCollectionSubprovider extends Subprovider {
    protected _web3Wrapper!: Web3Wrapper;
    // Lock is used to not accept normal transactions while doing call/snapshot magic because they'll be reverted later otherwise
    private readonly _lock = new Lock();
    private readonly _defaultFromAddress: string;
    private _isEnabled = true;
    private readonly _config: TraceCollectionSubproviderConfig;
    /**
     * Instantiates a TraceCollectionSubprovider instance
     * @param defaultFromAddress default from address to use when sending transactions
     */
    constructor(defaultFromAddress: string, config: TraceCollectionSubproviderConfig) {
        super();
        this._defaultFromAddress = defaultFromAddress;
        this._config = config;
    }
    /**
     * Starts trace collection
     */
    public start(): void {
        this._isEnabled = true;
    }
    /**
     * Stops trace collection
     */
    public stop(): void {
        this._isEnabled = false;
    }
    /**
     * This method conforms to the web3-provider-engine interface.
     * It is called internally by the ProviderEngine when it is this subproviders
     * turn to handle a JSON RPC request.
     * @param payload JSON RPC payload
     * @param next Callback to call if this subprovider decides not to handle the request
     * @param _end Callback to call if subprovider handled the request and wants to pass back the request.
     */
    // tslint:disable-next-line:prefer-function-over-method async-suffix
    public async handleRequest(payload: JSONRPCRequestPayload, next: NextCallback, _end: ErrorCallback): Promise<void> {
        if (this._isEnabled) {
            switch (payload.method) {
                case 'eth_sendTransaction':
                    if (!this._config.shouldCollectTransactionTraces) {
                        next();
                    } else {
                        const txData = payload.params[0];
                        next(logAsyncErrors(this._onTransactionSentAsync.bind(this, txData)));
                    }
                    return;

                case 'eth_call':
                    if (!this._config.shouldCollectCallTraces) {
                        next();
                    } else {
                        const callData = payload.params[0];
                        next(logAsyncErrors(this._onCallOrGasEstimateExecutedAsync.bind(this, callData)));
                    }
                    return;

                case 'eth_estimateGas':
                    if (!this._config.shouldCollectGasEstimateTraces) {
                        next();
                    } else {
                        const estimateGasData = payload.params[0];
                        next(logAsyncErrors(this._onCallOrGasEstimateExecutedAsync.bind(this, estimateGasData)));
                    }
                    return;

                default:
                    next();
                    return;
            }
        } else {
            next();
            return;
        }
    }
    /**
     * Set's the subprovider's engine to the ProviderEngine it is added to.
     * This is only called within the ProviderEngine source code, do not call
     * directly.
     * @param engine The ProviderEngine this subprovider is added to
     */
    public setEngine(engine: Provider): void {
        super.setEngine(engine);
        this._web3Wrapper = new Web3Wrapper(engine);
    }
    protected abstract async _recordTxTraceAsync(
        address: string,
        data: string | undefined,
        txHash: string,
    ): Promise<void>;
    private async _onTransactionSentAsync(
        txData: MaybeFakeTxData,
        err: Error | null,
        txHash: string | undefined,
        cb: Callback,
    ): Promise<void> {
        if (!txData.isFakeTransaction) {
            // This transaction is a usual transaction. Not a call executed as one.
            // And we don't want it to be executed within a snapshotting period
            await this._lock.acquire();
        }
        const NULL_ADDRESS = '0x0';
        if (_.isNull(err)) {
            const toAddress =
                _.isUndefined(txData.to) || txData.to === NULL_ADDRESS ? constants.NEW_CONTRACT : txData.to;
            await this._recordTxTraceAsync(toAddress, txData.data, txHash as string);
        } else {
            const latestBlock = await this._web3Wrapper.getBlockWithTransactionDataAsync(BlockParamLiteral.Latest);
            const transactions = latestBlock.transactions;
            for (const transaction of transactions) {
                const toAddress =
                    _.isUndefined(txData.to) || txData.to === NULL_ADDRESS ? constants.NEW_CONTRACT : txData.to;
                await this._recordTxTraceAsync(toAddress, transaction.input, transaction.hash);
            }
        }
        if (!txData.isFakeTransaction) {
            // This transaction is a usual transaction. Not a call executed as one.
            // And we don't want it to be executed within a snapshotting period
            this._lock.release();
        }
        cb();
    }
    private async _onCallOrGasEstimateExecutedAsync(
        callData: Partial<CallDataRPC>,
        _err: Error | null,
        _callResult: string,
        cb: Callback,
    ): Promise<void> {
        await this._recordCallOrGasEstimateTraceAsync(callData);
        cb();
    }
    private async _recordCallOrGasEstimateTraceAsync(callData: Partial<CallDataRPC>): Promise<void> {
        // We don't want other transactions to be exeucted during snashotting period, that's why we lock the
        // transaction execution for all transactions except our fake ones.
        await this._lock.acquire();
        const blockchainLifecycle = new BlockchainLifecycle(this._web3Wrapper);
        await blockchainLifecycle.startAsync();
        const fakeTxData = {
            gas: BLOCK_GAS_LIMIT.toString(16), // tslint:disable-line:custom-no-magic-numbers
            isFakeTransaction: true, // This transaction (and only it) is allowed to come through when the lock is locked
            ...callData,
            from: callData.from || this._defaultFromAddress,
        };
        try {
            const txData = marshaller.unmarshalTxData(fakeTxData);
            const txHash = await this._web3Wrapper.sendTransactionAsync(txData);
            await this._web3Wrapper.awaitTransactionMinedAsync(txHash, 0);
        } catch (err) {
            // TODO(logvinov) Check that transaction failed and not some other exception
            // Even if this transaction failed - we've already recorded it's trace.
            _.noop();
        }
        await blockchainLifecycle.revertAsync();
        this._lock.release();
    }
}