Wie ordnet man EVM-Trace der Vertragsquelle zu?

Mit anderen Worten, wie erhält man einen „klassischen Stacktrace“ einer fehlgeschlagenen Transaktion?

ZB haben wir einen Trace (sehen Sie sich nicht "Missing opcode 0xfd" an - es ist die Revert-Anweisung) und die Contract Solidity Source . Wie finde ich heraus, in welcher Zeile der Quelle die Ausnahme ausgelöst wurde?

Ich habe es geschafft, den Vertrag zusammenzustellen (mit solc --asm), aber es gibt keine PC-Hinweise (Programmzähler), daher kann ich keine Zeile finden, die PC = 557 entspricht. Ich gehe auch davon aus, dass die Optimierung während der Kompilierung durchgeführt wurde, aber trotz dieser Assemblierung noch bis zu einem gewissen Grad lesbar ist.

Ich verwende solc 0.4.16+commit.d7661dd9.Linux.g++.

Danke im Voraus.

Antworten (2)

Ich bin in dieses Kaninchenloch gegangen und habe am Ende einen Proof of Concept erhalten. Ich kann die Reise nicht empfehlen. Es gibt auf vielen Ebenen Impedanzfehlanpassungen, die viele Formatkonvertierungen erfordern. Am Ende verarbeitet meine Implementierung immer noch keine vertragsübergreifenden Anrufe. (Es scheint keine Möglichkeit zu geben, herauszufinden, zu welcher Vertragsadresse ein bestimmter Programmzähler gehört, außer die Aufrufanweisungen zu interpretieren).

Meine Implementierung ist zu schmutzig, um sie zu teilen, aber die wichtigsten Schritte sind:

1) Sie müssen solceine Laufzeit-Sourcemap erstellen. Es kann dies nicht direkt ausgeben, aber es kann dies als Teil der 'kombinierten json-Ausgabe' ausgeben. Führen Sie dazu aus solc --combined-json bin-runtime,srcmap-runtime.

const srcmaps = JSON.parse(fs.readFileSync("./Contract.json"));
const srcmap =
  srcmaps.contracts["./contracts/Contract.sol:Contract"]["srcmap-runtime"];

const source = fs.readFileSync("./contracts/Contract.sol").toString();

const bin = Buffer.from(
  srcmaps.contracts["./contracts/Contract.sol:Contract"]["bin-runtime"],
  "hex"
);

2) Das Sourcemap-Format ist komprimiert und Sie müssen einen Decoder schreiben. Die Spezifikation der Formate befindet sich in der Solidity-Dokumentation. Sie haben jetzt eine Möglichkeit, Anweisungsindizes Quellen-Offsets zuzuordnen.

3) Wir wollen keine Byte-Offsets in den Quelldateien, sondern Zeilen- und Spaltennummern. Dazu müssen Sie die Quelldateien parsen und eine Zuordnung von Byte-Offset zu Zeilen-/Spaltenpaaren erstellen. Ich beschloss, dies vorerst zu ignorieren und das get-line-from-posnpm-Paket zu verwenden.

Schritt 2 und 3 zusammen sind:

const parsed = srcmap
  .split(";")
  .map(l => l.split(":"))
  .map(([s, l, f, j]) => ({ s: s === "" ? undefined : s, l, f, j }))
  .reduce(
    ([last, ...list], { s, l, f, j }) => [
      {
        s: parseInt(s || last.s, 10),
        l: parseInt(l || last.l, 10),
        f: parseInt(f || last.f, 10),
        j: j || last.j
      },
      last,
      ...list
    ],
    [{}]
  )
  .reverse()
  .slice(1)
  .map(
    ({ s, l, f, j }) => `${srcmaps.sourceList[f]}:${getLineFromPos(source, s)}`
  );

4) Die Quellkarte befindet sich in der Anweisungsnummer, aber wir benötigen Bytecode-Adressen. Um dies zu lösen, müssen wir eine Karte vom Bytecode-Offset zur Befehlsnummer (oder umgekehrt) erstellen. Ich fand es am einfachsten, die Laufzeitbinärdatei selbst zu analysieren. Alle Anweisungen sind 1 Byte lang, außer PUSH_ndenen, die n+1lang sind.

const isPush = inst => inst >= 0x60 && inst < 0x7f;

const pushDataLength = inst => inst - 0x5f;

const instructionLength = inst => (isPush(inst) ? 1 + pushDataLength(inst) : 1);

const byteToInstIndex = bin => {
  const result = [];
  let byteIndex = 0;
  let instIndex = 0;
  while (byteIndex < bin.length) {
    const length = instructionLength(bin[byteIndex]);
    for (let i = 0; i < length; i += 1) {
      result.push(instIndex);
    }
    byteIndex += length;
    instIndex += 1;
  }
  return result;
};

Dann müssen Sie den Backtrace für eine bestimmte Transaktion abrufen:

const promisify = func => async (...args) =>
  new Promise((accept, reject) =>
    func(...args, (error, result) => (error ? reject(error) : accept(result)))
  );

const rpcCommand = method => async (...params) =>
  (await promisify(web3.currentProvider.sendAsync)({
    jsonrpc: "2.0",
    method,
    params,
    id: Date.now()
  })).result;

const traceTransaction = rpcCommand("debug_traceTransaction");

Sobald Sie all das haben, können Sie etwas bekommen, das einem klassischen Stracktrace ähnelt:

const trace = await traceTransaction(result.tx);
trace.structLogs.forEach(({op, pc, gasCost}) =>
  console.log(
    `${pc}\t${op}\t${gasCost}\t${byteToInstr[pc]}\t${parsed[
      byteToInstr[pc]
    ]}`
  )
);

Ich hoffe, dass ich das bald aufräumen und in eine Bibliothek verwandeln kann. Die Fähigkeit, Spuren zu handhaben und sie zurück auf die Solidität abzubilden, hat viele Verwendungsmöglichkeiten.

Danke @Remco, du hast eine großartige Arbeit geleistet, diese Beiträge erklären viel. Meiner Meinung nach ist das Fehlen einer Lösung für das fragliche Problem ein großer Mangel an Ethereum-Infrastruktur. Ich hoffe es kommt bald eine Lösung.
Es ist mehr als 1 Jahr her, seit dies gepostet wurde. Irgendwelche Updates zu besseren Tools?
Tolle Arbeit @Remco. Ich habe ein Vertragsbeispiel, in dem es ungefähr 100 Opcodes gibt, aber sourceMap hat nur ungefähr 15 ";". Ich dachte, sourceMap-Einträge sollten gleich der Anzahl der Anweisungen im Opcode sein! Übersehe ich etwas, vielleicht Kompression? Thx im Voraus!
Update 2021: Der Remix-Debugger verarbeitet Stacktraces jetzt ziemlich gut.
@JuanIgnacioPérezSacristán Es gibt Komprimierung, ja. docs.soliditylang.org/en/v0.7.5/internals/source_mappings.html

Wenn Sie den Quellcode des Vertrags erhalten können, können Sie Hardhat verwenden , um die Solidity-Stack-Traces abzurufen. Hardhat Network ist eine Debugging-First-EVM-Implementierung, die für die Low-Level-Entwicklung von Smart Contracts entwickelt wurde.

Schamloser Stecker: Beginnen Sie mit meiner Solidity-Vorlage, die Hardhat verwendet: https://github.com/paulrberg/solidity-template :

Randnotiz: Siehe die Ankündigung von Nomic Labs .