En otras palabras, ¿cómo obtener el "trace de pila clásico" de una transacción fallida?
Por ejemplo, tenemos un rastro (no mire "Missing opcode 0xfd" - es la instrucción de reversión) y la fuente de solidez del contrato . ¿Cómo saber en qué línea de la fuente se lanzó la excepción?
Logré ensamblar el contrato (usando solc --asm), pero no hay sugerencias de PC (contador de programa), por lo que no puedo encontrar una línea que corresponda a PC = 557. Además, creo que la optimización se realizó durante la compilación, pero a pesar de eso, el ensamblaje aún se puede leer hasta cierto punto.
Estoy usando solc 0.4.16+commit.d7661dd9.Linux.g++.
Gracias por adelantado.
Bajé por este agujero de conejo y obtuve una prueba de concepto para trabajar al final. No puedo recomendar el viaje. Hay discrepancias de impedancia en muchos niveles, lo que requiere muchas conversiones de formato. Al final, mi implementación aún no maneja llamadas de contratos cruzados. (Parece que no hay forma de averiguar a qué dirección de contrato pertenece un contador de programa en particular, aparte de interpretar las instrucciones de llamada).
Mi implementación es demasiado sucia para compartir, pero los pasos principales son:
1) Necesita solc
producir un mapa fuente en tiempo de ejecución. No puede generar esto directamente, pero puede generarlo como parte de la 'salida json combinada'. Para esto, ejecuta 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) El formato del mapa fuente está comprimido y necesita escribir un decodificador. La especificación de los formatos se encuentra en la documentación de solidez. Ahora tiene una forma de asignar índices de instrucción a compensaciones de origen.
3) no queremos compensaciones de bytes en los archivos fuente, sino números de línea y columna. Para esto, debe analizar los archivos de origen y crear una asignación desde el desplazamiento de bytes hasta los pares de línea/columna. Decidí ignorar esto por ahora y usé el get-line-from-pos
paquete npm.
Los pasos 2 y 3 juntos son:
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) El mapa de origen está en el número de instrucción, pero necesitamos direcciones de bytecode. Para resolver esto, necesitamos construir un mapa desde el bytecode offset hasta el número de instrucción (o al revés). Me resultó más fácil analizar el binario de tiempo de ejecución yo mismo. Todas las instrucciones tienen 1 byte de largo, excepto PUSH_n
las que son n+1
largas.
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;
};
Luego, debe obtener el seguimiento de una transacción determinada:
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");
Una vez que tenga todo eso, puede obtener algo parecido a un stracktrace clásico:
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]
]}`
)
);
Espero poder limpiar esto y convertirlo en una biblioteca pronto. La capacidad de manejar rastros y mapearlos de nuevo a la solidez tiene muchos usos.
Si puede obtener el código fuente del contrato, puede usar Hardhat para obtener los rastros de la pila de Solidity. Hardhat Network es una primera implementación de EVM de depuración, creada para el desarrollo de bajo nivel de contratos inteligentes.
Complemento desvergonzado: comience desde mi plantilla Solidity, que usa Hardhat: https://github.com/paulrberg/solidity-template :
Nota al margen: consulte el anuncio de Nomic Labs .
Zline
Pablo Razvan Berg
Juan Ignacio Pérez Sacristán
Pablo Razvan Berg
Pablo Razvan Berg
david callanan