diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dd1af74607..33e9601900 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -374,24 +374,35 @@ jobs: with: name: moonbeam path: build + - name: Use pnpm + uses: pnpm/action-setup@v2 + with: + version: 8.6.12 - name: Use Node.js 18.x uses: actions/setup-node@v3 with: node-version: 20.x + cache: "pnpm" + cache-dependency-path: test/pnpm-lock.yaml + - run: | + mkdir -p target/release + - name: "Download branch built node" + uses: actions/download-artifact@v3.0.2 + with: + name: moonbeam + path: target/release - name: Get tracing runtimes run: | ./scripts/build-last-tracing-runtime.sh ${{ needs.set-tags.outputs.git_branch }} - mkdir -p archived_tests/moonbase-overrides/ - mv build/wasm/moonbase-runtime-local-substitute-tracing.wasm archived_tests/moonbase-overrides/ + mkdir -p test/moonbase-overrides/ + mv build/wasm/moonbase-runtime-local-substitute-tracing.wasm test/moonbase-overrides/ - name: Typescript tracing tests (against dev service) env: - BINARY_PATH: ../build/moonbeam - ETHAPI_CMD: --ethapi=txpool,debug,trace - FORCE_WASM_EXECUTION: true - FORCE_COMPILED_WASM: true - WASM_RUNTIME_OVERRIDES: moonbase-overrides + DEBUG_COLOURS: "1" + NODE_OPTIONS: "--max-old-space-size=12288" run: | chmod uog+x build/moonbeam + chmod uog+x target/release/moonbeam #### Preparing the repository cd moonbeam-types-bundle @@ -402,17 +413,11 @@ jobs: cd ../typescript-api npm ci - cd ../archived_tests - npm ci - #### Prepares and copies the typescript generated API to include in the tests - npm run setup-typescript-api - - #### Compile typescript tests into javascript (more stable for Mocha) - #### This also better display typescript issues - npm run build - - #### Run tracing tests with mocha - node_modules/.bin/mocha --exit --parallel -j 2 'build/tracing-tests/**/test-*.js' + #### Install and run dev tracing + cd ../test + pnpm install + pnpm compile-solidity + pnpm moonwall test dev_moonbase_tracing docker-moonbeam: runs-on: ubuntu-latest diff --git a/scripts/run-tracing-tests.sh b/scripts/run-tracing-tests.sh index ac168335ca..15b3475444 100755 --- a/scripts/run-tracing-tests.sh +++ b/scripts/run-tracing-tests.sh @@ -4,7 +4,7 @@ echo 'Make sure you have built moonbeam-types-bundle and run "npm install" in th BUILD_LAST_TRACING_RUNTIME="no" -if [ -e archived_tests/moonbase-overrides/moonbase-runtime-local-substitute-tracing.wasm ]; then +if [ -e test/moonbase-overrides/moonbase-runtime-local-substitute-tracing.wasm ]; then if [[ "$1" == "-f" ]]; then BUILD_LAST_TRACING_RUNTIME="yes" fi @@ -14,8 +14,8 @@ fi if [[ "$BUILD_LAST_TRACING_RUNTIME" == "yes" ]]; then ./scripts/build-last-tracing-runtime.sh - mkdir -p archived_tests/moonbase-overrides/ - mv build/wasm/moonbase-runtime-local-substitute-tracing.wasm archived_tests/moonbase-overrides/ + mkdir -p test/moonbase-overrides/ + mv build/wasm/moonbase-runtime-local-substitute-tracing.wasm test/moonbase-overrides/ else echo "The tracing runtime is not rebuilt, if you want to rebuild it, use the option '-f'." fi @@ -24,13 +24,13 @@ echo "Preparing tests dependencies…" cd moonbeam-types-bundle npm ci npm run build + cd ../typescript-api npm ci echo "Run tracing tests…" -cd ../archived_tests -npm ci -npm run setup-typescript-api -npm run build -ETHAPI_CMD="--ethapi=txpool,debug,trace" FORCE_WASM_EXECUTION="true" WASM_RUNTIME_OVERRIDES="moonbase-overrides" node_modules/.bin/mocha --parallel -j 2 -r ts-node/register 'build/tracing-tests/**/test-*.js' --timeout 30000 +cd ../test +pnpm install +pnpm compile-solidity +pnpm moonwall test dev_moonbase_tracing cd .. diff --git a/test/helpers/tracer/blockscout_tracer.min.json b/test/helpers/tracer/blockscout_tracer.min.json new file mode 100644 index 0000000000..e81fe875de --- /dev/null +++ b/test/helpers/tracer/blockscout_tracer.min.json @@ -0,0 +1,3 @@ +{ + "body": "// tracer allows Geth's `debug_traceTransaction` to mimic the output of Parity's `trace_replayTransaction`\n{\n // The call stack of the EVM execution.\n callStack: [{}],\n\n // step is invoked for every opcode that the VM executes.\n step(log, db) {\n // Capture any errors immediately\n const error = log.getError();\n\n if (error !== undefined) {\n this.fault(log, db);\n } else {\n this.success(log, db);\n }\n },\n\n // fault is invoked when the actual execution of an opcode fails.\n fault(log, db) {\n // If the topmost call already reverted, don't handle the additional fault again\n if (this.topCall().error === undefined) {\n this.putError(log);\n }\n },\n\n putError(log) {\n if (this.callStack.length > 1) {\n this.putErrorInTopCall(log);\n } else {\n this.putErrorInBottomCall(log);\n }\n },\n\n putErrorInTopCall(log) {\n // Pop off the just failed call\n const call = this.callStack.pop();\n this.putErrorInCall(log, call);\n this.pushChildCall(call);\n },\n\n putErrorInBottomCall(log) {\n const call = this.bottomCall();\n this.putErrorInCall(log, call);\n },\n\n putErrorInCall(log, call) {\n call.error = log.getError();\n\n // Consume all available gas and clean any leftovers\n if (call.gasBigInt !== undefined) {\n call.gasUsedBigInt = call.gasBigInt;\n }\n\n delete call.outputOffset;\n delete call.outputLength;\n },\n\n topCall() {\n return this.callStack[this.callStack.length - 1];\n },\n\n bottomCall() {\n return this.callStack[0];\n },\n\n pushChildCall(childCall) {\n const topCall = this.topCall();\n\n if (topCall.calls === undefined) {\n topCall.calls = [];\n }\n\n topCall.calls.push(childCall);\n },\n\n pushGasToTopCall(log) {\n const topCall = this.topCall();\n\n if (topCall.gasBigInt === undefined) {\n topCall.gasBigInt = log.getGas();\n }\n topCall.gasUsedBigInt = topCall.gasBigInt - log.getGas() - log.getCost();\n },\n\n success(log, db) {\n const op = log.op.toString();\n\n this.beforeOp(log, db);\n\n switch (op) {\n case 'CREATE':\n this.createOp(log);\n break;\n case 'CREATE2':\n this.create2Op(log);\n break;\n case 'SELFDESTRUCT':\n this.selfDestructOp(log, db);\n break;\n case 'CALL':\n case 'CALLCODE':\n case 'DELEGATECALL':\n case 'STATICCALL':\n this.callOp(log, op);\n break;\n case 'REVERT':\n this.revertOp();\n break;\n }\n },\n\n beforeOp(log, db) {\n /**\n * Depths\n * 0 - `ctx`. Never shows up in `log.getDepth()`\n * 1 - first level of `log.getDepth()`\n *\n * callStack indexes\n *\n * 0 - pseudo-call stand-in for `ctx` in initializer (`callStack: [{}]`)\n * 1 - first callOp inside of `ctx`\n */\n const logDepth = log.getDepth();\n const callStackDepth = this.callStack.length;\n\n if (logDepth < callStackDepth) {\n // Pop off the last call and get the execution results\n const call = this.callStack.pop();\n\n const ret = log.stack.peek(0);\n\n if (!ret.equals(0)) {\n if (call.type === 'create' || call.type === 'create2') {\n call.createdContractAddressHash = toHex(toAddress(ret.toString(16)));\n call.createdContractCode = toHex(db.getCode(toAddress(ret.toString(16))));\n } else {\n call.output = toHex(log.memory.slice(call.outputOffset, call.outputOffset + call.outputLength));\n }\n } else if (call.error === undefined) {\n call.error = 'internal failure';\n }\n\n delete call.outputOffset;\n delete call.outputLength;\n\n this.pushChildCall(call);\n }\n else {\n this.pushGasToTopCall(log);\n }\n },\n\n createOp(log) {\n const inputOffset = log.stack.peek(1).valueOf();\n const inputLength = log.stack.peek(2).valueOf();\n const inputEnd = inputOffset + inputLength;\n const stackValue = log.stack.peek(0);\n\n const call = {\n type: 'create',\n from: toHex(log.contract.getAddress()),\n init: toHex(log.memory.slice(inputOffset, inputEnd)),\n valueBigInt: bigInt(stackValue.toString(10))\n };\n this.callStack.push(call);\n },\n\n create2Op(log) {\n const inputOffset = log.stack.peek(1).valueOf();\n const inputLength = log.stack.peek(2).valueOf();\n const inputEnd = inputOffset + inputLength;\n const stackValue = log.stack.peek(0);\n\n const call = {\n type: 'create2',\n from: toHex(log.contract.getAddress()),\n init: toHex(log.memory.slice(inputOffset, inputEnd)),\n valueBigInt: bigInt(stackValue.toString(10))\n };\n this.callStack.push(call);\n },\n\n selfDestructOp(log, db) {\n const contractAddress = log.contract.getAddress();\n\n this.pushChildCall({\n type: 'selfdestruct',\n from: toHex(contractAddress),\n to: toHex(toAddress(log.stack.peek(0).toString(16))),\n gasBigInt: log.getGas(),\n gasUsedBigInt: log.getCost(),\n valueBigInt: db.getBalance(contractAddress)\n });\n },\n\n callOp(log, op) {\n const to = toAddress(log.stack.peek(1).toString(16));\n\n // Skip any pre-compile invocations, those are just fancy opcodes\n if (!isPrecompiled(to)) {\n this.callCustomOp(log, op, to);\n }\n },\n\n callCustomOp(log, op, to) {\n const stackOffset = (op === 'DELEGATECALL' || op === 'STATICCALL' ? 0 : 1);\n\n const inputOffset = log.stack.peek(2 + stackOffset).valueOf();\n const inputLength = log.stack.peek(3 + stackOffset).valueOf();\n const inputEnd = inputOffset + inputLength;\n\n const call = {\n type: 'call',\n callType: op.toLowerCase(),\n from: toHex(log.contract.getAddress()),\n to: toHex(to),\n input: toHex(log.memory.slice(inputOffset, inputEnd)),\n outputOffset: log.stack.peek(4 + stackOffset).valueOf(),\n outputLength: log.stack.peek(5 + stackOffset).valueOf()\n };\n\n switch (op) {\n case 'CALL':\n case 'CALLCODE':\n call.valueBigInt = bigInt(log.stack.peek(2));\n break;\n case 'DELEGATECALL':\n // value inherited from scope during call sequencing\n break;\n case 'STATICCALL':\n // by definition static calls transfer no value\n call.valueBigInt = bigInt.zero;\n break;\n default:\n throw \"Unknown custom call op \" + op;\n }\n\n this.callStack.push(call);\n },\n\n revertOp() {\n this.topCall().error = 'execution reverted';\n },\n\n // result is invoked when all the opcodes have been iterated over and returns\n // the final result of the tracing.\n result(ctx, db) {\n const result = this.ctxToResult(ctx, db);\n const filtered = this.filterNotUndefined(result);\n const callSequence = this.sequence(filtered, [], filtered.valueBigInt, []).callSequence;\n return this.encodeCallSequence(callSequence);\n },\n\n ctxToResult(ctx, db) {\n var result;\n\n switch (ctx.type) {\n case 'CALL':\n result = this.ctxToCall(ctx);\n break;\n case 'CREATE':\n result = this.ctxToCreate(ctx, db);\n break;\n case 'CREATE2':\n result = this.ctxToCreate2(ctx, db);\n break;\n }\n\n return result;\n },\n\n ctxToCall(ctx) {\n const result = {\n type: 'call',\n callType: 'call',\n from: toHex(ctx.from),\n to: toHex(ctx.to),\n valueBigInt: bigInt(ctx.value.toString(10)),\n gasBigInt: bigInt(ctx.gas),\n gasUsedBigInt: bigInt(ctx.gasUsed),\n input: toHex(ctx.input)\n };\n\n this.putBottomChildCalls(result);\n this.putErrorOrOutput(result, ctx);\n\n return result;\n },\n\n putErrorOrOutput(result, ctx) {\n const error = this.error(ctx);\n\n if (error !== undefined) {\n result.error = error;\n } else {\n result.output = toHex(ctx.output);\n }\n },\n\n ctxToCreate(ctx, db) {\n const result = {\n type: 'create',\n from: toHex(ctx.from),\n init: toHex(ctx.input),\n valueBigInt: bigInt(ctx.value.toString(10)),\n gasBigInt: bigInt(ctx.gas),\n gasUsedBigInt: bigInt(ctx.gasUsed)\n };\n\n this.putBottomChildCalls(result);\n this.putErrorOrCreatedContract(result, ctx, db);\n\n return result;\n },\n\n ctxToCreate2(ctx, db) {\n const result = {\n type: 'create2',\n from: toHex(ctx.from),\n init: toHex(ctx.input),\n valueBigInt: bigInt(ctx.value.toString(10)),\n gasBigInt: bigInt(ctx.gas),\n gasUsedBigInt: bigInt(ctx.gasUsed)\n };\n\n this.putBottomChildCalls(result);\n this.putErrorOrCreatedContract(result, ctx, db);\n\n return result;\n },\n\n putBottomChildCalls(result) {\n const bottomCall = this.bottomCall();\n const bottomChildCalls = bottomCall.calls;\n\n if (bottomChildCalls !== undefined) {\n result.calls = bottomChildCalls;\n }\n },\n\n putErrorOrCreatedContract(result, ctx, db) {\n const error = this.error(ctx);\n\n if (error !== undefined) {\n result.error = error\n } else {\n result.createdContractAddressHash = toHex(ctx.to);\n result.createdContractCode = toHex(db.getCode(ctx.to));\n }\n },\n\n error(ctx) {\n var error;\n\n const bottomCall = this.bottomCall();\n const bottomCallError = bottomCall.error;\n\n if (bottomCallError !== undefined) {\n error = bottomCallError;\n } else {\n const ctxError = ctx.error;\n\n if (ctxError !== undefined) {\n error = ctxError;\n }\n }\n\n return error;\n },\n\n filterNotUndefined(call) {\n for (var key in call) {\n if (call[key] === undefined) {\n delete call[key];\n }\n }\n\n if (call.calls !== undefined) {\n for (var i = 0; i < call.calls.length; i++) {\n call.calls[i] = this.filterNotUndefined(call.calls[i]);\n }\n }\n\n return call;\n },\n\n // sequence converts the finalized calls from a call tree to a call sequence\n sequence(call, callSequence, availableValueBigInt, traceAddress) {\n const subcalls = call.calls;\n delete call.calls;\n\n call.traceAddress = traceAddress;\n\n if (call.type === 'call' && call.callType === 'delegatecall') {\n call.valueBigInt = availableValueBigInt;\n }\n\n var newCallSequence = callSequence.concat([call]);\n\n if (subcalls !== undefined) {\n for (var i = 0; i < subcalls.length; i++) {\n const nestedSequenced = this.sequence(\n subcalls[i],\n newCallSequence,\n call.valueBigInt,\n traceAddress.concat([i])\n );\n newCallSequence = nestedSequenced.callSequence;\n }\n }\n\n return {\n callSequence: newCallSequence\n };\n },\n\n encodeCallSequence(calls) {\n for (var i = 0; i < calls.length; i++) {\n this.encodeCall(calls[i]);\n }\n\n return calls;\n },\n\n encodeCall(call) {\n this.putValue(call);\n this.putGas(call);\n this.putGasUsed(call);\n\n return call;\n },\n\n putValue(call) {\n const valueBigInt = call.valueBigInt;\n delete call.valueBigInt;\n\n call.value = '0x' + valueBigInt.toString(16);\n },\n\n putGas(call) {\n const gasBigInt = call.gasBigInt;\n delete call.gasBigInt;\n\n if (gasBigInt === undefined) {\n gasBigInt = bigInt.zero;\n }\n\n call.gas = '0x' + gasBigInt.toString(16);\n },\n\n putGasUsed(call) {\n const gasUsedBigInt = call.gasUsedBigInt;\n delete call.gasUsedBigInt;\n\n if (gasUsedBigInt === undefined) {\n gasUsedBigInt = bigInt.zero;\n }\n\n call.gasUsed = '0x' + gasUsedBigInt.toString(16);\n }\n}\n" +} diff --git a/test/helpers/tracer/blockscout_tracer_v2.min.json b/test/helpers/tracer/blockscout_tracer_v2.min.json new file mode 100644 index 0000000000..41e5e3b9ab --- /dev/null +++ b/test/helpers/tracer/blockscout_tracer_v2.min.json @@ -0,0 +1,3 @@ +{ + "body": "// tracer allows Geth's `debug_traceTransaction` to mimic the output of Parity's `trace_replayTransaction`\n{\n // The call stack of the EVM execution.\n callStack: [{}],\n\n // step is invoked for every opcode that the VM executes.\n step(log, db) {\n // Capture any errors immediately\n const error = log.getError();\n\n if (error !== undefined) {\n this.fault(log, db);\n } else {\n this.success(log, db);\n }\n },\n\n // fault is invoked when the actual execution of an opcode fails.\n fault(log, db) {\n // If the topmost call already reverted, don't handle the additional fault again\n if (this.topCall().error === undefined) {\n this.putError(log);\n }\n },\n\n putError(log) {\n if (this.callStack.length > 1) {\n this.putErrorInTopCall(log);\n } else {\n this.putErrorInBottomCall(log);\n }\n },\n\n putErrorInTopCall(log) {\n // Pop off the just failed call\n const call = this.callStack.pop();\n this.putErrorInCall(log, call);\n this.pushChildCall(call);\n },\n\n putErrorInBottomCall(log) {\n const call = this.bottomCall();\n this.putErrorInCall(log, call);\n },\n\n putErrorInCall(log, call) {\n call.error = log.getError();\n\n // Consume all available gas and clean any leftovers\n if (call.gasBigInt !== undefined) {\n call.gasUsedBigInt = call.gasBigInt;\n }\n\n delete call.outputOffset;\n delete call.outputLength;\n },\n\n topCall() {\n return this.callStack[this.callStack.length - 1];\n },\n\n bottomCall() {\n return this.callStack[0];\n },\n\n pushChildCall(childCall) {\n const topCall = this.topCall();\n\n if (topCall.calls === undefined) {\n topCall.calls = [];\n }\n\n topCall.calls.push(childCall);\n },\n\n pushGasToTopCall(log) {\n const topCall = this.topCall();\n\n if (topCall.gasBigInt === undefined) {\n topCall.gasBigInt = log.getGas();\n }\n topCall.gasUsedBigInt = topCall.gasBigInt - log.getGas() - log.getCost();\n },\n\n success(log, db) {\n const op = log.op.toString();\n\n this.beforeOp(log, db);\n\n switch (op) {\n case 'CREATE':\n this.createOp(log);\n break;\n case 'CREATE2':\n this.create2Op(log);\n break;\n case 'SELFDESTRUCT':\n this.selfDestructOp(log, db);\n break;\n case 'CALL':\n case 'CALLCODE':\n case 'DELEGATECALL':\n case 'STATICCALL':\n this.callOp(log, op);\n break;\n case 'REVERT':\n this.revertOp();\n break;\n }\n },\n\n beforeOp(log, db) {\n /**\n * Depths\n * 0 - `ctx`. Never shows up in `log.getDepth()`\n * 1 - first level of `log.getDepth()`\n *\n * callStack indexes\n *\n * 0 - pseudo-call stand-in for `ctx` in initializer (`callStack: [{}]`)\n * 1 - first callOp inside of `ctx`\n */\n const logDepth = log.getDepth();\n const callStackDepth = this.callStack.length;\n\n if (logDepth < callStackDepth) {\n // Pop off the last call and get the execution results\n const call = this.callStack.pop();\n\n const ret = log.stack.peek(0);\n\n if (!ret.equals(0)) {\n if (call.type === 'create' || call.type === 'create2') {\n call.createdContractAddressHash = toHex(toAddress(ret.toString(16)));\n call.createdContractCode = toHex(db.getCode(toAddress(ret.toString(16))));\n } else {\n call.output = toHex(log.memory.slice(call.outputOffset, call.outputOffset + call.outputLength));\n }\n } else if (call.error === undefined) {\n call.error = 'internal failure';\n }\n\n delete call.outputOffset;\n delete call.outputLength;\n\n this.pushChildCall(call);\n }\n else {\n this.pushGasToTopCall(log);\n }\n },\n\n createOp(log) {\n const inputOffset = log.stack.peek(1).valueOf();\n const inputLength = log.stack.peek(2).valueOf();\n const inputEnd = inputOffset + inputLength;\n const stackValue = log.stack.peek(0);\n\n const call = {\n type: 'create',\n from: toHex(log.contract.getAddress()),\n init: toHex(log.memory.slice(inputOffset, inputEnd)),\n valueBigInt: bigInt(stackValue.toString(10))\n };\n this.callStack.push(call);\n },\n\n create2Op(log) {\n const inputOffset = log.stack.peek(1).valueOf();\n const inputLength = log.stack.peek(2).valueOf();\n const inputEnd = inputOffset + inputLength;\n const stackValue = log.stack.peek(0);\n\n const call = {\n type: 'create2',\n from: toHex(log.contract.getAddress()),\n init: toHex(log.memory.slice(inputOffset, inputEnd)),\n valueBigInt: bigInt(stackValue.toString(10))\n };\n this.callStack.push(call);\n },\n\n selfDestructOp(log, db) {\n const contractAddress = log.contract.getAddress();\n\n this.pushChildCall({\n type: 'selfdestruct',\n from: toHex(contractAddress),\n to: toHex(toAddress(log.stack.peek(0).toString(16))),\n gasBigInt: log.getGas(),\n gasUsedBigInt: log.getCost(),\n valueBigInt: db.getBalance(contractAddress)\n });\n },\n\n callOp(log, op) {\n const to = toAddress(log.stack.peek(1).toString(16));\n\n // Skip any pre-compile invocations, those are just fancy opcodes\n if (!isPrecompiled(to)) {\n this.callCustomOp(log, op, to);\n }\n },\n\n callCustomOp(log, op, to) {\n const stackOffset = (op === 'DELEGATECALL' || op === 'STATICCALL' ? 0 : 1);\n\n const inputOffset = log.stack.peek(2 + stackOffset).valueOf();\n const inputLength = log.stack.peek(3 + stackOffset).valueOf();\n const inputEnd = inputOffset + inputLength;\n\n const call = {\n type: 'call',\n callType: op.toLowerCase(),\n from: toHex(log.contract.getAddress()),\n to: toHex(to),\n input: toHex(log.memory.slice(inputOffset, inputEnd)),\n outputOffset: log.stack.peek(4 + stackOffset).valueOf(),\n outputLength: log.stack.peek(5 + stackOffset).valueOf()\n };\n\n switch (op) {\n case 'CALL':\n case 'CALLCODE':\n call.valueBigInt = bigInt(log.stack.peek(2));\n break;\n case 'DELEGATECALL':\n // value inherited from scope during call sequencing\n break;\n case 'STATICCALL':\n // by definition static calls transfer no value\n call.valueBigInt = bigInt.zero;\n break;\n default:\n throw 'Unknown custom call op ' + op;\n }\n\n this.callStack.push(call);\n },\n\n revertOp() {\n this.topCall().error = 'execution reverted';\n },\n\n // result is invoked when all the opcodes have been iterated over and returns\n // the final result of the tracing.\n result(ctx, db) {\n const result = this.ctxToResult(ctx, db);\n const filtered = this.filterNotUndefined(result);\n const callSequence = this.sequence(filtered, [], filtered.valueBigInt, []).callSequence;\n return this.encodeCallSequence(callSequence);\n },\n\n ctxToResult(ctx, db) {\n var result;\n\n switch (ctx.type) {\n case 'CALL':\n result = this.ctxToCall(ctx);\n break;\n case 'CREATE':\n result = this.ctxToCreate(ctx, db);\n break;\n case 'CREATE2':\n result = this.ctxToCreate2(ctx, db);\n break;\n }\n\n return result;\n },\n\n ctxToCall(ctx) {\n const result = {\n type: 'call',\n callType: 'call',\n from: toHex(ctx.from),\n to: toHex(ctx.to),\n valueBigInt: bigInt(ctx.value.toString(10)),\n gasBigInt: bigInt(ctx.gas),\n gasUsedBigInt: bigInt(ctx.gasUsed),\n input: toHex(ctx.input)\n };\n\n this.putBottomChildCalls(result);\n this.putErrorOrOutput(result, ctx);\n\n return result;\n },\n\n putErrorOrOutput(result, ctx) {\n const error = this.error(ctx);\n\n if (error !== undefined) {\n result.error = error;\n } else {\n result.output = toHex(ctx.output);\n }\n },\n\n ctxToCreate(ctx, db) {\n const result = {\n type: 'create',\n from: toHex(ctx.from),\n init: toHex(ctx.input),\n valueBigInt: bigInt(ctx.value.toString(10)),\n gasBigInt: bigInt(ctx.gas),\n gasUsedBigInt: bigInt(ctx.gasUsed)\n };\n\n this.putBottomChildCalls(result);\n this.putErrorOrCreatedContract(result, ctx, db);\n\n return result;\n },\n\n ctxToCreate2(ctx, db) {\n const result = {\n type: 'create2',\n from: toHex(ctx.from),\n init: toHex(ctx.input),\n valueBigInt: bigInt(ctx.value.toString(10)),\n gasBigInt: bigInt(ctx.gas),\n gasUsedBigInt: bigInt(ctx.gasUsed)\n };\n\n this.putBottomChildCalls(result);\n this.putErrorOrCreatedContract(result, ctx, db);\n\n return result;\n },\n\n putBottomChildCalls(result) {\n const bottomCall = this.bottomCall();\n const bottomChildCalls = bottomCall.calls;\n\n if (bottomChildCalls !== undefined) {\n result.calls = bottomChildCalls;\n }\n },\n\n putErrorOrCreatedContract(result, ctx, db) {\n const error = this.error(ctx);\n\n if (error !== undefined) {\n result.error = error\n } else {\n result.createdContractAddressHash = toHex(ctx.to);\n if (toHex(ctx.input) != '0x') {\n result.createdContractCode = toHex(db.getCode(ctx.to));\n } else {\n result.createdContractCode = '0x';\n }\n }\n },\n\n error(ctx) {\n var error;\n\n const bottomCall = this.bottomCall();\n const bottomCallError = bottomCall.error;\n\n if (bottomCallError !== undefined) {\n error = bottomCallError;\n } else {\n const ctxError = ctx.error;\n\n if (ctxError !== undefined) {\n error = ctxError;\n }\n }\n\n return error;\n },\n\n filterNotUndefined(call) {\n for (var key in call) {\n if (call[key] === undefined) {\n delete call[key];\n }\n }\n\n if (call.calls !== undefined) {\n for (var i = 0; i < call.calls.length; i++) {\n call.calls[i] = this.filterNotUndefined(call.calls[i]);\n }\n }\n\n return call;\n },\n\n // sequence converts the finalized calls from a call tree to a call sequence\n sequence(call, callSequence, availableValueBigInt, traceAddress) {\n const subcalls = call.calls;\n delete call.calls;\n\n call.traceAddress = traceAddress;\n\n if (call.type === 'call' && call.callType === 'delegatecall') {\n call.valueBigInt = availableValueBigInt;\n }\n\n var newCallSequence = callSequence.concat([call]);\n\n if (subcalls !== undefined) {\n for (var i = 0; i < subcalls.length; i++) {\n const nestedSequenced = this.sequence(\n subcalls[i],\n newCallSequence,\n call.valueBigInt,\n traceAddress.concat([i])\n );\n newCallSequence = nestedSequenced.callSequence;\n }\n }\n\n return {\n callSequence: newCallSequence\n };\n },\n\n encodeCallSequence(calls) {\n for (var i = 0; i < calls.length; i++) {\n this.encodeCall(calls[i]);\n }\n\n return calls;\n },\n\n encodeCall(call) {\n this.putValue(call);\n this.putGas(call);\n this.putGasUsed(call);\n\n return call;\n },\n\n putValue(call) {\n const valueBigInt = call.valueBigInt;\n delete call.valueBigInt;\n\n call.value = '0x' + valueBigInt.toString(16);\n },\n\n putGas(call) {\n const gasBigInt = call.gasBigInt;\n delete call.gasBigInt;\n\n if (gasBigInt === undefined) {\n gasBigInt = bigInt.zero;\n }\n\n call.gas = '0x' + gasBigInt.toString(16);\n },\n\n putGasUsed(call) {\n const gasUsedBigInt = call.gasUsedBigInt;\n delete call.gasUsedBigInt;\n\n if (gasUsedBigInt === undefined) {\n gasUsedBigInt = bigInt.zero;\n }\n\n call.gasUsed = '0x' + gasUsedBigInt.toString(16);\n }\n}\n" +} diff --git a/test/moonwall.config.json b/test/moonwall.config.json index 048181611f..dcd07ff794 100644 --- a/test/moonwall.config.json +++ b/test/moonwall.config.json @@ -308,6 +308,44 @@ ] } }, + { + "name": "dev_moonbase_tracing", + "testFileDir": ["suites/tracing-tests"], + "include": ["**/*test*"], + "contracts": "contracts/", + "runScripts": ["compile-contracts.ts compile"], + "multiThreads": true, + "envVars": ["DEBUG_COLORS=1"], + "reporters": ["html", "basic"], + "foundation": { + "type": "dev", + "launchSpec": [ + { + "name": "moonbeam", + "binPath": "../target/release/moonbeam", + "newRpcBehaviour": true, + "options": [ + "--execution=Wasm", + "--wasm-execution=compiled", + "--ethapi=txpool,debug,trace", + "--no-hardware-benchmarks", + "--no-telemetry", + "--reserved-only", + "--no-grandpa", + "--no-prometheus", + "--force-authoring", + "--rpc-cors=all", + "--alice", + "--chain=moonbase-dev", + "--sealing=manual", + "--tmp", + "--wasm-runtime-overrides=moonbase-overrides", + "--blocks-pruning=archive" + ] + } + ] + } + }, { "name": "dev_moonbase_custom", "testFileDir": ["suites/dev/"], diff --git a/test/suites/tracing-tests/test-trace-1.ts b/test/suites/tracing-tests/test-trace-1.ts new file mode 100644 index 0000000000..c2982f4f7e --- /dev/null +++ b/test/suites/tracing-tests/test-trace-1.ts @@ -0,0 +1,268 @@ +import { + beforeAll, + customDevRpcRequest, + describeSuite, + expect, + DevModeContext, + deployCreateCompiledContract, +} from "@moonwall/cli"; + +import { alith, ALITH_PRIVATE_KEY, createEthersTransaction } from "@moonwall/util"; + +import { Abi, encodeFunctionData } from "viem"; + +const BS_TRACER = require("../../helpers/tracer/blockscout_tracer.min.json"); + +export async function createContracts(context: DevModeContext) { + let nonce = await context.viem().getTransactionCount({ address: alith.address as `0x${string}` }); + const { contractAddress: callee, abi: abiCallee } = await deployCreateCompiledContract( + context, + "TraceCallee", + { nonce: nonce++ } + ); + + const { contractAddress: caller, abi: abiCaller } = await deployCreateCompiledContract( + context, + "TraceCaller", + { nonce: nonce++ } + ); + await context.createBlock(); + + return { + abiCallee, + abiCaller, + calleeAddr: callee, + callerAddr: caller, + nonce: nonce, + }; +} + +export async function nestedCall( + context: DevModeContext, + callerAddr: string, + calleeAddr: string, + abiCaller: Abi, + nonce: number +) { + const callTx = await createEthersTransaction(context, { + to: callerAddr, + data: encodeFunctionData({ + abi: abiCaller, + functionName: "someAction", + args: [calleeAddr, 6], + }), + nonce: nonce, + gasLimit: "0x100000", + value: "0x00", + }); + return await customDevRpcRequest("eth_sendRawTransaction", [callTx]); +} + +export async function nestedSingle(context: DevModeContext) { + const contracts = await createContracts(context); + return await nestedCall( + context, + contracts.callerAddr, + contracts.calleeAddr, + contracts.abiCaller, + contracts.nonce + ); +} + +describeSuite({ + id: "D3601", + title: "Trace", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + beforeAll(async () => {}); + + // This test proves that Raw traces are now stored outside the runtime. + // + // Previously exhausted Wasm memory allocation: + // Thread 'tokio-runtime-worker' panicked at 'Failed to allocate memory: + // "Allocator ran out of space"'. + it({ + id: "T01", + title: "should prevent Wasm memory overflow", + test: async function () { + const { abi: abiTraceFilter, hash: hash1 } = await context.deployContract!("TraceFilter", { + args: [false], + }); + let receipt = await context.viem().getTransactionReceipt({ hash: hash1 }); + let nonce = await context + .viem() + .getTransactionCount({ address: alith.address as `0x${string}` }); + // Produce a +58,000 step trace. + const callTx = await createEthersTransaction(context, { + from: alith.address, + to: receipt.contractAddress, + data: encodeFunctionData({ + abi: abiTraceFilter, + functionName: "set_and_loop", + args: [10], + }), + nonce: nonce, + gasLimit: "0x100000", + privateKey: ALITH_PRIVATE_KEY, + }); + + const { result } = await context.createBlock(callTx); + + expect( + async () => await customDevRpcRequest("debug_traceTransaction", [result?.hash]), + "Trace should be reverted but it worked instead" + ).rejects.toThrowError( + "replayed transaction generated too much data. try disabling memory or storage?" + ); + }, + }); + + it({ + id: "T02", + title: "should replay over an intermediate state", + test: async function () { + const { abi: abiIncrementor, hash: hash1 } = await context.deployContract!("Incrementor"); + let receipt = await context.viem().getTransactionReceipt({ hash: hash1 }); + + // In our case, the total number of transactions == the max value of the incrementer. + // If we trace the last transaction of the block, should return the total number of + // transactions we executed (10). + // If we trace the 5th transaction, should return 5 and so on. + // + // So we set 5 different target txs for a single block: the 1st, 3 intermediate, and + // the last. + const totalTxs = 10; + let targets = [1, 2, 5, 8, 10]; + let txs = []; + let nonce = await context + .viem() + .getTransactionCount({ address: alith.address as `0x${string}` }); + + // Create 10 transactions in a block. + for (let numTxs = nonce; numTxs <= nonce + totalTxs; numTxs++) { + const callTx = await createEthersTransaction(context, { + from: alith.address, + to: receipt.contractAddress, + data: encodeFunctionData({ + abi: abiIncrementor, + functionName: "incr", + args: [1], + }), + nonce: numTxs, + gasLimit: "0x100000", + privateKey: ALITH_PRIVATE_KEY, + }); + + const data = await customDevRpcRequest("eth_sendRawTransaction", [callTx]); + //console.log(data) + txs.push(data); + } + await context.createBlock(); + + // Trace 5 target transactions on it. + for (let target of targets) { + let index = target - 1; + + await context.viem().getTransactionReceipt({ hash: txs[index] }); + + let intermediateTx = await customDevRpcRequest("debug_traceTransaction", [txs[index]]); + + let evmResult = context.web3().utils.hexToNumber("0x" + intermediateTx.returnValue); + expect(evmResult).to.equal(target); + } + }, + }); + + it({ + id: "T03", + title: "should trace nested contract calls", + test: async function () { + const send = await nestedSingle(context); + await context.createBlock(); + let traceTx = await customDevRpcRequest("debug_traceTransaction", [send]); + let logs = []; + for (let log of traceTx.structLogs) { + if (logs.length == 1) { + logs.push(log); + } + if (log.op == "RETURN") { + logs.push(log); + } + } + expect(logs).to.be.lengthOf(2); + expect(logs[0].depth).to.be.equal(2); + expect(logs[1].depth).to.be.equal(1); + }, + }); + + it({ + id: "T04", + title: "should use optional disable parameters", + test: async function () { + const send = await nestedSingle(context); + await context.createBlock(); + let traceTx = await customDevRpcRequest("debug_traceTransaction", [ + send, + { disableMemory: true, disableStack: true, disableStorage: true }, + ]); + let logs = []; + for (let log of traceTx.structLogs) { + if ( + log.hasOwnProperty("storage") || + log.hasOwnProperty("memory") || + log.hasOwnProperty("stack") + ) { + logs.push(log); + } + } + expect(logs.length).to.be.equal(0); + }, + }); + + it({ + id: "T05", + title: "should format as request (Blockscout)", + test: async function () { + const send = await nestedSingle(context); + await context.createBlock(); + let traceTx = await customDevRpcRequest("debug_traceTransaction", [ + send, + { tracer: BS_TRACER.body }, + ]); + let entries = traceTx; + expect(entries).to.be.lengthOf(2); + let resCaller = entries[0]; + let resCallee = entries[1]; + expect(resCaller.callType).to.be.equal("call"); + expect(resCallee.type).to.be.equal("call"); + expect(resCallee.from).to.be.equal(resCaller.to); + expect(resCaller.traceAddress).to.be.empty; + expect(resCallee.traceAddress.length).to.be.eq(1); + expect(resCallee.traceAddress[0]).to.be.eq(0); + }, + }); + + it({ + id: "T06", + title: "should format as request (Blockscout)", + test: async function () { + const send = await nestedSingle(context); + await context.createBlock(); + let traceTx = await customDevRpcRequest("debug_traceTransaction", [ + send, + { tracer: BS_TRACER.body }, + ]); + let entries = traceTx; + expect(entries).to.be.lengthOf(2); + let resCaller = entries[0]; + let resCallee = entries[1]; + expect(resCaller.callType).to.be.equal("call"); + expect(resCallee.type).to.be.equal("call"); + expect(resCallee.from).to.be.equal(resCaller.to); + expect(resCaller.traceAddress).to.be.empty; + expect(resCallee.traceAddress.length).to.be.eq(1); + expect(resCallee.traceAddress[0]).to.be.eq(0); + }, + }); + }, +}); diff --git a/test/suites/tracing-tests/test-trace-2.ts b/test/suites/tracing-tests/test-trace-2.ts new file mode 100644 index 0000000000..5322a1e5df --- /dev/null +++ b/test/suites/tracing-tests/test-trace-2.ts @@ -0,0 +1,37 @@ +import { customDevRpcRequest, describeSuite, expect, TransactionTypes } from "@moonwall/cli"; + +import { nestedSingle } from "./test-trace-1"; + +const BS_TRACER_V2 = require("../../helpers/tracer/blockscout_tracer_v2.min.json"); + +describeSuite({ + id: "D3602", + title: "Trace blockscout v2 - AllEthTxTypes", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + for (const txnType of TransactionTypes) { + it({ + id: `T0${TransactionTypes.indexOf(txnType) + 1}`, + title: "should format as request", + test: async function () { + const send = await nestedSingle(context); + await context.createBlock(); + let traceTx = await customDevRpcRequest("debug_traceTransaction", [ + send, + { tracer: BS_TRACER_V2.body }, + ]); + let entries = traceTx; + expect(entries).to.be.lengthOf(2); + let resCaller = entries[0]; + let resCallee = entries[1]; + expect(resCaller.callType).to.be.equal("call"); + expect(resCallee.type).to.be.equal("call"); + expect(resCallee.from).to.be.equal(resCaller.to); + expect(resCaller.traceAddress).to.be.empty; + expect(resCallee.traceAddress.length).to.be.eq(1); + expect(resCallee.traceAddress[0]).to.be.eq(0); + }, + }); + } + }, +}); diff --git a/test/suites/tracing-tests/test-trace-3.ts b/test/suites/tracing-tests/test-trace-3.ts new file mode 100644 index 0000000000..6f41ff2930 --- /dev/null +++ b/test/suites/tracing-tests/test-trace-3.ts @@ -0,0 +1,69 @@ +import { customDevRpcRequest, describeSuite, expect, TransactionTypes } from "@moonwall/cli"; + +import { + alith, + ALITH_PRIVATE_KEY, + createEthersTransaction, + PRECOMPILE_CROWDLOAN_REWARDS_ADDRESS, +} from "@moonwall/util"; + +const BS_TRACER_V2 = require("../../helpers/tracer/blockscout_tracer_v2.min.json"); + +describeSuite({ + id: "D3603", + title: "Trace (Blockscout v2) - AllEthTxTypes", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + for (const txnType of TransactionTypes) { + it({ + id: `T0${TransactionTypes.indexOf(txnType) + 1}`, + title: "should trace correctly out of gas transaction execution", + test: async function () { + const { contractAddress: looperAddress } = await context.deployContract!("Looper"); + + const callTx = await createEthersTransaction(context, { + from: alith.address, + to: looperAddress, + data: "0x5bec9e67", + gasLimit: "0x100000", + value: "0x00", + privateKey: ALITH_PRIVATE_KEY, + }); + + const data = await customDevRpcRequest("eth_sendRawTransaction", [callTx]); + await context.createBlock(); + let trace = await customDevRpcRequest("debug_traceTransaction", [ + data, + { tracer: BS_TRACER_V2.body }, + ]); + expect(trace.length).to.be.eq(1); + expect(trace[0].error).to.be.equal("out of gas"); + }, + }); + + it({ + id: `T0${TransactionTypes.indexOf(txnType) + 2}`, + title: "should trace correctly precompiles", + test: async function () { + const callTx = await createEthersTransaction(context, { + from: alith.address, + to: PRECOMPILE_CROWDLOAN_REWARDS_ADDRESS, + data: "0x4e71d92d", + gasLimit: "0xdb3b", + value: "0x00", + privateKey: ALITH_PRIVATE_KEY, + }); + + const data = await customDevRpcRequest("eth_sendRawTransaction", [callTx]); + await context.createBlock(); + let trace = await customDevRpcRequest("debug_traceTransaction", [ + data, + { tracer: BS_TRACER_V2.body }, + ]); + + expect(trace.length).to.be.eq(1); + }, + }); + } + }, +}); diff --git a/test/suites/tracing-tests/test-trace-4.ts b/test/suites/tracing-tests/test-trace-4.ts new file mode 100644 index 0000000000..a1af386186 --- /dev/null +++ b/test/suites/tracing-tests/test-trace-4.ts @@ -0,0 +1,88 @@ +import { customDevRpcRequest, describeSuite, expect } from "@moonwall/cli"; +import { + ALITH_PRIVATE_KEY, + createEthersTransaction, + PRECOMPILE_CROWDLOAN_REWARDS_ADDRESS, + baltathar, + alith, +} from "@moonwall/util"; + +const BS_TRACER = require("../../helpers/tracer/blockscout_tracer.min.json"); + +describeSuite({ + id: "D3604", + title: "Trace (Blockscout)", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + it({ + id: "T01", + title: "should trace correctly out of gas transaction execution", + test: async function () { + const { contractAddress: looperAddress } = await context.deployContract!("Looper"); + + const callTx = await createEthersTransaction(context, { + from: alith.address, + to: looperAddress, + data: "0x5bec9e67", + gasLimit: "0x100000", + value: "0x00", + privateKey: ALITH_PRIVATE_KEY, + }); + + const data = await customDevRpcRequest("eth_sendRawTransaction", [callTx]); + await context.createBlock(); + let trace = await customDevRpcRequest("debug_traceTransaction", [ + data, + { tracer: BS_TRACER.body }, + ]); + expect(trace.length).to.be.eq(1); + expect(trace[0].error).to.be.equal("out of gas"); + }, + }); + + it({ + id: "T02", + title: "should trace correctly precompiles", + test: async function () { + const callTx = await createEthersTransaction(context, { + from: alith.address, + to: PRECOMPILE_CROWDLOAN_REWARDS_ADDRESS, + data: "0x4e71d92d", + gasLimit: "0xdb3b", + value: "0x00", + privateKey: ALITH_PRIVATE_KEY, + }); + + const data = await customDevRpcRequest("eth_sendRawTransaction", [callTx]); + await context.createBlock(); + let trace = await customDevRpcRequest("debug_traceTransaction", [ + data, + { tracer: BS_TRACER.body }, + ]); + + expect(trace.length).to.be.eq(1); + }, + }); + + it({ + id: "T03", + title: "should trace correctly transfers (raw)", + test: async function () { + const callTx = await createEthersTransaction(context, { + from: alith.address, + to: baltathar.address, + data: "0x", + gasLimit: "0xdb3b", + value: "0x10000000", + privateKey: ALITH_PRIVATE_KEY, + }); + + const data = await customDevRpcRequest("eth_sendRawTransaction", [callTx]); + await context.createBlock(); + let trace = await customDevRpcRequest("debug_traceTransaction", [data]); + + expect(trace.gas).to.be.eq("0x5208"); // 21_000 gas for a transfer. + }, + }); + }, +}); diff --git a/test/suites/tracing-tests/test-trace-5.ts b/test/suites/tracing-tests/test-trace-5.ts new file mode 100644 index 0000000000..1bcc79af11 --- /dev/null +++ b/test/suites/tracing-tests/test-trace-5.ts @@ -0,0 +1,158 @@ +import { + customDevRpcRequest, + describeSuite, + expect, + deployCreateCompiledContract, +} from "@moonwall/cli"; + +import { alith } from "@moonwall/util"; +import { nestedCall, nestedSingle, createContracts } from "./test-trace-1"; + +describeSuite({ + id: "D3605", + title: "Trace (callTrace)", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + it({ + id: "T01", + title: "should format as request (Call)", + test: async function () { + const send = await nestedSingle(context); + await context.createBlock(); + let traceTx = await customDevRpcRequest("debug_traceTransaction", [ + send, + { tracer: "callTracer" }, + ]); + let res = traceTx; + // Fields + expect(Object.keys(res).sort()).to.deep.equal([ + "calls", + "from", + "gas", + "gasUsed", + "input", + "output", + "to", + "type", + "value", + ]); + // Type + expect(res.type).to.be.equal("CALL"); + // Nested calls + let calls = res.calls; + expect(calls.length).to.be.eq(1); + let nested_call = calls[0]; + expect(res.to).to.be.equal(nested_call.from); + expect(nested_call.type).to.be.equal("CALL"); + }, + }); + + it({ + id: "T02", + title: "should format as request (Create)", + test: async function () { + let nonce = await context + .viem() + .getTransactionCount({ address: alith.address as `0x${string}` }); + const { contractAddress: callee, hash: createTxHash } = await deployCreateCompiledContract( + context, + "TraceCallee", + { nonce: nonce++ } + ); + + let traceTx = await customDevRpcRequest("debug_traceTransaction", [ + createTxHash, + { tracer: "callTracer" }, + ]); + + // Fields + expect(Object.keys(traceTx).sort()).to.deep.equal([ + "from", + "gas", + "gasUsed", + "input", + "output", + "to", + "type", + "value", + ]); + // Type + expect(traceTx.type).to.be.equal("CREATE"); + }, + }); + + it({ + id: "T03", + title: "should trace block by number and hash", + test: async function () { + const contracts = await createContracts(context); + let nonce = contracts.nonce; + await nestedCall( + context, + contracts.callerAddr, + contracts.calleeAddr, + contracts.abiCaller, + nonce++ + ); + await nestedCall( + context, + contracts.callerAddr, + contracts.calleeAddr, + contracts.abiCaller, + nonce++ + ); + await nestedCall( + context, + contracts.callerAddr, + contracts.calleeAddr, + contracts.abiCaller, + nonce++ + ); + await context.createBlock(); + const block = await context.viem().getBlock({ blockTag: "latest" }); + const block_number = context.web3().utils.toHex(await context.viem().getBlockNumber()); + const block_hash = block.hash; + // Trace block by number. + let traceTx = await customDevRpcRequest("debug_traceBlockByNumber", [ + block_number, + { tracer: "callTracer" }, + ]); + expect(block.transactions.length).to.be.equal(traceTx.length); + traceTx.forEach((trace: { [key: string]: any }) => { + expect(trace.calls.length).to.be.equal(1); + expect(Object.keys(trace).sort()).to.deep.equal([ + "calls", + "from", + "gas", + "gasUsed", + "input", + "output", + "to", + "type", + "value", + ]); + }); + // Trace block by hash (actually the rpc method is an alias of debug_traceBlockByNumber). + traceTx = await customDevRpcRequest("debug_traceBlockByHash", [ + block_hash, + { tracer: "callTracer" }, + ]); + expect(block.transactions.length).to.be.equal(traceTx.length); + traceTx.forEach((trace: { [key: string]: any }) => { + expect(trace.calls.length).to.be.equal(1); + expect(Object.keys(trace).sort()).to.deep.equal([ + "calls", + "from", + "gas", + "gasUsed", + "input", + "output", + "to", + "type", + "value", + ]); + }); + }, + }); + }, +}); diff --git a/test/suites/tracing-tests/test-trace-6.ts b/test/suites/tracing-tests/test-trace-6.ts new file mode 100644 index 0000000000..01cc11d0d4 --- /dev/null +++ b/test/suites/tracing-tests/test-trace-6.ts @@ -0,0 +1,202 @@ +import { customDevRpcRequest, describeSuite, expect, fetchCompiledContract } from "@moonwall/cli"; + +import { + alith, + ALITH_PRIVATE_KEY, + createEthersTransaction, + PRECOMPILE_BATCH_ADDRESS, +} from "@moonwall/util"; + +import { encodeFunctionData } from "viem"; + +describeSuite({ + id: "D3606", + title: "Trace (call list)", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + it({ + id: "T01", + title: "should correctly trace subcall", + test: async function () { + const { contractAddress: contractProxy, abi: abiProxy } = await context.deployContract!( + "CallForwarder" + ); + + const { contractAddress: contractDummy, abi: abiDummy } = await context.deployContract!( + "MultiplyBy7" + ); + + const callTx = await createEthersTransaction(context, { + from: alith.address, + to: contractProxy, + gasLimit: "0x100000", + value: "0x00", + privateKey: ALITH_PRIVATE_KEY, + data: encodeFunctionData({ + abi: abiProxy, + functionName: "call", + args: [ + contractDummy, + encodeFunctionData({ + abi: abiDummy, + functionName: "multiply", + args: [42], + }), + ], + }), + }); + + const data = await customDevRpcRequest("eth_sendRawTransaction", [callTx]); + await context.createBlock(); + let trace = await customDevRpcRequest("debug_traceTransaction", [ + data, + { tracer: "callTracer" }, + ]); + + expect(trace.from).to.be.eq(alith.address.toLowerCase()); + expect(trace.to).to.be.eq(contractProxy.toLowerCase()); + expect(trace.calls.length).to.be.eq(1); + expect(trace.calls[0].from).to.be.eq(contractProxy.toLowerCase()); + expect(trace.calls[0].to).to.be.eq(contractDummy.toLowerCase()); + expect(trace.calls[0].type).to.be.eq("CALL"); + }, + }); + + it({ + id: "T02", + title: "should correctly trace delegatecall subcall", + test: async function () { + const { contractAddress: contractProxy, abi: abiProxy } = await context.deployContract!( + "CallForwarder" + ); + + const { contractAddress: contractDummy, abi: abiDummy } = await context.deployContract!( + "MultiplyBy7" + ); + + const callTx = await createEthersTransaction(context, { + from: alith.address, + to: contractProxy, + gasLimit: "0x100000", + value: "0x00", + privateKey: ALITH_PRIVATE_KEY, + data: encodeFunctionData({ + abi: abiProxy, + functionName: "delegateCall", + args: [ + contractDummy, + encodeFunctionData({ + abi: abiDummy, + functionName: "multiply", + args: [42], + }), + ], + }), + }); + + const data = await customDevRpcRequest("eth_sendRawTransaction", [callTx]); + await context.createBlock(); + let trace = await customDevRpcRequest("debug_traceTransaction", [ + data, + { tracer: "callTracer" }, + ]); + + expect(trace.from).to.be.eq(alith.address.toLowerCase()); + expect(trace.to).to.be.eq(contractProxy.toLowerCase()); + expect(trace.calls.length).to.be.eq(1); + expect(trace.calls[0].from).to.be.eq(contractProxy.toLowerCase()); + expect(trace.calls[0].to).to.be.eq(contractDummy.toLowerCase()); + expect(trace.calls[0].type).to.be.eq("DELEGATECALL"); + }, + }); + + it({ + id: "T03", + title: "should correctly trace precompile subcall (call list)", + timeout: 10000, + test: async function () { + const { contractAddress: contractProxy, abi: abiProxy } = await context.deployContract!( + "CallForwarder" + ); + + const { contractAddress: contractDummy, abi: abiDummy } = await context.deployContract!( + "MultiplyBy7" + ); + + const abiBatch = fetchCompiledContract("Batch").abi; + + const callTx = await createEthersTransaction(context, { + from: alith.address, + to: PRECOMPILE_BATCH_ADDRESS, + gasLimit: "0x100000", + value: "0x00", + privateKey: ALITH_PRIVATE_KEY, + data: encodeFunctionData({ + abi: abiBatch, + functionName: "batchAll", + args: [ + [contractProxy, contractProxy], + [], + [ + encodeFunctionData({ + abi: abiProxy, + functionName: "call", + args: [ + contractDummy, + encodeFunctionData({ + abi: abiDummy, + functionName: "multiply", + args: [42], + }), + ], + }), + encodeFunctionData({ + abi: abiProxy, + functionName: "delegateCall", + args: [ + contractDummy, + encodeFunctionData({ + abi: abiDummy, + functionName: "multiply", + args: [42], + }), + ], + }), + ], + [], + ], + }), + }); + + const data = await customDevRpcRequest("eth_sendRawTransaction", [callTx]); + await context.createBlock(); + let trace = await customDevRpcRequest("debug_traceTransaction", [ + data, + { tracer: "callTracer" }, + ]); + + expect(trace.from).to.be.eq(alith.address.toLowerCase()); + expect(trace.to).to.be.eq(PRECOMPILE_BATCH_ADDRESS); + expect(trace.calls.length).to.be.eq(2); + + expect(trace.calls[0].from).to.be.eq(PRECOMPILE_BATCH_ADDRESS); + expect(trace.calls[0].to).to.be.eq(contractProxy.toLowerCase()); + expect(trace.calls[0].type).to.be.eq("CALL"); + + expect(trace.calls[0].calls.length).to.be.eq(1); + expect(trace.calls[0].calls[0].from).to.be.eq(contractProxy.toLowerCase()); + expect(trace.calls[0].calls[0].to).to.be.eq(contractDummy.toLowerCase()); + expect(trace.calls[0].calls[0].type).to.be.eq("CALL"); + + expect(trace.calls[1].from).to.be.eq(PRECOMPILE_BATCH_ADDRESS); + expect(trace.calls[1].to).to.be.eq(contractProxy.toLowerCase()); + expect(trace.calls[1].type).to.be.eq("CALL"); + + expect(trace.calls[1].calls.length).to.be.eq(1); + expect(trace.calls[1].calls[0].from).to.be.eq(contractProxy.toLowerCase()); + expect(trace.calls[1].calls[0].to).to.be.eq(contractDummy.toLowerCase()); + expect(trace.calls[1].calls[0].type).to.be.eq("DELEGATECALL"); + }, + }); + }, +}); diff --git a/test/suites/tracing-tests/test-trace-7.ts b/test/suites/tracing-tests/test-trace-7.ts new file mode 100644 index 0000000000..429dba1502 --- /dev/null +++ b/test/suites/tracing-tests/test-trace-7.ts @@ -0,0 +1,50 @@ +import { customDevRpcRequest, describeSuite, expect } from "@moonwall/cli"; + +import { ALITH_PRIVATE_KEY, alith, createEthersTransaction } from "@moonwall/util"; + +import { encodeFunctionData } from "viem"; + +describeSuite({ + id: "D3607", + title: "Raw trace limits", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + it({ + id: "T01", + title: "should not trace call that would produce too big responses", + timeout: 50000, + test: async function () { + const { contractAddress: traceFilterContract, abi: abiTraceFilter } = + await context.deployContract!("TraceFilter", { + args: [false], + }); + + const callTx = await createEthersTransaction(context, { + from: alith.address, + to: traceFilterContract, + gasLimit: "0x800000", + value: "0x00", + privateKey: ALITH_PRIVATE_KEY, + data: encodeFunctionData({ + abi: abiTraceFilter, + functionName: "heavy_steps", + args: [ + 100, // number of storage modified + 1000, // numbers of simple steps (that will have 100 storage items in trace) + ], + }), + }); + + const data = await customDevRpcRequest("eth_sendRawTransaction", [callTx]); + await context.createBlock(); + + expect( + async () => await customDevRpcRequest("debug_traceTransaction", [data]), + "Trace should be reverted but it worked instead" + ).rejects.toThrowError( + "replayed transaction generated too much data. try disabling memory or storage?" + ); + }, + }); + }, +}); diff --git a/test/suites/tracing-tests/test-trace-concurrency.ts b/test/suites/tracing-tests/test-trace-concurrency.ts new file mode 100644 index 0000000000..c40a2f6873 --- /dev/null +++ b/test/suites/tracing-tests/test-trace-concurrency.ts @@ -0,0 +1,75 @@ +import "@moonbeam-network/api-augment"; +import { + describeSuite, + expect, + beforeAll, + deployCreateCompiledContract, + customDevRpcRequest, +} from "@moonwall/cli"; +import { createEthersTransaction } from "@moonwall/util"; +import { Abi, encodeFunctionData } from "viem"; + +describeSuite({ + id: "D3608", + title: "Trace filter - Concurrency", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + let looperAddress: `0x${string}`; + let looperABI: Abi; + + beforeAll(async () => { + const { contractAddress, abi } = await deployCreateCompiledContract(context, "Looper"); + looperAddress = contractAddress; + looperABI = abi; + + await context.createBlock(); + + for (let i = 0; i < 50; i++) { + const rawSigned = await createEthersTransaction(context, { + to: looperAddress, + data: encodeFunctionData({ + abi: looperABI, + functionName: "incrementalLoop", + args: [2000], + }), + gasLimit: 200_000, + }); + + const { result } = await context.createBlock(rawSigned); + } + }, 180000); + + // This test is based on the time needed for trace_filter to perform those actions. + // It will start a slow query (taking 1s) and will try to execute a fast one after to see if it + // goes through or wait for the first one to finish + it({ + id: "T01", + title: "should allow concurrent execution", + modifier: "skip", + test: async function () { + const queryRange = async (range: number, index: number) => { + const start = Date.now(); + await customDevRpcRequest("trace_filter", [ + { + fromBlock: context.web3().utils.numberToHex(1), + toBlock: context.web3().utils.numberToHex(range), + }, + ]); + const end = Date.now(); + console.log(`[${index}] 1-${range} Took: ${end - start} ms`); + }; + + // We start the slow query (around 1000ms), without waiting for it + const initialQueryPromise = queryRange(40, 1); + const startTime = Date.now(); + await queryRange(1, 2); + const endTime = Date.now(); + // Less than 500ms is large enough (it should take at max 50ms) + expect(endTime - startTime).to.be.lessThan(1000); + + // Wait for the initial query to finish to avoid pending queries + await initialQueryPromise; + }, + }); + }, +}); diff --git a/test/suites/tracing-tests/test-trace-erc20-xcm.ts b/test/suites/tracing-tests/test-trace-erc20-xcm.ts new file mode 100644 index 0000000000..8b54d0ea2b --- /dev/null +++ b/test/suites/tracing-tests/test-trace-erc20-xcm.ts @@ -0,0 +1,147 @@ +import { beforeAll, customDevRpcRequest, describeSuite, expect } from "@moonwall/cli"; +import { ALITH_ADDRESS, CHARLETH_ADDRESS, alith, customWeb3Request } from "@moonwall/util"; +import { ApiPromise } from "@polkadot/api"; +import { expectEVMResult } from "helpers/eth-transactions"; +import { ERC20_TOTAL_SUPPLY } from "helpers/transactions"; +import { + XcmFragment, + XcmFragmentConfig, + injectHrmpMessageAndSeal, + sovereignAccountOfSibling, +} from "helpers/xcm"; +import { hexToNumber, parseEther } from "viem"; + +describeSuite({ + id: "D3609", + title: "Trace ERC20 xcm", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + let erc20ContractAddress: string; + let transactionHash: string; + + beforeAll(async () => { + const { contractAddress, status } = await context.deployContract!("ERC20WithInitialSupply", { + args: ["ERC20", "20S", ALITH_ADDRESS, ERC20_TOTAL_SUPPLY], + }); + erc20ContractAddress = contractAddress; + expect(status).eq("success"); + + const paraId = 888; + const paraSovereign = sovereignAccountOfSibling(context, paraId); + const amountTransferred = 1_000_000n; + + // Get pallet indices + const metadata = await context.polkadotJs().rpc.state.getMetadata(); + const balancesPalletIndex = metadata.asLatest.pallets + .find(({ name }) => name.toString() == "Balances")! + .index.toNumber(); + const erc20XcmPalletIndex = metadata.asLatest.pallets + .find(({ name }) => name.toString() == "Erc20XcmBridge")! + .index.toNumber(); + + // Send some native tokens to the sovereign account of paraId (to pay fees) + await context + .polkadotJs() + .tx.balances.transfer(paraSovereign, parseEther("1")) + .signAndSend(alith); + await context.createBlock(); + + // Send some erc20 tokens to the sovereign account of paraId + const rawTx = await context.writeContract!({ + contractName: "ERC20WithInitialSupply", + contractAddress: erc20ContractAddress as `0x${string}`, + functionName: "transfer", + args: [paraSovereign, amountTransferred], + rawTxOnly: true, + }); + const { result } = await context.createBlock(rawTx); + expectEVMResult(result!.events, "Succeed"); + expect( + await context.readContract!({ + contractName: "ERC20WithInitialSupply", + contractAddress: erc20ContractAddress as `0x${string}`, + functionName: "balanceOf", + args: [paraSovereign], + }) + ).equals(amountTransferred); + + // Create the incoming xcm message + const config: XcmFragmentConfig = { + assets: [ + { + multilocation: { + parents: 0, + interior: { + X1: { PalletInstance: Number(balancesPalletIndex) }, + }, + }, + fungible: 1_000_000_000_000_000n, + }, + { + multilocation: { + parents: 0, + interior: { + X2: [ + { + PalletInstance: erc20XcmPalletIndex, + }, + { + AccountKey20: { + network: null, + key: erc20ContractAddress, + }, + }, + ], + }, + }, + fungible: amountTransferred, + }, + ], + beneficiary: CHARLETH_ADDRESS, + }; + + const xcmMessage = new XcmFragment(config) + .withdraw_asset() + .clear_origin() + .buy_execution() + .deposit_asset_v3(2n) + .as_v3(); + + // Mock the reception of the xcm message + await injectHrmpMessageAndSeal(context, paraId, { + type: "XcmVersionedXcm", + payload: xcmMessage, + }); + + transactionHash = (await context.viem().getBlock()).transactions[0]; + + // Erc20 tokens should have been received + expect( + await context.readContract!({ + contractName: "ERC20WithInitialSupply", + contractAddress: erc20ContractAddress as `0x${string}`, + functionName: "balanceOf", + args: [CHARLETH_ADDRESS], + }) + ).equals(amountTransferred); + }); + + it({ + id: "T01", + title: "should trace ERC20 xcm transaction with debug_traceTransaction", + test: async function () { + const receipt = await context + .viem() + .getTransactionReceipt({ hash: transactionHash as `0x${string}` }); + const trace = await customDevRpcRequest("debug_traceTransaction", [ + transactionHash, + { tracer: "callTracer" }, + ]); + // We traced the transaction, and the traced gas used should be greater* than or equal + // to the one recorded in the ethereum transaction receipt. + // *gasUsed on tracing does not take into account gas refund. + expect(hexToNumber(trace.gasUsed)).gte(Number(receipt.gasUsed)); + }, + }); + }, +}); diff --git a/test/suites/tracing-tests/test-trace-ethereum-xcm-1.ts b/test/suites/tracing-tests/test-trace-ethereum-xcm-1.ts new file mode 100644 index 0000000000..97305f4628 --- /dev/null +++ b/test/suites/tracing-tests/test-trace-ethereum-xcm-1.ts @@ -0,0 +1,146 @@ +import { beforeAll, customDevRpcRequest, describeSuite, expect } from "@moonwall/cli"; +import { + XcmFragment, + injectHrmpMessageAndSeal, + descendOriginFromAddress20, + RawXcmMessage, +} from "helpers/xcm"; +import { hexToNumber, Abi, encodeFunctionData } from "viem"; + +describeSuite({ + id: "D3610", + title: "Trace ethereum xcm #1", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + let incremetorAddress: `0x${string}`; + let incremetorABI: Abi; + let transactionHashes: `0x${string}`[] = []; + + beforeAll(async () => { + const { contractAddress, abi } = await context.deployContract!("Incrementor"); + incremetorAddress = contractAddress; + incremetorABI = abi; + + const { originAddress, descendOriginAddress } = descendOriginFromAddress20(context); + const sendingAddress = originAddress; + const transferredBalance = 10_000_000_000_000_000_000n; + + // We first fund parachain 2000 sovreign account + await context.createBlock( + context.polkadotJs().tx.balances.transfer(descendOriginAddress, transferredBalance), + { allowFailures: false } + ); + + // Get Pallet balances index + const metadata = await context.polkadotJs().rpc.state.getMetadata(); + const balancesPalletIndex = metadata.asLatest.pallets + .find(({ name }) => name.toString() == "Balances")! + .index.toNumber(); + + const xcmTransactions = [ + { + V1: { + gas_limit: 100000, + fee_payment: { + Auto: { + Low: null, + }, + }, + action: { + Call: incremetorAddress, + }, + value: 0n, + input: encodeFunctionData({ + abi: incremetorABI, + functionName: "incr", + args: [], + }), + access_list: null, + }, + }, + { + V2: { + gas_limit: 100000, + action: { + Call: incremetorAddress, + }, + value: 0n, + input: encodeFunctionData({ + abi: incremetorABI, + functionName: "incr", + args: [], + }), + access_list: null, + }, + }, + ]; + + for (const xcmTransaction of xcmTransactions) { + const transferCall = context.polkadotJs().tx.ethereumXcm.transact(xcmTransaction); + const transferCallEncoded = transferCall?.method.toHex(); + const xcmMessage = new XcmFragment({ + assets: [ + { + multilocation: { + parents: 0, + interior: { + X1: { PalletInstance: balancesPalletIndex }, + }, + }, + fungible: transferredBalance / 2n, + }, + ], + weight_limit: { + refTime: 4000000000n, + proofSize: 80000n, + } as any, + descend_origin: sendingAddress, + }) + .descend_origin() + .withdraw_asset() + .buy_execution() + .push_any({ + Transact: { + originKind: "SovereignAccount", + requireWeightAtMost: { + refTime: 3000000000n, + proofSize: 50000n, + }, + call: { + encoded: transferCallEncoded, + }, + }, + }) + .as_v3(); + + // Send an XCM and create block to execute it + await injectHrmpMessageAndSeal(context, 1, { + type: "XcmVersionedXcm", + payload: xcmMessage, + } as RawXcmMessage); + + // Retrieve the stored ethereum transaction hash + transactionHashes.push( + (await context.viem().getBlock({ blockTag: "latest" })).transactions[0] + ); + } + }); + + it({ + id: "T01", + title: "should trace ethereum xcm transactions with debug_traceTransaction", + test: async function () { + for (const hash of transactionHashes) { + const receipt = await context.viem().getTransactionReceipt({ hash }); + const trace = await customDevRpcRequest("debug_traceTransaction", [ + hash, + { tracer: "callTracer" }, + ]); + // We traced the transaction, and the traced gas used matches the one recorded + // in the ethereum transaction receipt. + expect(hexToNumber(trace.gasUsed)).to.eq(Number(receipt.gasUsed)); + } + }, + }); + }, +}); diff --git a/test/suites/tracing-tests/test-trace-ethereum-xcm-2.ts b/test/suites/tracing-tests/test-trace-ethereum-xcm-2.ts new file mode 100644 index 0000000000..a449109272 --- /dev/null +++ b/test/suites/tracing-tests/test-trace-ethereum-xcm-2.ts @@ -0,0 +1,138 @@ +import { beforeAll, customDevRpcRequest, describeSuite, expect } from "@moonwall/cli"; +import { + XcmFragment, + injectHrmpMessage, + descendOriginFromAddress20, + RawXcmMessage, +} from "helpers/xcm"; +import { alith } from "@moonwall/util"; +import { Abi, encodeFunctionData } from "viem"; + +describeSuite({ + id: "D3611", + title: "Trace ethereum xcm #2", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + let ethereumXcmDescendedOrigin: `0x${string}`; + let xcmContractAddress: `0x${string}`; + let ethContractAddress: `0x${string}`; + let xcmContractABI: Abi; + + beforeAll(async () => { + const { contractAddress, abi } = await context.deployContract!("Incrementor"); + xcmContractAddress = contractAddress; + xcmContractABI = abi; + + const { originAddress, descendOriginAddress } = descendOriginFromAddress20(context); + ethereumXcmDescendedOrigin = descendOriginAddress; + + const sendingAddress = originAddress; + const transferredBalance = 10_000_000_000_000_000_000n; + + // We first fund parachain 2000 sovreign account + await context.createBlock( + context.polkadotJs().tx.balances.transfer(ethereumXcmDescendedOrigin, transferredBalance), + { allowFailures: false } + ); + + // Get Pallet balances index + const metadata = await context.polkadotJs().rpc.state.getMetadata(); + const balancesPalletIndex = metadata.asLatest.pallets + .find(({ name }) => name.toString() == "Balances")! + .index.toNumber(); + + const xcmTransaction = { + V2: { + gas_limit: 100000, + action: { + Call: xcmContractAddress, + }, + value: 0n, + input: encodeFunctionData({ + abi: xcmContractABI, + functionName: "incr", + args: [], + }), + access_list: null, + }, + }; + + const transferCall = context.polkadotJs().tx.ethereumXcm.transact(xcmTransaction); + const transferCallEncoded = transferCall?.method.toHex(); + const xcmMessage = new XcmFragment({ + assets: [ + { + multilocation: { + parents: 0, + interior: { + X1: { PalletInstance: balancesPalletIndex }, + }, + }, + fungible: transferredBalance / 2n, + }, + ], + weight_limit: { + refTime: 4000000000n, + proofSize: 80000n, + } as any, + descend_origin: sendingAddress, + }) + .descend_origin() + .withdraw_asset() + .buy_execution() + .push_any({ + Transact: { + originKind: "SovereignAccount", + requireWeightAtMost: { + refTime: 3000000000n, + proofSize: 50000n, + }, + call: { + encoded: transferCallEncoded, + }, + }, + }) + .as_v3(); + + // Send an XCM and create block to execute it + await injectHrmpMessage(context, 1, { + type: "XcmVersionedXcm", + payload: xcmMessage, + } as RawXcmMessage); + + // By calling deployContract() a new block will be created, + // including the ethereum xcm call + regular ethereum transaction + const { contractAddress: eventEmitterAddress, abi: eventEmitterABI } = + await context.deployContract!("EventEmitter", { + from: alith.address, + } as any); + ethContractAddress = eventEmitterAddress; + }); + + it({ + id: "T01", + title: "should trace ethereum xcm transactions with debug_traceBlockByNumber", + test: async function () { + const number = await context.viem().getBlockNumber(); + const trace = await customDevRpcRequest("debug_traceBlockByNumber", [ + number.toString(), + { tracer: "callTracer" }, + ]); + // 2 ethereum transactions: ethereum xcm call + regular ethereum transaction + expect(trace.length).to.eq(2); + // 1st transaction is xcm. + // - `From` is the descended origin. + // - `To` is the xcm contract address. + expect(trace[0].from).to.eq(ethereumXcmDescendedOrigin.toLowerCase()); + expect(trace[0].to).to.eq(xcmContractAddress.toLowerCase()); + expect(trace[0].type).to.eq("CALL"); + // 2nd transaction is regular ethereum transaction. + // - `From` is Alith's adddress. + // - `To` is the ethereum contract address. + expect(trace[1].from).to.eq(alith.address.toLowerCase()); + expect(trace[1].to).to.eq(ethContractAddress.toLowerCase()); + expect(trace[1].type).be.eq("CREATE"); + }, + }); + }, +}); diff --git a/test/suites/tracing-tests/test-trace-filter-reorg.ts b/test/suites/tracing-tests/test-trace-filter-reorg.ts new file mode 100644 index 0000000000..46dc8a2c63 --- /dev/null +++ b/test/suites/tracing-tests/test-trace-filter-reorg.ts @@ -0,0 +1,98 @@ +import "@moonbeam-network/api-augment"; +import { describeSuite, customDevRpcRequest } from "@moonwall/cli"; +import { createEthersTransaction, generateKeyringPair } from "@moonwall/util"; + +describeSuite({ + id: "D3612", + title: "Trace filter reorg", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + it({ + id: "T01", + title: "successfully reorg", + timeout: 150000000, + test: async function () { + const randomAccount = generateKeyringPair(); + + // Create a first base block. + const block1 = await context.createBlock([], {}); + + const rawSigned = await createEthersTransaction(context, { + to: randomAccount.address, + data: null, + value: "0x200", + gasLimit: 25000, + }); + + // Create a first branch including a transaction. + await context.createBlock(rawSigned, { + parentHash: block1.block.hash, + finalize: false, + }); + // Contains nonce 0. + + const rawSigned2 = await createEthersTransaction(context, { + to: randomAccount.address, + data: null, + value: "0x300", + gasLimit: 25000, + nonce: 1, + }); + + // Create a branch. // nonce 1 + const block2a = await context.createBlock(rawSigned2, { + parentHash: block1.block.hash, + finalize: false, + }); + // Contains nonce 1. + + // Continue this new branch, it reorgs. + // + // This block doesn't contain the transaction with nonce 0. Reorg doesn't seems to add back + // extrinsics into the pool. + // + // This block however will contain the transaction with nonce 1 but the + // chain don't expect this nonce so the the Ethereum transaction in not executed. + // However it is still in the list of extrinsics for this block. + const block3a = await context.createBlock([], { + parentHash: block2a.block.hash, + finalize: false, + }); + // Contains nonce 1 again !. + + // Additionnal blocks. + const block4a = await context.createBlock([], { + parentHash: block3a.block.hash, + finalize: true, + }); + // Contains nonce 0. + + const block5a = await context.createBlock([], { + parentHash: block4a.block.hash, + finalize: true, + }); + // Contains nonce 1. + + const block6a = await context.createBlock([], { + parentHash: block5a.block.hash, + finalize: true, + }); + + await context.createBlock([], { + parentHash: block6a.block.hash, + finalize: true, + }); + + // Trace block 3a. + // With old tracer the nonce check was missing and thus the transaction was replayed, + // leading to a mismatch and a crash when mapping the Frontier data. + await customDevRpcRequest("trace_filter", [ + { + fromBlock: "0x01", + toBlock: "0x07", + }, + ]); + }, + }); + }, +}); diff --git a/test/suites/tracing-tests/test-trace-filter.ts b/test/suites/tracing-tests/test-trace-filter.ts new file mode 100644 index 0000000000..01b1d1b2f6 --- /dev/null +++ b/test/suites/tracing-tests/test-trace-filter.ts @@ -0,0 +1,257 @@ +import { beforeAll, customDevRpcRequest, describeSuite, expect } from "@moonwall/cli"; +import { ALITH_ADDRESS, ALITH_CONTRACT_ADDRESSES, alith } from "@moonwall/util"; + +describeSuite({ + id: "D3613", + title: "Trace filter - Contract creation ", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + // Setup: Create 4 blocks with TraceFilter contracts + beforeAll(async () => { + const { contractAddress } = await context.deployContract!("TraceFilter", { + args: [false], + }); + await context.deployContract!("TraceFilter", { + gas: 90_000n, + args: [true], + }); + + await context.deployContract!("TraceFilter", { + args: [false], + }); + await context.deployContract!("TraceFilter", { + args: [false], + }); + + const rawTx3 = await context.writeContract!({ + contractAddress, + contractName: "TraceFilter", + functionName: "subcalls", + + args: [ALITH_CONTRACT_ADDRESSES[2], ALITH_CONTRACT_ADDRESSES[3]], + gas: 1000_000n, + rawTxOnly: true, + }); + await context.createBlock(rawTx3, { allowFailures: false }); + }); + + it({ + id: "T01", + title: "should be able to replay deployed contract", + test: async function () { + const response = await customDevRpcRequest("trace_filter", [ + { + fromBlock: "0x01", + toBlock: "0x01", + }, + ]); + + const transactionHash = (await context.viem().getBlock({ blockNumber: 1n })) + .transactions[0]; + + expect(response.length).to.equal(1); + expect(response[0].action).to.include({ + creationMethod: "create", + from: ALITH_ADDRESS.toLocaleLowerCase(), + gas: "0x9caba", + value: "0x0", + }); + expect(response[0].result).to.include({ + address: ALITH_CONTRACT_ADDRESSES[0].toLocaleLowerCase(), + gasUsed: "0x1509b", // TODO : Compare with value from another (comparable) network. + }); + + expect(response[0]).to.include({ + blockNumber: 1, + subtraces: 0, + transactionHash: transactionHash, + transactionPosition: 0, + type: "create", + }); + }, + }); + + it({ + id: "T02", + title: "should be able to replay reverted contract", + test: async function () { + const response = await customDevRpcRequest("trace_filter", [ + { + fromBlock: "0x02", + toBlock: "0x02", + }, + ]); + + const transactionHash = (await context.web3().eth.getBlock(2)).transactions[0]; + + expect(response.length).to.equal(1); + expect(response[0].action.creationMethod).to.equal("create"); + expect(response[0].action.from).to.equal(ALITH_ADDRESS.toLocaleLowerCase()); + expect(response[0].action.gas).to.equal("0x104b"); + expect(response[0].action.init).to.be.a("string"); + expect(response[0].action.value).to.equal("0x0"); + expect(response[0].blockHash).to.be.a("string"); + expect(response[0].blockNumber).to.equal(2); + expect(response[0].result).to.equal(undefined); + expect(response[0].error).to.equal("Reverted"); + expect(response[0].subtraces).to.equal(0); + expect(response[0].traceAddress.length).to.equal(0); + expect(response[0].transactionHash).to.equal(transactionHash); + expect(response[0].transactionPosition).to.equal(0); + expect(response[0].type).to.equal("create"); + }, + }); + + it({ + id: "T03", + title: "should be able to trace through multiple blocks", + test: async function () { + const response = await customDevRpcRequest("trace_filter", [ + { + fromBlock: "0x02", + toBlock: "0x04", + }, + ]); + + expect(response.length).to.equal(3); + expect(response[0].blockNumber).to.equal(2); + expect(response[0].transactionPosition).to.equal(0); + expect(response[1].blockNumber).to.equal(3); + expect(response[1].transactionPosition).to.equal(0); + expect(response[2].blockNumber).to.equal(4); + expect(response[2].transactionPosition).to.equal(0); + }, + }); + + it({ + id: "T04", + title: "should be able to trace sub-call with reverts", + test: async function () { + const response = await customDevRpcRequest("trace_filter", [ + { + fromBlock: "0x05", + toBlock: "0x05", + }, + ]); + + expect(response.length).to.equal(7); + expect(response[0].subtraces).to.equal(2); + expect(response[0].traceAddress).to.deep.equal([]); + expect(response[1].subtraces).to.equal(2); + expect(response[1].traceAddress).to.deep.equal([0]); + expect(response[2].subtraces).to.equal(0); + expect(response[2].traceAddress).to.deep.equal([0, 0]); + expect(response[3].subtraces).to.equal(0); + expect(response[3].traceAddress).to.deep.equal([0, 1]); + expect(response[4].subtraces).to.equal(2); + expect(response[4].traceAddress).to.deep.equal([1]); + expect(response[5].subtraces).to.equal(0); + expect(response[5].traceAddress).to.deep.equal([1, 0]); + expect(response[6].subtraces).to.equal(0); + expect(response[6].traceAddress).to.deep.equal([1, 1]); + }, + }); + + it({ + id: "T05", + title: "should support tracing range of blocks", + test: async function () { + const response = await customDevRpcRequest("trace_filter", [ + { + fromBlock: "0x03", + toBlock: "0x05", + }, + ]); + + expect(response.length).to.equal(9); + expect(response[0].blockNumber).to.equal(3); + expect(response[0].transactionPosition).to.equal(0); + expect(response[1].blockNumber).to.equal(4); + expect(response[1].transactionPosition).to.equal(0); + expect(response[2].blockNumber).to.equal(5); + expect(response[2].transactionPosition).to.equal(0); + expect(response[3].blockNumber).to.equal(5); + expect(response[3].transactionPosition).to.equal(0); + expect(response[4].blockNumber).to.equal(5); + expect(response[4].transactionPosition).to.equal(0); + expect(response[5].blockNumber).to.equal(5); + expect(response[5].transactionPosition).to.equal(0); + expect(response[6].blockNumber).to.equal(5); + expect(response[6].transactionPosition).to.equal(0); + expect(response[7].blockNumber).to.equal(5); + expect(response[7].transactionPosition).to.equal(0); + expect(response[8].blockNumber).to.equal(5); + expect(response[8].transactionPosition).to.equal(0); + }, + }); + + it({ + id: "T06", + title: "should support filtering trace per fromAddress", + test: async function () { + const response = await customDevRpcRequest("trace_filter", [ + { + fromBlock: "0x03", + toBlock: "0x05", + fromAddress: [alith.address], + }, + ]); + + expect(response.length).to.equal(3); + }, + }); + + it({ + id: "T07", + title: "should support filtering trace per toAddress", + test: async function () { + const response = await customDevRpcRequest("trace_filter", [ + { + fromBlock: "0x03", + toBlock: "0x05", + toAddress: [ALITH_CONTRACT_ADDRESSES[3]], + }, + ]); + + expect(response.length).to.equal(4); + }, + }); + + it({ + id: "T08", + title: "should succeed for 500 traces request", + test: async function () { + await customDevRpcRequest("trace_filter", [ + { + fromBlock: "0x01", + toBlock: "0x04", + count: 500, + }, + ]).catch(() => { + expect.fail("should not fail"); + }); + }, + }); + + it({ + id: "T09", + title: "should fail for 501 traces request", + test: async function () { + await customDevRpcRequest("trace_filter", [ + { + fromBlock: "0x01", + toBlock: "0x04", + count: 501, + }, + ]).then( + () => { + expect.fail("should not succeed"); + }, + (error) => { + expect(error.message).to.eq("count (501) can't be greater than maximum (500)"); + } + ); + }, + }); + }, +}); diff --git a/test/suites/tracing-tests/test-trace-gas.ts b/test/suites/tracing-tests/test-trace-gas.ts new file mode 100644 index 0000000000..c76c167c90 --- /dev/null +++ b/test/suites/tracing-tests/test-trace-gas.ts @@ -0,0 +1,101 @@ +import "@moonbeam-network/api-augment"; +import { describeSuite, customDevRpcRequest, beforeAll, expect } from "@moonwall/cli"; +import { createEthersTransaction } from "@moonwall/util"; +import { Abi, encodeFunctionData } from "viem"; +import { numberToHex } from "@polkadot/util"; + +describeSuite({ + id: "D3614", + title: "Trace filter - Gas Loop", + foundationMethods: "dev", + testCases: ({ context, it, log }) => { + const testLoops: { + count: number; + txHash?: string; + blockNumber?: number; + expectedGas: string; + }[] = [ + { count: 0, expectedGas: "0x53dd" }, + { count: 100, expectedGas: "0x144ed" }, + { count: 1000, expectedGas: "0x67965" }, + ]; + + let looperAddress: `0x${string}`; + let looperAbi: Abi; + beforeAll(async () => { + const { contractAddress, abi } = await context.deployContract!("Looper"); + looperAddress = contractAddress; + looperAbi = abi; + + // For each loop, create a block with the contract execution. + // 1 block is create for each so it is easier to select the execution using trace_filter + // by specifying the fromBlock and toBlock + for (let i = 0; i < testLoops.length; i++) { + const loop = testLoops[i]; + const { result } = await context.createBlock( + createEthersTransaction(context, { + to: looperAddress, + data: encodeFunctionData({ + abi: looperAbi, + functionName: "incrementalLoop", + args: [loop.count], + }), + gasLimit: 3_000_000, + }) + ); + loop.txHash = result?.hash; + loop.blockNumber = i + 2; + } + }); + + it({ + id: "T01", + title: "should return 21630 gasUsed for 0 loop", + test: async function () { + const trace = await customDevRpcRequest("trace_filter", [ + { + fromBlock: numberToHex(testLoops[0].blockNumber), + toBlock: numberToHex(testLoops[0].blockNumber), + }, + ]); + expect(trace[0].result).to.not.be.undefined; + expect(trace[0].result.error).to.not.exist; + expect(trace[0].result.gasUsed).to.equal(testLoops[0].expectedGas); + }, + }); + + it({ + id: "T02", + title: "should return 245542 gasUsed for 100 loop", + test: async function () { + const trace = await customDevRpcRequest("trace_filter", [ + { + fromBlock: numberToHex(testLoops[1].blockNumber), + toBlock: numberToHex(testLoops[1].blockNumber), + }, + ]); + + expect(trace[0].result).to.not.be.undefined; + expect(trace[0].result.error).to.not.exist; + expect(trace[0].result.gasUsed).to.equal(testLoops[1].expectedGas); + }, + }); + + it({ + id: "T03", + title: "should return 2068654 gasUsed for 1000 loop", + test: async function () { + const trace = await customDevRpcRequest("trace_filter", [ + { + fromBlock: numberToHex(testLoops[2].blockNumber), + toBlock: numberToHex(testLoops[2].blockNumber), + }, + ]); + + expect(trace[0].result).to.not.be.undefined; + expect(trace[0].result.error).to.not.exist; + expect(trace[0].result.gasUsed).to.equal(testLoops[2].expectedGas); + }, + }); + }, +});