diff --git a/packages/codec/lib/core.ts b/packages/codec/lib/core.ts index 8ec4af5caef..611949ed72a 100644 --- a/packages/codec/lib/core.ts +++ b/packages/codec/lib/core.ts @@ -79,7 +79,9 @@ export function* decodeCalldata( let selector: string; //first: is this a creation call? if (isConstructor) { - allocation = allocations.constructorAllocations[contextHash].input; + allocation = ( + allocations.constructorAllocations[contextHash] || { input: undefined } + ).input; } else { //skipping any error-handling on this read, as a calldata read can't throw anyway let rawSelector = yield* read( @@ -92,7 +94,7 @@ export function* decodeCalldata( ); selector = Conversion.toHexString(rawSelector); allocation = ( - allocations.functionAllocations[contextHash][selector] || { + (allocations.functionAllocations[contextHash] || {})[selector] || { input: undefined } ).input; diff --git a/packages/debugger/lib/controller/sagas/index.js b/packages/debugger/lib/controller/sagas/index.js index 0affd06e49e..c1b137134c5 100644 --- a/packages/debugger/lib/controller/sagas/index.js +++ b/packages/debugger/lib/controller/sagas/index.js @@ -7,6 +7,7 @@ import {prefixName, isDeliberatelySkippedNodeType} from "lib/helpers"; import * as trace from "lib/trace/sagas"; import * as data from "lib/data/sagas"; +import * as txlog from "lib/txlog/sagas"; import * as evm from "lib/evm/sagas"; import * as solidity from "lib/solidity/sagas"; import * as stacktrace from "lib/stacktrace/sagas"; @@ -310,4 +311,5 @@ export function* reset() { yield* solidity.reset(); yield* trace.reset(); yield* stacktrace.reset(); + yield* txlog.reset(); } diff --git a/packages/debugger/lib/data/sagas/index.js b/packages/debugger/lib/data/sagas/index.js index 0b5386e71bb..f3474a85fd1 100644 --- a/packages/debugger/lib/data/sagas/index.js +++ b/packages/debugger/lib/data/sagas/index.js @@ -131,6 +131,7 @@ export function* decodeReturnValue() { const contexts = yield select(data.views.contexts); const status = yield select(data.current.returnStatus); //may be undefined const returnAllocation = yield select(data.current.returnAllocation); //may be null + debug("returnAllocation: %O", returnAllocation); const decoder = Codec.decodeReturndata( { @@ -165,6 +166,70 @@ export function* decodeReturnValue() { return result.value; } +//by default, decodes the call being made at the current step; +//if the flag is passed, instead decodes the call you're currently in +export function* decodeCall(decodeCurrent = false) { + const isCall = yield select(data.current.isCall); + const isCreate = yield select(data.current.isCreate); + if (!isCall && !isCreate && !decodeCurrent) { + return null; + } + const currentCallIsCreate = yield select(data.current.currentCallIsCreate); + const userDefinedTypes = yield select(data.views.userDefinedTypes); + let state = decodeCurrent + ? yield select(data.current.state) + : yield select(data.next.state); + if (decodeCurrent && currentCallIsCreate) { + //if we want to decode the *current* call, but the current call + //is a creation, we had better pass in the code, not the calldata + state = { + ...state, + calldata: state.code + }; + } + const allocations = yield select(data.info.allocations); + debug("allocations: %O", allocations); + const contexts = yield select(data.views.contexts); + const context = decodeCurrent + ? yield select(data.current.context) + : yield select(data.current.callContext); + const isConstructor = decodeCurrent + ? yield select(data.current.currentCallIsCreate) + : isCreate; + + const decoder = Codec.decodeCalldata( + { + state, + userDefinedTypes, + allocations, + contexts, + currentContext: context + }, + isConstructor + ); + + debug("beginning decoding"); + let result = decoder.next(); + while (!result.done) { + debug("request received"); + let request = result.value; + let response; + switch (request.type) { + //skip storage case, it won't happen here + case "code": + response = yield* requestCode(request.address); + break; + default: + debug("unrecognized request type!"); + } + debug("sending response"); + result = decoder.next(response); + } + //at this point, result.value holds the final value + debug("done decoding"); + return result.value; +} + //NOTE: calling this *can* add a new instance, which will not //go away on a reset! Yes, this is a little weird, but we //decided this is OK for now diff --git a/packages/debugger/lib/data/selectors/index.js b/packages/debugger/lib/data/selectors/index.js index 7176c955a89..4a79d6c80a3 100644 --- a/packages/debugger/lib/data/selectors/index.js +++ b/packages/debugger/lib/data/selectors/index.js @@ -107,6 +107,10 @@ function debuggerContextToDecoderContext(context) { contractKind, isConstructor, abi: Codec.AbiData.Utils.computeSelectors(abi), + fallbackAbi: { + fallback: (abi || []).find(item => item.type === "fallback") || null, + receive: (abi || []).find(item => item.type === "receive") || null + }, payable, compiler, compilationId @@ -885,7 +889,7 @@ const data = createSelectorTree({ } ), - /* + /** * data.current.inModifier */ inModifier: createLeaf( @@ -893,7 +897,7 @@ const data = createSelectorTree({ node => node && node.nodeType === "ModifierDefinition" ), - /* + /** * data.current.inFunctionOrModifier */ inFunctionOrModifier: createLeaf( @@ -1404,6 +1408,33 @@ const data = createSelectorTree({ return allocation.output; } } + ), + + /** + * data.current.isCall + */ + isCall: createLeaf([evm.current.step.isCall], identity), + + /** + * data.current.isCreate + */ + isCreate: createLeaf([evm.current.step.isCreate], identity), + + /** + * data.current.currentCallIsCreate + */ + currentCallIsCreate: createLeaf( + [evm.current.call], + call => call.binary !== undefined + ), + + /** + * data.current.callContext + * note that we convert to decoder context! + */ + callContext: createLeaf( + [evm.current.step.callContext], + debuggerContextToDecoderContext ) }, @@ -1428,12 +1459,32 @@ const data = createSelectorTree({ /** * data.next.state.returndata - * NOTE: this is only for use by returnValue(); this is *not* + * NOTE: this is only for use by decodeReturnValue(); this is *not* * an accurate reflection of the current contents of returndata! * we don't track that at the moment */ returndata: createLeaf([evm.current.step.returnValue], data => Codec.Conversion.toBytes(data) + ), + + /** + * data.next.state.calldata + * NOTE: this is only for use by decodeCall(); this is *not* + * necessarily the actual next contents of calldata! + */ + calldata: createLeaf( + [ + evm.current.step.isCall, + evm.current.step.isCreate, + evm.current.step.callData, + evm.current.step.createBinary + ], + (isCall, isCreate, data, binary) => { + if (!isCall && !isCreate) { + return null; + } + return Codec.Conversion.toBytes(isCall ? data : binary); + } ) }, @@ -1475,7 +1526,7 @@ const data = createSelectorTree({ } ), - /* + /** * data.next.modifierBeingInvoked */ modifierBeingInvoked: createLeaf( diff --git a/packages/debugger/lib/debugger.js b/packages/debugger/lib/debugger.js index 5256edbf810..c73f974e372 100644 --- a/packages/debugger/lib/debugger.js +++ b/packages/debugger/lib/debugger.js @@ -6,6 +6,7 @@ import Session from "./session"; import { createNestedSelector } from "reselect-tree"; import dataSelector from "./data/selectors"; +import txlogSelector from "./txlog/selectors"; import astSelector from "./ast/selectors"; import traceSelector from "./trace/selectors"; import evmSelector from "./evm/selectors"; @@ -26,7 +27,7 @@ const Debugger = { * @param {{contracts: Array, files: Array, provider: Web3Provider, compilations: Array}} options - * @return {Debugger} instance */ - forTx: async function(txHash, options = {}) { + forTx: async function (txHash, options = {}) { let { contracts, files, provider, compilations, lightMode } = options; if (!compilations) { compilations = Compilations.Utils.shimArtifacts(contracts, files); @@ -44,7 +45,7 @@ const Debugger = { * @param {{contracts: Array, files: Array, provider: Web3Provider, compilations: Array}} options - * @return {Debugger} instance */ - forProject: async function(options = {}) { + forProject: async function (options = {}) { let { contracts, files, provider, compilations, lightMode } = options; if (!compilations) { compilations = Compilations.Utils.shimArtifacts(contracts, files); @@ -74,6 +75,7 @@ const Debugger = { return createNestedSelector({ ast: astSelector, data: dataSelector, + txlog: txlogSelector, trace: traceSelector, evm: evmSelector, solidity: soliditySelector, diff --git a/packages/debugger/lib/evm/selectors/index.js b/packages/debugger/lib/evm/selectors/index.js index 6b539c52750..42128f81016 100644 --- a/packages/debugger/lib/evm/selectors/index.js +++ b/packages/debugger/lib/evm/selectors/index.js @@ -14,7 +14,8 @@ import { isShortCallMnemonic, isDelegateCallMnemonicBroad, isDelegateCallMnemonicStrict, - isStaticCallMnemonic + isStaticCallMnemonic, + isSelfDestructMnemonic } from "lib/helpers"; const ZERO_WORD = "00".repeat(Codec.Evm.Utils.WORD_SIZE); @@ -126,6 +127,13 @@ function createStepSelectors(step, state = null) { */ isCreate: createLeaf(["./trace"], step => isCreateMnemonic(step.op)), + /** + * .isSelfDestruct + */ + isSelfDestruct: createLeaf(["./trace"], step => + isSelfDestructMnemonic(step.op) + ), + /** * .isCreate2 */ @@ -319,6 +327,21 @@ function createStepSelectors(step, state = null) { } ), + /** + * .salt + */ + salt: createLeaf( + ["./isCreate2", state], + + (isCreate2, { stack }) => { + if (!isCreate2) { + return null; + } + + return "0x" + stack[stack.length - 4]; + } + ), + /** * .callContext * @@ -540,7 +563,7 @@ const evm = createSelectorTree({ ), /** - * evm.current.step.isInstantCallOrReturn + * evm.current.step.isInstantCallOrCreate * * are we doing a call or create for which there are no trace steps? * This can happen if: @@ -564,7 +587,7 @@ const evm = createSelectorTree({ ), /** - * .isNormalHalting + * evm.current.step.isNormalHalting */ isNormalHalting: createLeaf( ["./isHalting", "./returnStatus"], @@ -572,7 +595,7 @@ const evm = createSelectorTree({ ), /** - * .isHalting + * evm.current.step.isHalting * * whether the instruction halts or returns from a calling context * HACK: the check for stepsRemainining === 0 is a hack to cover @@ -595,19 +618,20 @@ const evm = createSelectorTree({ /** * evm.current.step.returnStatus - * checks the return status of the *current* halting instruction - * returns null if not halting + * checks the return status of the *current* halting instruction or insta-call + * returns null if not halting & not an insta-call * (returns a boolean -- true for success, false for failure) */ returnStatus: createLeaf( [ "./isHalting", + "./isInstantCallOrCreate", "/next/state", trace.stepsRemaining, "/transaction/status" ], - (isHalting, { stack }, remaining, finalStatus) => { - if (!isHalting) { + (isHalting, isInstaCall, { stack }, remaining, finalStatus) => { + if (!isHalting && !isInstaCall) { return null; //not clear this'll do much good since this may get //read as false, but, oh well, may as well } @@ -670,6 +694,24 @@ const evm = createSelectorTree({ } return stack[stack.length - 1]; } + ), + + /** + * evm.current.step.beneficiary + * NOTE: for a value-destroying selfdestruct, returns null + */ + beneficiary: createLeaf( + ["./isSelfDestruct", "../state", "../call"], + + (isSelfDestruct, { stack }, { storageAddress: currentAddress }) => { + if (!isSelfDestruct) { + return null; + } + const beneficiary = Codec.Evm.Utils.toAddress( + stack[stack.length - 1] + ); + return beneficiary !== currentAddress ? beneficiary : null; + } ) }, diff --git a/packages/debugger/lib/session/index.js b/packages/debugger/lib/session/index.js index 00bd1bfa7bf..bf8f62db2af 100644 --- a/packages/debugger/lib/session/index.js +++ b/packages/debugger/lib/session/index.js @@ -10,6 +10,7 @@ import configureStore from "lib/store"; import * as controller from "lib/controller/actions"; import * as actions from "./actions"; import data from "lib/data/selectors"; +import txlog from "lib/txlog/selectors"; import stacktrace from "lib/stacktrace/selectors"; import session from "lib/session/selectors"; import * as dataSagas from "lib/data/sagas"; @@ -513,6 +514,7 @@ export default class Session { return createNestedSelector({ ast, data, + txlog, trace, evm, solidity, diff --git a/packages/debugger/lib/session/reducers.js b/packages/debugger/lib/session/reducers.js index 5263f308a01..09528bf30bc 100644 --- a/packages/debugger/lib/session/reducers.js +++ b/packages/debugger/lib/session/reducers.js @@ -9,6 +9,7 @@ import solidity from "lib/solidity/reducers"; import trace from "lib/trace/reducers"; import controller from "lib/controller/reducers"; import stacktrace from "lib/stacktrace/reducers"; +import txlog from "lib/txlog/reducers"; import * as actions from "./actions"; @@ -96,6 +97,7 @@ const session = combineReducers({ const reduceState = combineReducers({ session, data, + txlog, evm, solidity, stacktrace, diff --git a/packages/debugger/lib/session/sagas/index.js b/packages/debugger/lib/session/sagas/index.js index 8eace2df489..3e8d82f3453 100644 --- a/packages/debugger/lib/session/sagas/index.js +++ b/packages/debugger/lib/session/sagas/index.js @@ -12,6 +12,7 @@ import * as stacktrace from "lib/stacktrace/sagas"; import * as evm from "lib/evm/sagas"; import * as trace from "lib/trace/sagas"; import * as data from "lib/data/sagas"; +import * as txlog from "lib/txlog/sagas"; import * as web3 from "lib/web3/sagas"; import * as actions from "../actions"; @@ -107,8 +108,11 @@ function* startFullMode() { //better not start this twice! return; } - debug("turning on data listener"); - yield fork(data.saga); + debug("turning on data & txlog listeners"); + const listenersToActivate = [data.saga, txlog.saga]; + for (let listener of listenersToActivate) { + yield fork(listener); + } debug("visiting ASTs"); // visit asts @@ -118,7 +122,10 @@ function* startFullMode() { debug("saving allocation table"); yield* data.recordAllocations(); - yield* trace.addSubmoduleToCount(); + yield* trace.addSubmoduleToCount(listenersToActivate.length); + + //begin any full-mode modules that need beginning + yield* txlog.begin(); yield put(actions.setFullMode()); } @@ -141,6 +148,7 @@ function* forkListeners(moduleOptions) { let mainApps = [evm, solidity, stacktrace]; if (!moduleOptions.lightMode) { mainApps.push(data); + mainApps.push(txlog); } let otherApps = [trace, controller, web3]; const submoduleCount = mainApps.length; @@ -192,9 +200,13 @@ function* fetchTx(txHash) { ); debug("sending initial call"); - yield* evm.begin(result); //note: this must occur *before* the other two + yield* evm.begin(result); //note: this must occur *before* the other ones! yield* solidity.begin(); yield* stacktrace.begin(); + if (!(yield select(session.status.lightMode))) { + //full-mode-only modules + yield* txlog.begin(); + } } function* recordContexts(contexts) { @@ -237,6 +249,7 @@ export function* unload() { yield* evm.unload(); yield* trace.unload(); yield* stacktrace.unload(); + yield* txlog.unload(); yield put(actions.unloadTransaction()); } diff --git a/packages/debugger/lib/session/selectors/index.js b/packages/debugger/lib/session/selectors/index.js index 318d99db617..124cf7de301 100644 --- a/packages/debugger/lib/session/selectors/index.js +++ b/packages/debugger/lib/session/selectors/index.js @@ -64,7 +64,7 @@ const session = createSelectorTree({ let constructorArgs; if (creationBinary !== undefined) { let creationContext = contexts[creationContextId]; - if (creationContext !== null) { + if (creationContext) { //slice off the bytecode part of the constructor to leave the arguments constructorArgs = creationBinary.slice( creationContext.binary.length diff --git a/packages/debugger/lib/solidity/selectors/index.js b/packages/debugger/lib/solidity/selectors/index.js index 37bb34ac791..f3c16079364 100644 --- a/packages/debugger/lib/solidity/selectors/index.js +++ b/packages/debugger/lib/solidity/selectors/index.js @@ -265,11 +265,12 @@ let solidity = createSelectorTree({ [ "./instructionAtProgramCounter", evm.current.step.programCounter, - evm.next.step.programCounter + evm.next.step.programCounter, + evm.current.step.isContextChange ], - (map, current, next) => { - if (!map[next]) { + (map, current, next, changesContext) => { + if (changesContext || !map[next]) { return true; } diff --git a/packages/debugger/lib/trace/sagas/index.js b/packages/debugger/lib/trace/sagas/index.js index 966851ee09e..8a5b103c968 100644 --- a/packages/debugger/lib/trace/sagas/index.js +++ b/packages/debugger/lib/trace/sagas/index.js @@ -19,9 +19,9 @@ export function* setSubmoduleCount(count) { yield put(actions.setSubmoduleCount(count)); } -export function* addSubmoduleToCount() { +export function* addSubmoduleToCount(increment = 1) { let count = yield select(trace.application.submoduleCount); - yield put(actions.setSubmoduleCount(count + 1)); + yield put(actions.setSubmoduleCount(count + increment)); } export function* advance() { diff --git a/packages/debugger/lib/txlog/actions/index.js b/packages/debugger/lib/txlog/actions/index.js new file mode 100644 index 00000000000..edcb8ca156b --- /dev/null +++ b/packages/debugger/lib/txlog/actions/index.js @@ -0,0 +1,203 @@ +export const INTERNAL_CALL = "TXLOG_INTERNAL_CALL"; +export function internalCall(pointer, newPointer) { + return { + type: INTERNAL_CALL, + pointer, + newPointer + }; +} + +export const ABSORBED_CALL = "TXLOG_ABSORBED_CALL"; +export function absorbedCall(pointer) { + return { + type: ABSORBED_CALL, + pointer + }; +} + +export const INTERNAL_RETURN = "TXLOG_INTERNAL_RETURN"; +export function internalReturn(pointer, newPointer, variables) { + return { + type: INTERNAL_RETURN, + pointer, + newPointer, + variables + }; +} + +export const EXTERNAL_CALL = "TXLOG_EXTERNAL_CALL"; +export function externalCall( + pointer, + newPointer, + address, + context, + value, + isDelegate, + kind, + decoding, + calldata, + absorbNextInternalCall +) { + return { + type: EXTERNAL_CALL, + pointer, + newPointer, + address, + context, + value, + isDelegate, + kind, + decoding, + calldata, + absorbNextInternalCall + }; +} + +export const INSTANT_EXTERNAL_CALL = "TXLOG_INSTANT_EXTERNAL_CALL"; +export function instantExternalCall( + pointer, + newPointer, //does not actually affect the current pointer! + address, + context, + value, + isDelegate, + kind, + decoding, + calldata, + absorbNextInternalCall, + status +) { + return { + type: INSTANT_EXTERNAL_CALL, + pointer, + newPointer, + address, + context, + value, + isDelegate, + kind, + decoding, + calldata, + absorbNextInternalCall, + status + }; +} + +export const CREATE = "TXLOG_CREATE"; +export function create( + pointer, + newPointer, + address, + context, + value, + salt, + decoding, + binary +) { + return { + type: CREATE, + pointer, + newPointer, + address, + context, + value, + salt, + decoding, + binary + }; +} + +export const INSTANT_CREATE = "TXLOG_INSTANT_CREATE"; +export function instantCreate( + pointer, + newPointer, //does not actually affect the current pointer! + address, + context, + value, + salt, + decoding, + binary, + status +) { + return { + type: INSTANT_CREATE, + pointer, + newPointer, + address, + context, + value, + salt, + decoding, + binary, + status + }; +} + +export const EXTERNAL_RETURN = "TXLOG_EXTERNAL_RETURN"; +export function externalReturn(pointer, newPointer, decodings) { + return { + type: EXTERNAL_RETURN, + pointer, + newPointer, + decodings + }; +} + +export const SELFDESTRUCT = "TXLOG_SELFDESTRUCT"; +export function selfdestruct(pointer, newPointer, beneficiary) { + return { + type: SELFDESTRUCT, + pointer, + newPointer, + beneficiary + }; +} + +export const REVERT = "TXLOG_REVERT"; +export function revert(pointer, newPointer, error) { + return { + type: REVERT, + pointer, + newPointer, + error + }; +} + +export const IDENTIFY_FUNCTION_CALL = "TXLOG_IDENTIFY_FUNCTION_CALL"; +export function identifyFunctionCall( + pointer, + functionNode, + contractNode, + variables +) { + return { + type: IDENTIFY_FUNCTION_CALL, + pointer, + functionNode, + contractNode, + variables + }; +} + +export const RECORD_ORIGIN = "TXLOG_RECORD_ORIGIN"; +export function recordOrigin(pointer, address) { + return { + type: RECORD_ORIGIN, + pointer, + address + }; +} + +export const RESET = "TXLOG_RESET"; +export function reset() { + return { + type: RESET + }; +} + +export const UNLOAD_TRANSACTION = "TXLOG_UNLOAD_TRANSACTION"; +export function unloadTransaction() { + return { + type: UNLOAD_TRANSACTION + }; +} diff --git a/packages/debugger/lib/txlog/reducers.js b/packages/debugger/lib/txlog/reducers.js new file mode 100644 index 00000000000..19d643a099c --- /dev/null +++ b/packages/debugger/lib/txlog/reducers.js @@ -0,0 +1,372 @@ +import debugModule from "debug"; +const debug = debugModule("debugger:txlog:reducers"); + +import {combineReducers} from "redux"; + +import * as actions from "./actions"; + +//NOTE: even though we refer to nodes by JSON pointer, +//these pointers are "fake" in that we don't actually +//use them *as* JSON pointers; it's just a convenient +//method of IDing them that also has a nice intuitive +//meaning (you'll notice we don't actually import +//json-pointer here or anywhere else in this submodule) +const DEFAULT_TX_LOG = { + byPointer: { + "": { + // "" is the root node + type: "transaction", + actions: [] + } + } +}; + +function transactionLog(state = DEFAULT_TX_LOG, action) { + const {pointer, newPointer} = action; + const node = state.byPointer[pointer]; + switch (action.type) { + case actions.RECORD_ORIGIN: + if (node.type === "transaction") { + return { + byPointer: { + ...state.byPointer, + [pointer]: { + ...node, + origin: action.address + } + } + }; + } else { + debug("attempt to set origin of bad node type!"); + return state; + } + case actions.INTERNAL_CALL: + return { + byPointer: { + ...state.byPointer, + [pointer]: { + ...node, + actions: [...node.actions, newPointer] + }, + [newPointer]: { + type: "callinternal", + actions: [], + waitingForFunctionDefinition: true + } + } + }; + case actions.ABSORBED_CALL: + return { + byPointer: { + ...state.byPointer, + [pointer]: { + ...node, + absorbNextInternalCall: false + } + } + }; + case actions.INTERNAL_RETURN: + //pop the top call from the stack if it's internal (and set its return values) + //if the top call is instead external, just set its return values if appropriate. + //(this is how we handle internal/external return absorption) + const modifiedNode = {...node}; + if (modifiedNode.type === "callinternal") { + modifiedNode.returnKind = "return"; + modifiedNode.returnValues = action.variables; + delete modifiedNode.waitingForFunctionDefinition; + } else if (modifiedNode.type === "callexternal") { + if (modifiedNode.kind === "function") { + //don't set return variables for non-function external calls + modifiedNode.returnValues = action.variables; + } + } else { + debug("returninternal once tx done!"); + } + return { + byPointer: { + ...state.byPointer, + [pointer]: modifiedNode + } + }; + case actions.INSTANT_EXTERNAL_CALL: + case actions.EXTERNAL_CALL: + case actions.INSTANT_CREATE: + case actions.CREATE: { + const instant = + action.type === actions.INSTANT_EXTERNAL_CALL || + action.type === actions.INSTANT_CREATE; + let modifiedNode = { + ...node, + actions: [...node.actions, newPointer] + }; + if ( + modifiedNode.type === "callexternal" && + modifiedNode.kind === "library" + ) { + //didn't identify it as function, so set it to message + modifiedNode.kind = "message"; + } + const { + address, + binary, //only for creates + context, + value, + salt, //only for creates + isDelegate, + decoding, + calldata, + status + } = action; + let kind; + if ( + action.type === actions.CREATE || + action.type === actions.INSTANT_CREATE + ) { + //these don't have kind in the action, so we instead determine + //it this way + kind = context ? "constructor" : "unknowncreate"; + } else { + kind = action.kind; + } + const contractName = context ? context.contractName : undefined; + let functionName, variables; + if (decoding.kind === "function" || decoding.kind === "constructor") { + functionName = decoding.abi.name; + variables = decoding.arguments; + } + let call = { + type: "callexternal", + address, + contextHash: context.context || null, + value, + kind, + isDelegate, + functionName, + contractName, + arguments: variables, + actions: [] + }; + if (kind === "message" || kind === "library") { + call.data = calldata; + } else if (kind === "unknowncreate") { + call.binary = binary; + } + if (kind === "constructor" || kind === "unknowncreate") { + call.salt = salt; + } + if (instant) { + call.returnKind = status ? "return" : "revert"; + } else { + //If kind === "message", set waiting to false. + //Why? Well, because fallback functions and receive functions + //typically have their function definitions skipped over, so the next + //one we hit would instead be a function *called* from the fallback + //function, which is not what we want. + call.waitingForFunctionDefinition = kind !== "message"; + //if kind is message or constructor, we don't want to absorb. + call.absorbNextInternalCall = + (kind === "function" || kind === "library") && + action.absorbNextInternalCall; + } + return { + byPointer: { + ...state.byPointer, + [pointer]: modifiedNode, + [newPointer]: call + } + }; + } + case actions.EXTERNAL_RETURN: + case actions.REVERT: + case actions.SELFDESTRUCT: { + //first: set the returnKind and other info + let modifiedNode = {...node}; + if ( + modifiedNode.type === "callexternal" && + modifiedNode.kind === "library" + ) { + //didn't identify it as function, so set it to message + modifiedNode.kind = "message"; + } + switch (action.type) { + case actions.EXTERNAL_RETURN: + if (!modifiedNode.returnKind) { + modifiedNode.returnKind = "return"; + } + break; + case actions.REVERT: + modifiedNode.returnKind = "revert"; + modifiedNode.error = action.error; + break; + case actions.SELFDESTRUCT: + modifiedNode.returnKind = "selfdestruct"; + modifiedNode.beneficiary = action.beneficiary; + break; + } + let newState = { + byPointer: { + ...state.byPointer, + [pointer]: modifiedNode + } + }; + //now: pop all calls from stack until we pop an external call. + //we don't handle return values here since those are handled + //in returninternal (yay absorption) + let currentPointer; + for ( + currentPointer = pointer; + currentPointer.replace(/\/actions\/\d+$/, "") !== newPointer; //stop *before* the stop pointer + currentPointer = currentPointer.replace(/\/actions\/\d+$/, "") //cut off end + ) { + debug("currentNode!"); + let currentNode = {...newState.byPointer[currentPointer]}; //clone + if (!currentNode.returnKind) { + //set the return kind on any nodes popped along the way that don't have + //one already to note that they failed to return due to a call they made + //reverting + currentNode.returnKind = "unwind"; + } + delete currentNode.waitingForFunctionDefinition; + debug("set currentNode!"); + newState.byPointer[currentPointer] = currentNode; + } + //now handle the external call. + //note that currentPointer now points to it. + debug("finalNode!"); + let finalNode = {...newState.byPointer[currentPointer]}; //clone + //first let's set the returnKind if there isn't one already + //(in which case we can infer it was unwound). + if (!finalNode.returnKind) { + finalNode.returnKind = "unwind"; + } + //now let's set its return variables if applicable. + if ( + finalNode.kind === "function" && + action.type === actions.EXTERNAL_RETURN && + action.decodings + ) { + const decoding = action.decodings.find( + decoding => decoding.kind === "return" + ); + if (decoding) { + //we'll trust this method over the method resulting from an internal return, + //*if* it produces a valid return-value decoding. if it doesn't, we ignore it. + finalNode.returnValues = decoding.arguments; + } + } + //also, set immutables if applicable -- note that we do *not* attempt to set + //these the internal way, as we don't have a reliable way of doing that + if ( + finalNode.kind === "constructor" && + action.type === actions.EXTERNAL_RETURN && + action.decodings + ) { + const decoding = action.decodings.find( + decoding => decoding.kind === "bytecode" + ); + if (decoding && decoding.immutables) { + finalNode.returnImmutables = decoding.immutables; + } + } + //finally, delete internal info + delete finalNode.waitingForFunctionDefinition; + delete finalNode.absorbNextInternalCall; + debug("set finalNode!"); + newState.byPointer[currentPointer] = finalNode; + return newState; + } + case actions.IDENTIFY_FUNCTION_CALL: { + const {functionNode, contractNode, variables} = action; + const functionName = functionNode.name || undefined; //replace "" with undefined + const contractName = + contractNode && contractNode.nodeType === "ContractDefinition" + ? contractNode.name + : null; + let modifiedNode = { + ...node, + waitingForFunctionDefinition: false + }; + //note: I don't handle the following in the object spread above + //because I don't want undefined or null counting against it + if (!modifiedNode.functionName) { + modifiedNode.functionName = functionName; + } + if (!modifiedNode.contractName) { + modifiedNode.contractName = contractName; + } + if (!modifiedNode.arguments) { + modifiedNode.arguments = variables; + } + if ( + modifiedNode.type === "callexternal" && + modifiedNode.kind === "library" + ) { + modifiedNode.kind = "function"; + delete modifiedNode.data; + } + return { + byPointer: { + ...state.byPointer, + [pointer]: modifiedNode + } + }; + } + case actions.UNLOAD_TRANSACTION: + return DEFAULT_TX_LOG; + default: + return state; + } +} + +function currentNodePointer(state = "", action) { + switch (action.type) { + case actions.INTERNAL_CALL: + case actions.EXTERNAL_CALL: + case actions.CREATE: + case actions.INTERNAL_RETURN: + case actions.EXTERNAL_RETURN: + case actions.REVERT: + case actions.SELFDESTRUCT: + //note that instant calls/creates are not included! + return action.newPointer; + case actions.RESET: + return "/actions/0"; //reset to status after initial call + case actions.UNLOAD_TRANSACTION: + return ""; + default: + return state; + } +} + +//this is a stack of the pointers to external calls. +//note: not to the frames below them! +function pointerStack(state = [], action) { + switch (action.type) { + case actions.EXTERNAL_CALL: + case actions.CREATE: + //note that instant calls & creates are not included! + return [...state, action.newPointer]; + case actions.EXTERNAL_RETURN: + case actions.REVERT: + case actions.SELFDESTRUCT: + return state.slice(0, -1); + case actions.RESET: + return ["/actions/0"]; //reset to status after initial call + case actions.UNLOAD_TRANSACTION: + return []; + default: + return state; + } +} + +const proc = combineReducers({ + transactionLog, + currentNodePointer, + pointerStack +}); + +const reducer = combineReducers({ + proc +}); + +export default reducer; diff --git a/packages/debugger/lib/txlog/sagas/index.js b/packages/debugger/lib/txlog/sagas/index.js new file mode 100644 index 00000000000..9bd376af826 --- /dev/null +++ b/packages/debugger/lib/txlog/sagas/index.js @@ -0,0 +1,298 @@ +import debugModule from "debug"; +const debug = debugModule("debugger:txlog:sagas"); + +import { put, takeEvery, select } from "redux-saga/effects"; +import { prefixName } from "lib/helpers"; +import * as Codec from "@truffle/codec"; + +import * as actions from "../actions"; +import { TICK } from "lib/trace/actions"; +import * as trace from "lib/trace/sagas"; +import * as data from "lib/data/sagas"; + +import txlog from "../selectors"; + +function* tickSaga() { + yield* updateTransactionLogSaga(); + yield* trace.signalTickSagaCompletion(); +} + +function* updateTransactionLogSaga() { + const pointer = yield select(txlog.current.pointer); //log pointer, not AST pointer + if (yield select(txlog.current.isHalting)) { + //note that we process this case first so that it overrides the others! + const newPointer = yield select(txlog.current.externalReturnPointer); + const status = yield select(txlog.current.returnStatus); + if (status) { + if (yield select(txlog.current.isSelfDestruct)) { + const beneficiary = yield select(txlog.current.beneficiary); + //note: this selector returns null for a value-destroying selfdestruct + debug("sd: %o %o", pointer, newPointer); + yield put(actions.selfdestruct(pointer, newPointer, beneficiary)); + } else { + const decodings = yield* data.decodeReturnValue(); + debug("external return: %o %o", pointer, newPointer); + yield put(actions.externalReturn(pointer, newPointer, decodings)); + } + } else { + const error = (yield* data.decodeReturnValue())[0]; //NOTE: we will do this a better way in the future! + debug("revert: %o %o", pointer, newPointer); + yield put(actions.revert(pointer, newPointer, error)); + } + } else if (yield select(txlog.current.isJump)) { + const jumpDirection = yield select(txlog.current.jumpDirection); + if (jumpDirection === "i") { + const internal = yield select(txlog.next.inInternalSourceOrYul); //don't log jumps into internal sources or Yul + if (!internal) { + //we don't do any decoding/fn identification here because that's handled by + //the function identification case + if (!(yield select(txlog.current.waitingForInternalCallToAbsorb))) { + const newPointer = yield select(txlog.current.nextCallPointer); + debug("internal call: %o %o", pointer, newPointer); + yield put(actions.internalCall(pointer, newPointer)); + } else { + debug("absorbed call: %o", pointer); + yield put(actions.absorbedCall(pointer)); + } + } + } else if (jumpDirection === "o") { + const internal = yield select(txlog.current.inInternalSourceOrYul); //don't log jumps out of internal sources or Yul + if (!internal) { + //in this case, we have to do decoding & fn identification + const newPointer = yield select(txlog.current.internalReturnPointer); + const outputAllocations = yield select( + txlog.current.outputParameterAllocations + ); + if (outputAllocations) { + const compilationId = yield select(txlog.current.compilationId); + //can't do a yield* inside a map, have to do this loop manually + let variables = []; + for (let { name, definition, pointer } of outputAllocations) { + name = name ? name : undefined; //replace "" with undefined + const decodedValue = yield* data.decode( + definition, + pointer, + compilationId + ); + variables.push({ name, value: decodedValue }); + } + debug("internal return: %o %o", pointer, newPointer); + yield put(actions.internalReturn(pointer, newPointer, variables)); + } else { + debug("internal return: %o %o", pointer, newPointer); + yield put(actions.internalReturn(pointer, newPointer, undefined)); //I guess? + } + } + } + } else if (yield select(txlog.current.isCall)) { + const newPointer = yield select(txlog.current.nextCallPointer); + const address = yield select(txlog.current.callAddress); + const value = yield select(txlog.current.callValue); + //distinguishing DELEGATECALL vs CALLCODE seems unnecessary here + const isDelegate = yield select(txlog.current.isDelegateCallBroad); + //we need to determine what kind of call this is. + //we'll sort them into: function, constructor, message, library + //(library is a placeholder to be replaced later) + const context = yield select(txlog.current.callContext); + const calldata = yield select(txlog.current.callData); + const instant = yield select(txlog.current.isInstantCallOrCreate); + const kind = callKind(context, calldata, instant); + const absorb = yield select(txlog.current.absorbNextInternalCall); + const decoding = yield* data.decodeCall(); + if (instant) { + const status = yield select(txlog.current.returnStatus); + debug("instacall: %o %o", pointer, newPointer); + yield put( + actions.instantExternalCall( + pointer, + newPointer, //note: doesn't actually change the current pointer + address, + context, + value, + isDelegate, + kind, + decoding, + calldata, + absorb, + status + ) + ); + } else { + debug("external call: %o %o", pointer, newPointer); + yield put( + actions.externalCall( + pointer, + newPointer, + address, + context, + value, + isDelegate, + kind, + decoding, + calldata, + absorb + ) + ); + } + } else if (yield select(txlog.current.isCreate)) { + const newPointer = yield select(txlog.current.nextCallPointer); + const address = yield select(txlog.current.createdAddress); + const context = yield select(txlog.current.callContext); + const value = yield select(txlog.current.createValue); + const salt = yield select(txlog.current.salt); //is null for an ordinary create + const instant = yield select(txlog.current.isInstantCallOrCreate); + const binary = yield select(txlog.current.createBinary); + const decoding = yield* data.decodeCall(); + if (instant) { + const status = yield select(txlog.current.returnStatus); + debug("instacreate: %o %o", pointer, newPointer); + yield put( + actions.instantCreate( + pointer, + newPointer, //note: doesn't actually change the current pointer + address, + context, + value, + salt, + decoding, + binary, + status + ) + ); + } else { + debug("create: %o %o", pointer, newPointer); + yield put( + actions.create( + pointer, + newPointer, + address, + context, + value, + salt, + decoding, + binary + ) + ); + } + } + //we process this last in case jump & function def on same step + //(which is in fact how it typically goes!) + if (yield select(txlog.current.onFunctionDefinition)) { + if (yield select(txlog.current.waitingForFunctionDefinition)) { + debug("identifying"); + const inputAllocations = yield select( + txlog.current.inputParameterAllocations + ); + debug("inputAllocations: %O", inputAllocations); + if (inputAllocations) { + const functionNode = yield select(txlog.current.astNode); + const contractNode = yield select(txlog.current.contract); + const compilationId = yield select(txlog.current.compilationId); + //can't do a yield* inside a map, have to do this loop manually + let variables = []; + for (let { name, definition, pointer } of inputAllocations) { + const decodedValue = yield* data.decode( + definition, + pointer, + compilationId + ); + variables.push({ name, value: decodedValue }); + } + debug("identify: %o", pointer); + yield put( + actions.identifyFunctionCall( + pointer, + functionNode, + contractNode, + variables + ) + ); + } + } + } +} + +function callKind(context, calldata, instant) { + if (context) { + if (context.contractKind === "library") { + return instant ? "message" : "library"; + //for an instant return, just get it out of the way and set it to + //message rather than leaving it open (it'll get resolved in favor + //of message by our criteria) + } else { + const abi = context.abi; + const selector = calldata + .slice(0, 2 + 2 * Codec.Evm.Utils.SELECTOR_SIZE) + .padEnd("00", 2 + 2 * Codec.Evm.Utils.SELECTOR_SIZE); + debug("selector: %s", selector); + if (abi && selector in abi) { + return "function"; + } + } + } + return "message"; +} + +export function* reset() { + yield put(actions.reset()); +} + +export function* unload() { + yield put(actions.unloadTransaction()); +} + +export function* begin() { + const pointer = yield select(txlog.current.pointer); + const newPointer = yield select(txlog.current.nextCallPointer); + const origin = yield select(txlog.transaction.origin); + debug("origin: %o", pointer); + yield put(actions.recordOrigin(pointer, origin)); + const { + address, + binary, + storageAddress, + value, + data: calldata + } = yield select(txlog.current.call); + const context = yield select(txlog.current.context); + //note: there was an instant check here (based on checking if there are no + //trace steps) but I took it out, because even though having no trace steps + //is essentially an insta-call, the debugger doesn't treat it that way (it + //will see the return later), so we shouldn't here either + const decoding = yield* data.decodeCall(true); //pass flag to decode *current* call + if (address) { + const kind = callKind(context, calldata, false); //no insta-calls here! + const absorb = yield select(txlog.transaction.absorbFirstInternalCall); + debug("initial call: %o %o", pointer, newPointer); + yield put(actions.externalCall( + pointer, + newPointer, + address, + context, + value, + false, //initial call is never delegate + kind, + decoding, + calldata, + absorb + )); + } else { + debug("initial create: %o %o", pointer, newPointer); + yield put(actions.create( + pointer, + newPointer, + storageAddress, + context, + value, + null, //initial create never has salt + decoding, + binary + )); + } +} + +export function* saga() { + yield takeEvery(TICK, tickSaga); +} + +export default prefixName("txlog", saga); diff --git a/packages/debugger/lib/txlog/selectors/index.js b/packages/debugger/lib/txlog/selectors/index.js new file mode 100644 index 00000000000..0d29194eb69 --- /dev/null +++ b/packages/debugger/lib/txlog/selectors/index.js @@ -0,0 +1,411 @@ +import debugModule from "debug"; +const debug = debugModule("debugger:txlog:selectors"); + +import { createSelectorTree, createLeaf } from "reselect-tree"; + +import data from "lib/data/selectors"; +import evm from "lib/evm/selectors"; +import solidity from "lib/solidity/selectors"; + +import * as Codec from "@truffle/codec"; + +const identity = x => x; + +function createMultistepSelectors(stepSelector) { + return { + /** + * .source + * HACK: see notes in solidity selectors about cases + * where this won't work + */ + source: createLeaf([stepSelector.source], identity), + + /** + * .astNode + * HACK: see notes in solidity selectors about cases + * where this won't work + */ + astNode: createLeaf([stepSelector.node], identity), + + /** + * .inInternalSourceOrYul + */ + inInternalSourceOrYul: createLeaf( + ["./source", "./astNode"], + //note: the first of these won't actually happen atm, as source id + //of -1 would instead result in source.id === undefined, but I figure + //I'll include that condition in case I end up changing this later + (source, node) => + !node || source.internal || node.nodeType.startsWith("Yul") + ) + }; +} + +let txlog = createSelectorTree({ + /** + * txlog.state + */ + state: state => state.txlog, + + /** + * txlog.proc + */ + proc: { + /** + * txlog.proc.transactionLog + */ + transactionLog: createLeaf(["/state"], state => state.proc.transactionLog.byPointer), + }, + + /** + * txlog.transaction + */ + transaction: { + /** + * txlog.transaction.origin + */ + origin: createLeaf([evm.transaction.globals.tx], tx => tx.origin), + + /** + * txlog.transaction.absorbFirstInternalCall + */ + absorbFirstInternalCall: createLeaf( + [solidity.transaction.bottomStackframeRequiresPhantomFrame], + identity + ) + }, + + /** + * txlog.current + */ + current: { + ...createMultistepSelectors(solidity.current), + + /** + * txlog.current.state + */ + state: createLeaf([evm.current.state], identity), + + /** + * txlog.current.pointer + * NOTE: transaction log pointer; NOT the AST pointer! + */ + pointer: createLeaf(["/state"], state => state.proc.currentNodePointer), + + /** + * txlog.current.pointerStack + */ + pointerStack: createLeaf(["/state"], state => state.proc.pointerStack), + + /** + * txlog.current.node + * NOTE: transaction log node; NOT the AST node! + */ + node: createLeaf( + ["./pointer", "/proc/transactionLog"], + (pointer, log) => log[pointer] + ), + + /** + * txlog.current.waitingForFunctionDefinition + * This selector indicates whether there's a call (internal or external) + * that is waiting to have its function definition identified when we hit + * a function definition node. + */ + waitingForFunctionDefinition: createLeaf( + ["./node"], + node => + (node.type === "callinternal" || + node.type === "callexternal") && + node.waitingForFunctionDefinition + ), + + /** + * txlog.current.waitingForInternalCallToAbsorb + */ + waitingForInternalCallToAbsorb: createLeaf( + ["./node"], + node => + node.type === "callexternal" && + node.absorbNextInternalCall + ), + + /** + * txlog.current.nextCallPointer + * the pointer where a new call will be added + */ + nextCallPointer: createLeaf( + ["./pointer", "./node"], + (pointer, node) => `${pointer}/actions/${node.actions.length}` + ), + + /** + * txlog.current.internalReturnPointer + * the pointer where we'll end up after an internal return + * (if we're on an internal call, it returns; if we're not, + * we stay put and just absorb the info) + */ + internalReturnPointer: createLeaf( + ["./pointer", "./node"], + (pointer, node) => + node.type === "callinternal" + ? pointer.replace(/\/actions\/\d+$/, "") + : pointer + ), + + /** + * txlog.current.externalReturnPointer + * the pointer where we'll end up after an external return + * (take the top stack entry, then go up one more) + * (there should always be something on the stack when this + * selector is used) + */ + externalReturnPointer: createLeaf( + ["./pointerStack"], + stack => stack[stack.length - 1].replace(/\/actions\/\d+$/, "") + ), + + /** + * txlog.current.context + * Note we use data context, not evm context + * (i.e. decoder context, not debugger context) + */ + context: createLeaf([data.current.context], identity), + + /** + * txlog.current.call + */ + call: createLeaf([evm.current.call], identity), + + /** + * txlog.current.contract + */ + contract: createLeaf([data.current.contract], identity), + + /** + * txlog.current.isSourceRangeFinal + */ + isSourceRangeFinal: createLeaf( + [solidity.current.isSourceRangeFinal], + identity + ), + + /** + * txlog.current.onFunctionDefinition + */ + onFunctionDefinition: createLeaf( + ["./astNode", "./isSourceRangeFinal"], + (node, ready) => ready && node && node.nodeType === "FunctionDefinition" + ), + + /** + * txlog.current.compilationId + */ + compilationId: createLeaf([data.current.compilationId], identity), + + /** + * txlog.current.isJump + */ + isJump: createLeaf([evm.current.step.isJump], identity), + + /** + * txlog.current.jumpDirection + */ + jumpDirection: createLeaf([solidity.current.jumpDirection], identity), + + /** + * txlog.current.isCall + */ + isCall: createLeaf([evm.current.step.isCall], identity), + + /** + * txlog.current.isDelegateCallBroad + */ + isDelegateCallBroad: createLeaf( + [evm.current.step.isDelegateCallBroad], + identity + ), + + /** + * txlog.current.isCreate + */ + isCreate: createLeaf([evm.current.step.isCreate], identity), + + /** + * txlog.current.isInstantCallOrCreate + */ + isInstantCallOrCreate: createLeaf( + [evm.current.step.isInstantCallOrCreate], + identity + ), + + /** + * txlog.current.isHalting + */ + isHalting: createLeaf([evm.current.step.isHalting], identity), + + /** + * txlog.current.returnStatus + */ + returnStatus: createLeaf([evm.current.step.returnStatus], identity), + + /** + * txlog.current.callValue + */ + callValue: createLeaf([evm.current.step.callValue], identity), + + /** + * txlog.current.callAddress + */ + callAddress: createLeaf([evm.current.step.callAddress], identity), + + /** + * txlog.current.callContext + * note we make sure to use data, not evm, context! + * (i.e. decoder context, not debugger context) + */ + callContext: createLeaf([data.current.callContext], identity), + + /** + * txlog.current.absorbNextInternalCall + */ + absorbNextInternalCall: createLeaf( + [solidity.current.callRequiresPhantomFrame], + identity + ), + + /** + * txlog.current.callData + */ + callData: createLeaf([evm.current.step.callData], identity), + + /** + * txlog.current.createBinary + */ + createBinary: createLeaf([evm.current.step.createBinary], identity), + + /** + * txlog.current.createValue + */ + createValue: createLeaf([evm.current.step.createValue], identity), + + /** + * txlog.current.createdAddress + */ + createdAddress: createLeaf([evm.current.step.createdAddress], identity), + + /** + * txlog.current.salt + */ + salt: createLeaf([evm.current.step.salt], identity), + + /** + * txlog.current.isSelfDestruct + */ + isSelfDestruct: createLeaf([evm.current.step.isSelfDestruct], identity), + + /** + * txlog.current.beneficiary + */ + beneficiary: createLeaf([evm.current.step.beneficiary], identity), + + /** + * txlog.current.inputParameterAllocations + */ + inputParameterAllocations: createLeaf( + ["./astNode", "./state"], + (functionDefinition, { stack }) => { + if ( + !functionDefinition || + functionDefinition.nodeType !== "FunctionDefinition" + ) { + return null; + } + return locateParameters( + functionDefinition.parameters.parameters, + stack.length - 1 + ); + } + ), + + /** + * txlog.current.outputParameterAllocations + */ + outputParameterAllocations: createLeaf( + ["./astNode", "./state"], + (functionDefinition, { stack }) => { + if ( + !functionDefinition || + functionDefinition.nodeType !== "FunctionDefinition" + ) { + return null; + } + //when this selector is invoked, we're on the jump out step, so the + //top element of the stack is the return address; we need to skip past that + return locateParameters( + functionDefinition.returnParameters.parameters, + stack.length - 2 + ); + } + ) + }, + + /** + * txlog.next + */ + next: { + ...createMultistepSelectors(solidity.next) + }, + + /** + * txlog.views + */ + views: { + /** + * txlog.views.transactionLog + * contains the actual transformed transaction log ready for use! + */ + transactionLog: createLeaf( + ["/proc/transactionLog"], + log => { + const tie = node => + node.actions + ? { + ...node, + actions: node.actions.map( + pointer => tie(log[pointer]) + ) + } + : node; + return tie(log[""]); //"" is always the root node + } + ) + } + +}); + +function locateParameters(parameters, top) { + const reverseParameters = parameters.slice().reverse(); + //note we clone before reversing because reverse() is in place + + let results = []; + let currentPosition = top; + for (let parameter of reverseParameters) { + const words = Codec.Ast.Utils.stackSize(parameter); + const pointer = { + location: "stack", + from: currentPosition - words + 1, + to: currentPosition + }; + + results.unshift({ + name: parameter.name ? parameter.name : undefined, //replace "" with undefined + definition: parameter, + pointer + }); + currentPosition -= words; + } + return results; +} + +export default txlog; diff --git a/packages/debugger/test/txlog.js b/packages/debugger/test/txlog.js new file mode 100644 index 00000000000..394078ac30a --- /dev/null +++ b/packages/debugger/test/txlog.js @@ -0,0 +1,412 @@ +import debugModule from "debug"; +const debug = debugModule("test:txlog"); // eslint-disable-line no-unused-vars + +import {assert} from "chai"; + +import Ganache from "ganache-core"; + +import {prepareContracts} from "./helpers"; +import Debugger from "lib/debugger"; +import * as Codec from "@truffle/codec"; + +import txlog from "lib/txlog/selectors"; + +const __TXLOG = ` +//SPDX-License-Identifier: MIT +pragma solidity ^0.7.0; + +contract VizTest { + + event Dummy(); + + function testCall(uint x) public returns (uint y) { + return called(x); + } + + function called(uint x) public returns (uint y) { + emit Dummy(); + return x + 1; + } + + function testLibrary() public { + VizLibrary.loudIncrement(1); + } + + function testTransfer() public { + tx.origin.transfer(1); + } + + fallback() external { + called(msg.data.length); + } + + function testRevert() public { + callReverter(); + } + + function callReverter() public { + revert("Oops!"); + } + + constructor() payable { + } +} + +contract Secondary { + + event Dummy(); + + uint immutable x = another(); + uint immutable w; + + constructor(uint y) { + w = y; + } + + function another() public returns (uint z) { + emit Dummy(); + return 2; + } + + function secret() public returns (uint) { + return x + w; //just here so x & w are used + } +} + +library VizLibrary { + event Noise(); + + function loudIncrement(uint x) external returns (uint y) { + emit Noise(); + return x + 1; + } +} +`; + +let sources = { + "VizTest.sol": __TXLOG +}; + +const __MIGRATION = ` +let VizTest = artifacts.require("VizTest"); +let VizLibrary = artifacts.require("VizLibrary"); + +module.exports = function(deployer) { + deployer.deploy(VizLibrary); + deployer.link(VizLibrary, VizTest); + deployer.deploy(VizTest, { value: 100 }); +}; +`; + +let migrations = { + "2_deploy_contracts.js": __MIGRATION +}; + +function byName(variables) { + return Object.assign( + {}, + ...variables.map(variable => ({ + [variable.name]: variable.value + })) + ); +} + +describe("Transaction log (visualizer)", function () { + var provider; + + var abstractions; + var compilations; + + before("Create Provider", async function () { + provider = Ganache.provider({seed: "debugger", gasLimit: 7000000}); + }); + + before("Prepare contracts and artifacts", async function () { + this.timeout(30000); + + let prepared = await prepareContracts(provider, sources, migrations); + abstractions = prepared.abstractions; + compilations = prepared.compilations; + }); + + it("Correctly logs a simple call", async function () { + this.timeout(12000); + let instance = await abstractions.VizTest.deployed(); + let receipt = await instance.testCall(108); + let txHash = receipt.tx; + + let bugger = await Debugger.forTx(txHash, { + provider, + compilations + }); + + await bugger.continueUntilBreakpoint(); //run till end + + const root = bugger.view(txlog.views.transactionLog); + assert.equal(root.type, "transaction"); + assert.lengthOf(root.actions, 1); + let call = root.actions[0]; + assert.equal(call.type, "callexternal"); + assert.equal(call.kind, "function"); + assert.equal(call.address, instance.address); + assert.equal(call.functionName, "testCall"); + assert.equal(call.contractName, "VizTest"); + assert.equal(call.returnKind, "return"); + debug("arguments: %O", call.arguments); + let inputs = Codec.Format.Utils.Inspect.nativizeVariables( + byName(call.arguments) + ); + debug("nativized: %O", inputs); + assert.deepEqual(inputs, { + x: 108 + }); + let outputs = Codec.Format.Utils.Inspect.nativizeVariables( + byName(call.returnValues) + ); + assert.deepEqual(outputs, { + y: 109 + }); + assert.lengthOf(call.actions, 1); + call = call.actions[0]; + assert.equal(call.type, "callinternal"); + assert.equal(call.functionName, "called"); + assert.equal(call.contractName, "VizTest"); + assert.equal(call.returnKind, "return"); + inputs = Codec.Format.Utils.Inspect.nativizeVariables( + byName(call.arguments) + ); + assert.deepEqual(inputs, { + x: 108 + }); + outputs = Codec.Format.Utils.Inspect.nativizeVariables( + byName(call.returnValues) + ); + assert.deepEqual(outputs, { + y: 109 + }); + }); + + it("Correctly logs a creation", async function () { + this.timeout(12000); + let instance = await abstractions.Secondary.new(108); + let txHash = instance.transactionHash; + + let bugger = await Debugger.forTx(txHash, { + provider, + compilations + }); + + await bugger.continueUntilBreakpoint(); //run till end + + const root = bugger.view(txlog.views.transactionLog); + assert.equal(root.type, "transaction"); + assert.lengthOf(root.actions, 1); + let call = root.actions[0]; + assert.equal(call.type, "callexternal"); + assert.equal(call.kind, "constructor"); + assert.equal(call.address, instance.address); + assert.isUndefined(call.functionName); + assert.equal(call.contractName, "Secondary"); + assert.equal(call.returnKind, "return"); + let inputs = Codec.Format.Utils.Inspect.nativizeVariables( + byName(call.arguments) + ); + assert.deepEqual(inputs, { + y: 108 + }); + debug("immuts: %O", call.returnImmutables); + let outputs = Codec.Format.Utils.Inspect.nativizeVariables( + byName(call.returnImmutables) + ); + assert.deepEqual(outputs, { + x: 2, + w: 108 + }); + assert.lengthOf(call.actions, 1); + call = call.actions[0]; + assert.equal(call.type, "callinternal"); + assert.equal(call.functionName, "another"); + assert.equal(call.contractName, "Secondary"); + assert.equal(call.returnKind, "return"); + assert.lengthOf(call.arguments, 0); + outputs = Codec.Format.Utils.Inspect.nativizeVariables( + byName(call.returnValues) + ); + assert.include(outputs, { + z: 2 + }); + }); + + it("Correctly logs a library call", async function () { + this.timeout(12000); + let instance = await abstractions.VizTest.deployed(); + let library = await abstractions.VizLibrary.deployed(); + let receipt = await instance.testLibrary(); + let txHash = receipt.tx; + + let bugger = await Debugger.forTx(txHash, { + provider, + compilations + }); + + await bugger.continueUntilBreakpoint(); //run till end + + const root = bugger.view(txlog.views.transactionLog); + assert.equal(root.type, "transaction"); + assert.lengthOf(root.actions, 1); + let call = root.actions[0]; + assert.equal(call.type, "callexternal"); + assert.equal(call.kind, "function"); + assert.equal(call.address, instance.address); + assert.equal(call.functionName, "testLibrary"); + assert.equal(call.contractName, "VizTest"); + assert.equal(call.returnKind, "return"); + assert.lengthOf(call.arguments, 0); + assert.lengthOf(call.returnValues, 0); + assert.lengthOf(call.actions, 1); + call = call.actions[0]; + assert.equal(call.type, "callexternal"); + assert.equal(call.kind, "function"); + assert.equal(call.address, library.address); + assert.isTrue(call.isDelegate); + assert.equal(call.functionName, "loudIncrement"); + assert.equal(call.contractName, "VizLibrary"); + assert.equal(call.returnKind, "return"); + let inputs = Codec.Format.Utils.Inspect.nativizeVariables( + byName(call.arguments) + ); + assert.deepEqual(inputs, { + x: 1 + }); + let outputs = Codec.Format.Utils.Inspect.nativizeVariables( + byName(call.returnValues) + ); + assert.deepEqual(outputs, { + y: 2 + }); + }); + + it("Correctly logs an ether transfer", async function () { + this.timeout(12000); + let instance = await abstractions.VizTest.deployed(); + let receipt = await instance.testTransfer(); + let txHash = receipt.tx; + + let bugger = await Debugger.forTx(txHash, { + provider, + compilations + }); + + await bugger.continueUntilBreakpoint(); //run till end + + const root = bugger.view(txlog.views.transactionLog); + assert.equal(root.type, "transaction"); + let origin = root.origin; + assert.lengthOf(root.actions, 1); + let call = root.actions[0]; + assert.equal(call.type, "callexternal"); + assert.equal(call.kind, "function"); + assert.equal(call.address, instance.address); + assert.equal(call.functionName, "testTransfer"); + assert.equal(call.contractName, "VizTest"); + assert.equal(call.returnKind, "return"); + assert.lengthOf(call.arguments, 0); + assert.lengthOf(call.returnValues, 0); + assert.lengthOf(call.actions, 1); + call = call.actions[0]; + assert.equal(call.type, "callexternal"); + assert.equal(call.kind, "message"); + assert.equal(call.address, origin); + assert.equal(call.value.toNumber(), 1); + assert.equal(call.returnKind, "return"); + }); + + it("Correctly logs a fallback call", async function () { + this.timeout(12000); + let instance = await abstractions.VizTest.deployed(); + let receipt = await instance.sendTransaction({data: "0xdeadbeef"}); + let txHash = receipt.tx; + + let bugger = await Debugger.forTx(txHash, { + provider, + compilations + }); + + await bugger.continueUntilBreakpoint(); //run till end + + const root = bugger.view(txlog.views.transactionLog); + assert.equal(root.type, "transaction"); + assert.lengthOf(root.actions, 1); + let call = root.actions[0]; + assert.equal(call.type, "callexternal"); + assert.equal(call.kind, "message"); + assert.equal(call.address, instance.address); + assert.equal(call.contractName, "VizTest"); + assert.equal(call.data, "0xdeadbeef"); + assert.equal(call.returnKind, "return"); + assert.lengthOf(call.actions, 1); + call = call.actions[0]; + assert.equal(call.type, "callinternal"); + assert.equal(call.functionName, "called"); + assert.equal(call.contractName, "VizTest"); + assert.equal(call.returnKind, "return"); + let inputs = Codec.Format.Utils.Inspect.nativizeVariables( + byName(call.arguments) + ); + assert.deepEqual(inputs, { + x: 4 + }); + let outputs = Codec.Format.Utils.Inspect.nativizeVariables( + byName(call.returnValues) + ); + assert.deepEqual(outputs, { + y: 5 + }); + }); + + it("Correctly logs a revert", async function () { + this.timeout(12000); + let instance = await abstractions.VizTest.deployed(); + //HACK: because this transaction fails, we have to extract the hash from + //the resulting exception (there is supposed to be a non-hacky way but it + //does not presently work) + let txHash; + try { + await instance.testRevert(); //this will throw because of the revert + } catch (error) { + txHash = error.hashes[0]; //it's the only hash involved + } + + let bugger = await Debugger.forTx(txHash, { + provider, + compilations + }); + + await bugger.continueUntilBreakpoint(); //run till end + + const root = bugger.view(txlog.views.transactionLog); + assert.equal(root.type, "transaction"); + assert.lengthOf(root.actions, 1); + let call = root.actions[0]; + assert.equal(call.type, "callexternal"); + assert.equal(call.kind, "function"); + assert.equal(call.address, instance.address); + assert.equal(call.functionName, "testRevert"); + assert.equal(call.contractName, "VizTest"); + assert.equal(call.returnKind, "unwind"); + assert.lengthOf(call.arguments, 0); + assert.lengthOf(call.actions, 1); + call = call.actions[0]; + assert.equal(call.type, "callinternal"); + assert.equal(call.functionName, "callReverter"); + assert.equal(call.contractName, "VizTest"); + assert.equal(call.returnKind, "revert"); + assert.lengthOf(call.arguments, 0); + assert.equal(call.error.kind, "revert"); + assert.lengthOf(call.error.arguments, 1); + assert.equal( + Codec.Format.Utils.Inspect.nativize(call.error.arguments[0].value), + "Oops!" + ); + }); +});