¿Cómo se puede detectar el estado de la transacción de un error lanzado cuando el gas puede ser exactamente el mismo que el gas utilizado para una transacción exitosa?

Esta pregunta surgió al responder Estado de transacción .

En el siguiente ejemplo, envío gas por 21 000 (la cantidad requerida para una transacción regular). En esta situación gas == gasUsed, ¿no se puede usar para determinar si el contrato ha generado una excepción?

> eth.sendTransaction({from: eth.accounts[0], to: eth.accounts[1], value: web3.toWei(1.2345, "ether"), gas: 21000})
"0xc7c63b67747c0c825229ce3d36d226423adb8cab6bebe12b6d5001e0dc3f79b3"
> eth.getTransaction("0xc7c63b67747c0c825229ce3d36d226423adb8cab6bebe12b6d5001e0dc3f79b3")
{
  blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
  blockNumber: null,
  from: "0xa7857047907d53a2e494d5f311b4b586dc6a96d2",
  gas: 21000,
  gasPrice: 20000000000,
  hash: "0xc7c63b67747c0c825229ce3d36d226423adb8cab6bebe12b6d5001e0dc3f79b3",
  input: "0x",
  nonce: 55,
  to: "0x4d5bbe7fbc80933ffa90ece988a764e41ee6d018",
  transactionIndex: null,
  value: 1234500000000000000
}
> eth.getTransactionReceipt("0xc7c63b67747c0c825229ce3d36d226423adb8cab6bebe12b6d5001e0dc3f79b3")
{
  blockHash: "0xf0af8236ceec7ad1839d67c9934ab062a8d95fa1f88b06139f97dbdfbd1cd842",
  blockNumber: 2234,
  contractAddress: null,
  cumulativeGasUsed: 21000,
  from: "0xa7857047907d53a2e494d5f311b4b586dc6a96d2",
  gasUsed: 21000,
  logs: [],
  root: "3280f47a0de1149ad5c5fda421faaf95f303da8a77e83c8ec6ac2b3d8ca27abc",
  to: "0x4d5bbe7fbc80933ffa90ece988a764e41ee6d018",
  transactionHash: "0xc7c63b67747c0c825229ce3d36d226423adb8cab6bebe12b6d5001e0dc3f79b3",
  transactionIndex: 0
}

Se puede establecer una transacción con el gas exactamente igual al gas que se usará, usando la función web3.eth.estimateGas para estimar primero el gas requerido.

Entonces, ¿cómo se puede detectar el estado de la transacción de un error lanzado cuando el gas puede ser exactamente el mismo que el gas utilizado para una transacción exitosa?



EDIT 15/06/2016 - Explicando las diferencias de ¿Cómo puedo saber cuándo me he quedado sin gasolina programáticamente?

En esta pregunta, ya SABEMOS cómo detectar programáticamente que nos hemos quedado sin gasolina. Y ya sabemos que este método para detectar una transacción con error no es confiable, ya que gas == gasUsedes una condición válida para una transacción sin error.

Lo que hace esta pregunta es qué alternativas confiables existen para detectar una transacción de error cuando la situación de "quedarse sin gasolina" no es confiable.

No es un duplicado en mi punto de vista: consulte EDIT 15/06/2016 arriba.

Respuestas (4)

Resumen

Envié una transacción de prueba (# 1) para averiguar primero cuánto gas usa una transacción de contrato inteligente exitosa. De los resultados de esta transacción, el gas requerido para una transacción exitosa es 26747.

Luego usé esta cantidad de gas de 26747 en mi transacción que iba a ser exitosa (#2). y gasUsed == gas.

Luego uso esta misma cantidad de gas 26747 en mi transacción que va a ser FRACASADA (#3). y gasUsed == gas.

La única forma confiable que he encontrado hasta ahora para verificar fácilmente si una transacción de contrato inteligente es exitosa es usar debug.traceTransactiony verificar el último mensaje de error. Si esto es así, ""entonces no ocurrió ningún error. Si esto es "Out of gas"o "invalid jump destination (PUSH1) 2", entonces ocurrió un error.

Y aquí hay un pequeño código para determinar el estado de su transacción.

> var status = debug.traceTransaction("0xd23219e2ea10528b245deb9e47993cae2ffd3ffe9fc27aeb808e94fc4e75d37b")
undefined
> if (status.structLogs.length > 0) {
  console.log(status.structLogs[status.structLogs.length-1].error)
}
"invalid jump destination (PUSH1) 2"

El valor de retorno anterior será ""si no hay errores o "Out of gas"si te quedas sin gasolina.

ACTUALIZACIÓN 29/07/2016: la geth --fastdescarga de blockchain no contiene la información para debug.traceTransaction(...)mostrar.



Detalles

Estoy usando el siguiente ejemplo para un contrato inteligente que arroja una excepción si _value <12345:

contract TestStatus {
    uint public value;
    function setValue(uint256 _value) {
        value = _value;
        if (_value < 12345) {
            throw;
        }
    }
}

Aplané la fuente a:

var testStatusSource='contract TestStatus { uint public value; function setValue(uint256 _value) { value = _value; if (_value < 12345) { throw; } }}'

Compilé e inserté el contrato en la cadena de bloques:

var testStatusCompiled = web3.eth.compile.solidity(testStatusSource);

var testStatusContract = web3.eth.contract(testStatusCompiled.TestStatus.info.abiDefinition);
var testStatus = testStatusContract.new({
  from:web3.eth.accounts[0], 
  data: testStatusCompiled.TestStatus.code, gas: 1000000}, 
  function(e, contract) {
    if (!e) {
      if (!contract.address) {
        console.log("Contract transaction send: TransactionHash: " + contract.transactionHash + 
          " waiting to be mined...");
      } else {
        console.log("Contract mined! Address: " + contract.address);
        console.log(contract);
      }
  }
})
Contract transaction send: TransactionHash: 0x4de11cc54484333036f45a2441563d6badae43d2e2789e0b113b6703a582a879 waiting to be mined...
undefined
...
Contract mined! Address: 0x87847eb0f944fbb6d5c5a4891e3b103a63cee45c
[object Object]

Envié una transacción que no causará un error para determinar el gas requerido:

# Not an error
testStatus.setValue(123456, eth.accounts[0], {
  from:web3.eth.accounts[0], 
  data: testStatusCompiled.TestStatus.code,
  gas: 41747
});
...
> eth.getTransactionReceipt("0x727cb3c6846bbb05c8437e97aa32db39a252426b09ed837a76bb364748ce73c4")
{
  blockHash: "0x293b4d357d72615c409ba37a6430253d64b4646e0366ac987d1364a258cf81fa",
  blockNumber: 2465,
  contractAddress: null,
  cumulativeGasUsed: 26747,
  from: "0xa7857047907d53a2e494d5f311b4b586dc6a96d2",
  gasUsed: 26747,
  logs: [],
  root: "fbb29f9eff2340aa8d7df7feec74671c2b693fb9e7b6ec3b710ca6d64813da0e",
  to: "0x87847eb0f944fbb6d5c5a4891e3b103a63cee45c",
  transactionHash: "0x727cb3c6846bbb05c8437e97aa32db39a252426b09ed837a76bb364748ce73c4",
  transactionIndex: 0
}

El gas requerido es 26747.

Envié una transacción value=123456que NO fallará y gas==gasUsed:

testStatus.setValue(123456, eth.accounts[0], {
  from:web3.eth.accounts[0], 
  data: testStatusCompiled.TestStatus.code,
  gas: 26747
});
0xc0ab94adfe473811d055f98528a625dbebf66026b7ec5ad8eab939de862bc2d1

Estoy usando debug.traceTransactionpara confirmar que esta transacción no ha encontrado un error.

> var status = debug.traceTransaction("0xc0ab94adfe473811d055f98528a625dbebf66026b7ec5ad8eab939de862bc2d1")
undefined
> status.structLogs[status.structLogs.length-1].error
""

Y en esta situacióngas(26747) == gasUsed(26747)

eth.getTransaction("0xc0ab94adfe473811d055f98528a625dbebf66026b7ec5ad8eab939de862bc2d1")
{
  blockHash: "0x961277e2bebfe8332a23d240672b2658eced493b70fd145c99991c3c8651adcc",
  blockNumber: 2475,
  from: "0xa7857047907d53a2e494d5f311b4b586dc6a96d2",
  gas: 26747,
  gasPrice: 20000000000,
  hash: "0xc0ab94adfe473811d055f98528a625dbebf66026b7ec5ad8eab939de862bc2d1",
  input: "0x55241077000000000000000000000000000000000000000000000000000000000001e240",
  nonce: 66,
  to: "0x87847eb0f944fbb6d5c5a4891e3b103a63cee45c",
  transactionIndex: 0,
  value: 0
}
eth.getTransactionReceipt("0xc0ab94adfe473811d055f98528a625dbebf66026b7ec5ad8eab939de862bc2d1")
{
  blockHash: "0x961277e2bebfe8332a23d240672b2658eced493b70fd145c99991c3c8651adcc",
  blockNumber: 2475,
  contractAddress: null,
  cumulativeGasUsed: 26747,
  from: "0xa7857047907d53a2e494d5f311b4b586dc6a96d2",
  gasUsed: 26747,
  logs: [],
  root: "f850e1e5c99352f47e3409e4e9f966a49c13152e63aaa7bb3765f0b37bfe09e5",
  to: "0x87847eb0f944fbb6d5c5a4891e3b103a63cee45c",
  transactionHash: "0xc0ab94adfe473811d055f98528a625dbebf66026b7ec5ad8eab939de862bc2d1",
  transactionIndex: 0
}

Luego envié una transacción value=123que falló y gas==gasUsed:

testStatus.setValue(123, eth.accounts[0], {
  from:web3.eth.accounts[0], 
  data: testStatusCompiled.TestStatus.code,
  gas: 26747
});

0x9ee86a200528de32a695f1e2dd0d94a3871fefc7e49c5fd24a4a37eab1b99f7a
> var status = debug.traceTransaction("0x9ee86a200528de32a695f1e2dd0d94a3871fefc7e49c5fd24a4a37eab1b99f7a")
undefined
> status.structLogs[status.structLogs.length-1].error
"invalid jump destination (PUSH1) 2"

> eth.getTransaction("0x9ee86a200528de32a695f1e2dd0d94a3871fefc7e49c5fd24a4a37eab1b99f7a")
{
  blockHash: "0x22fb5fcaef27dd017efcde8c3f78df7f5168c505210f5d08872a9c2877146044",
  blockNumber: 2484,
  from: "0xa7857047907d53a2e494d5f311b4b586dc6a96d2",
  gas: 26747,
  gasPrice: 20000000000,
  hash: "0x9ee86a200528de32a695f1e2dd0d94a3871fefc7e49c5fd24a4a37eab1b99f7a",
  input: "0x55241077000000000000000000000000000000000000000000000000000000000000007b",
  nonce: 67,
  to: "0x87847eb0f944fbb6d5c5a4891e3b103a63cee45c",
  transactionIndex: 0,
  value: 0
}
> eth.getTransactionReceipt("0x9ee86a200528de32a695f1e2dd0d94a3871fefc7e49c5fd24a4a37eab1b99f7a")
{
  blockHash: "0x22fb5fcaef27dd017efcde8c3f78df7f5168c505210f5d08872a9c2877146044",
  blockNumber: 2484,
  contractAddress: null,
  cumulativeGasUsed: 26747,
  from: "0xa7857047907d53a2e494d5f311b4b586dc6a96d2",
  gasUsed: 26747,
  logs: [],
  root: "ab51ab81c19eb8da7f8d0216d6c2f1d8946e88842b758ff0af13c28ff7e181b4",
  to: "0x87847eb0f944fbb6d5c5a4891e3b103a63cee45c",
  transactionHash: "0x9ee86a200528de32a695f1e2dd0d94a3871fefc7e49c5fd24a4a37eab1b99f7a",
  transactionIndex: 0
}

Entonces, la única forma fácil y confiable que encontré para determinar si una transacción de contrato inteligente tuvo éxito o falló es usar debug.traceTransaction.

Aquí está el resultado de debug.traceTransactionla última transacción que falló.

debug.traceTransaction("0x9ee86a200528de32a695f1e2dd0d94a3871fefc7e49c5fd24a4a37eab1b99f7a")
{
  gas: 26747,
  returnValue: "",
  structLogs: [{
      depth: 1,
      error: "",
      gas: 5280,
      gasCost: 3,
      memory: null,
      op: "PUSH1",
      pc: 0,
      stack: [],
      storage: {}
  }, {
      depth: 1,
      error: "",
      gas: 5277,
      gasCost: 3,
      memory: null,
      op: "PUSH1",
      pc: 2,
      stack: ["0000000000000000000000000000000000000000000000000000000000000060"],
      storage: {}
  }, {
  ...
  }, {
      depth: 1,
      error: "invalid jump destination (PUSH1) 2",
      gas: 129,
      gasCost: 8,
      memory: ["0000000000000000000000000000000000000000000000000000000000000000", "0000000000000000000000000000000000000000000000000000000000000000", "0000000000000000000000000000000000000000000000000000000000000060"],
      op: "JUMP",
      pc: 66,
      stack: ["0000000000000000000000000000000000000000000000000000000055241077", "0000000000000000000000000000000000000000000000000000000000000022", "000000000000000000000000000000000000000000000000000000000000007b"],
      storage: {
        0000000000000000000000000000000000000000000000000000000000000000: "000000000000000000000000000000000000000000000000000000000000007b"
      }
  }]
}

Y aquí hay un breve script para verificar si la transacción pasó o falló:

> var status = debug.traceTransaction("0x9ee86a200528de32a695f1e2dd0d94a3871fefc7e49c5fd24a4a37eab1b99f7a")
undefined
> status.structLogs[status.structLogs.length-1].error
"invalid jump destination (PUSH1) 2"
Upvoted, agradable, fácil uso de Geth's debug.traceTransaction.
En general, si ((eth.getTransactionReceipt($transaction_id) != null) && (status.structLogs.length == 0) ) , ¿puedo concluir que la transacción se aprobó?
Para una transacción a un contrato, las instrucciones de la máquina virtual del contrato se ejecutarán y los pasos se muestran en status.structLogs, por lo que status.structLogs.length siempre será > 0. Para una transacción a otra cuenta sin contrato, status.structLogs. la longitud será 0.
Buena solución, pero para el entorno de producción, ¿podemos realmente activar la depuración? ¿cuál será el sobrecoste?. Si el nodo local no es el nodo de minería, ¿obtenemos que traceTransaction devuelva algo?
La sobrecarga es que no puede recuperar la debug.traceTransaction(...)información de la parte de la cadena de bloques que se sincroniza rápidamente: la sobrecarga es el espacio en disco y el tiempo de sincronización.
Mi debug.traceTransaction(<transactionID>) devuelve un structLogs vacío: []. ¿Cuál podría ser la razón? @BokkyPooBah
Su nodo debe sincronizarse con la opción de nodo de archivo completo. Pruebe con una transacción reciente y si funciona, pero una transacción anterior no funciona, el problema es que su nodo se ha sincronizado rápidamente.
status.structLogs[status.structLogs.length-1].errordevuelve: "{}". ¿Puedo concluir que la transacción pasó? @BokkyPooBah
Los resultados de debug.traceTransactionpueden haber cambiado. Aquí hay un tx con un error durante la ICO de TokenCard. var status = debug.traceTransaction("0xe686a5fd36ec7921c847b9285c9ea09724bc6065c7374462a448f0d384adad82");. status.structLogs[status.structLogs.length-1].errorregresa {}_ pero status.structLogs[status.structLogs.length-1].opvuelve"Missing opcode 0xfd"
Sí, debug.traceTransaction se ha actualizado. Entonces, como resultado, si status.structLogs[status.structLogs.length-1].op returns=> "Missing opcode 0xfd"¿podría concluir que la transacción se lanzó? Solo quiero tener un identificador para verificar que la transacción se realice. @BokkyPooBah
¿Qué pasa si: op:JUMP and error: {}.. ¿significa también lanzamiento de transacción? @BokkyPooBah
op:RETURN and error: null es una transaccion valida? @BokkyPooBah

Desde el bloque 4370000 (Bizancio), se ha agregado un statusindicador a los recibos.

eth.getTransactionReceipt(transactionHash)devolverá un statuscampo que tiene un valor de 0cuando una transacción ha fallado y 1cuando la transacción ha tenido éxito.

Antes del bloque 4370000

El estado del recibo es nulo y, para determinar si no hay gasolina, la transacción debe procesarse a través del EVM, que es lo que hacen algunos exploradores de bloques .

La falta de gas no se puede detectar estáticamente (sin ejecutar la transacción), en el caso general, debido a las implicaciones del problema de detención .

Incluso en el caso de la transacción más simple que tiene y usa exactamente 21 000 gas, la transacción debe ejecutarse porque el destinatario podría rechazar el Ether que se le envía mediante una función de respaldo. Por lo general, el código de una función de respaldo debe ejecutarse para determinar si evita una excepción de falta de combustible.

gas == gasUsedes una buena heurística práctica para Sin gasolina, dado que es más seguro suministrar una cantidad razonable de gasolina más que web3.eth.estimateGasporque la gasolina no utilizada se reembolsa.

No creo que el problema de la detención se aplique aquí; la transacción ya ha sido ejecutada por la red; podría indicar el motivo de la detención si el protocolo tuviera una disposición al respecto.

Aquí está mi código de Python para verificar esto usando Populus y web3.py :

from web3 import Web3
from populus.utils.transactions import wait_for_transaction_receipt

class TransactionConfirmationError(Exception):
    """A transaction was not correctly included in blockchain."""


def confirm_transaction(web3: Web3, txid: str, timeout=60) -> dict:
    """Make sure a transaction was correctly performed.

    Confirm that

    * The transaction has been mined in blockchain

    * The transaction did not throw an error (used up all its gas)

    http://ethereum.stackexchange.com/q/6007/620

    :raise TransactionConfirmationError: If we did not get it confirmed in time
    :return: Transaction receipt
    """

    try:
        receipt = wait_for_transaction_receipt(web3, txid, timeout)
    except Timeout as e:
        raise TransactionConfirmationError("Could not confirm tx {} within timeout {}".format(txid, timeout)) from e

    tx = web3.eth.getTransaction(txid)

    if tx["gas"] == receipt["gasUsed"]:
        raise TransactionConfirmationError("Transaction failed (out of gas, thrown): {}".format(txid))

    return receipt
¿Por qué no agrega el debug.traceTransaction(...)cheque a su código? Entonces obtendrá el error exacto, y también puede detectar condiciones sin error donde gas==gasUsed ?
Está bien. Actualizará la respuesta para obtener el error exacto.

Personalmente, he estado usando una solución diferente al método traceTransaction. Simplemente consiste en tener un registro de eventos (digamos Completed()) llamado al final de las transacciones (función de contrato). El proceso es entonces:

  • Envía la transacción y obtén el TxHash
  • Inicie un setTimeout (maxtoWait, devolución de llamada)
  • Regístrese en "Completado ()" en esa dirección de contrato de bloque actual a bloque actual + 5 (o más), con devolución de llamada
  • En devolución de llamada,
    • si se llama por tiempo de espera (sin argumentos), entonces tx falló (¡o se supuso que sí!)
    • compruebe si log.transactionHash corresponde a su tx
    • si es así, entonces la transacción tuvo éxito
    • si no, verifique si log.blockNumber >= initialBlock+5, entonces la transacción falló.

Es más complejo, pero no utiliza traceTransaction, que considero una función de depuración en lugar de un entorno de producción.

Esta es una buena manera de hacerlo si tiene acceso al código fuente del contrato inteligente antes de su implementación. En mi trabajo, haciendo contabilidad forense (es decir, contabilidad después del hecho), su método no es posible. Pero, como digo, es una buena manera de hacerlo si tienes el control de la fuente.