21

This question arose from answering Transaction Status.

In the following example, I am sending gas of 21,000 (the amount required for a regular transaction). In this situation gas == gasUsed, and so cannot be used to determine if the contract has thrown an exception?

> 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
}

A transaction can be set with the gas being exactly the same as the gas that will be used, using web3.eth.estimateGas function to firstly estimate the required gas.

So, how can the transaction status from a thrown error be detected when gas can be exactly the same as the gasUsed for a successful transaction?



EDIT 15/06/2016 - Explaining differences from How do I know when I've run out of gas programmatically?

In this question, we already KNOW how to detect that we have run out of gas programmatically. And we already know that this method to detect an error transaction is not reliable, as gas == gasUsed is a valid condition for a non-error transaction.

What this question is asking is what reliable alternatives are there to detect an error transaction when the "run out of gas" situation is not reliable?

2

4 Answers 4

21

Summary

I have sent a test transaction (#1) to firstly find out how much gas a successful smart contract transaction uses. From the results of this transaction, the gas required for a successful transaction is 26747.

I then used this 26747 gas amount in my going-to-be-successful transaction (#2). And gasUsed == gas.

I then use this same 26747 gas amount in my going-to-be-UNSUCCESSFUL transaction (#3). And gasUsed == gas.

The only reliable way I have found so far to easily check if a smart contract transaction is successful is to use debug.traceTransaction and check the last error message. If this is "" then no error occurred. If this is "Out of gas" or "invalid jump destination (PUSH1) 2", then an error occurred.

And here's a short bit of code to determine the status of your transaction.

> 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"

The return value above will be "" if there are no errors, or "Out of gas" if you run out of gas.

UPDATE 29/07/2016 - the geth --fast blockchain download does not contain the information for debug.traceTransaction(...) to display.



Details

I'm using the following example for a smart contract that throws an exception if _value < 12345:

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

I flattened the source to:

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

I compiled and inserted the contract into the blockchain:

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]

I sent a transaction that won't cause an error to determine the gas required:

# 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
}

The gas required is 26747.

I sent a transaction with value=123456 that will NOT fail, and gas==gasUsed:

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

I'm using debug.traceTransaction to confirm that this transaction has not encountered an error.

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

And in this situation gas(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
}

I then sent a transaction with value=123 that failed, and 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
}

So the only easy reliable way I've found to determine if a smart contract transaction has succeeded or failed is to use debug.traceTransaction.

Here's the output of debug.traceTransaction for the last transaction that failed.

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"
      }
  }]
}

And here's a short script to check if the transaction passed or failed:

> var status = debug.traceTransaction("0x9ee86a200528de32a695f1e2dd0d94a3871fefc7e49c5fd24a4a37eab1b99f7a")
undefined
> status.structLogs[status.structLogs.length-1].error
"invalid jump destination (PUSH1) 2"
14
  • 1
    Upvoted, nice, easy use of Geth's debug.traceTransaction.
    – eth
    Jun 14, 2016 at 10:04
  • Overall, if( (eth.getTransactionReceipt($transaction_id) != null) && (status.structLogs.length == 0) ) , then could I conclude that the transaction is passed?
    – alper
    Nov 24, 2016 at 13:54
  • 2
    For a transaction to a contract, the contract vm instructions will execute and the steps are shown in the status.structLogs, so status.structLogs.length will always be > 0. For a transaction to another non-contract account, status.structLogs.length will be 0. Nov 24, 2016 at 15:50
  • Good solution but for production environment can we really activate the debug. what will be the overhead?. If the local node is not the mining node, do we get the traceTransaction return something? Feb 20, 2017 at 5:48
  • 1
11

Since block 4370000 (Byzantium), a status indicator has been added to receipts.

eth.getTransactionReceipt(transactionHash) will return a status field that has a value of 0 when a transaction has failed and 1 when the transaction has succeeded.

Before block 4370000

The receipt status is null, and to ascertain Out of Gas, the transaction must be processed through the EVM, which is what some block explorers do.

Out of Gas cannot be detected statically (without executing the transaction), in the general case, due to implications of the Halting Problem.

Even in the case of the simplest transaction that has and uses exactly 21,000 gas, the transaction needs to be executed because the recipient could reject the Ether being sent to it by using a fallback function. Generally, the code in a fallback function needs to run to determine if it avoids an Out of Gas exception.

gas == gasUsed is a good practical heuristic for Out of Gas, given that it's safer to supply a reasonable amount of gas more than web3.eth.estimateGas since unused gas is refunded.

2
  • I don't think the halting problem applies here; the transaction's already been executed by the network; it could indicate the reason for halting if the protocol had a provision for that. Jun 14, 2016 at 15:00
  • Agree, I asked ethereum.stackexchange.com/questions/6144/…
    – eth
    Jun 16, 2016 at 22:17
4

Here is my Python code to check this using Populus and 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
2
  • 1
    Why don't you add the debug.traceTransaction(...) check to your code - you will then get the exact error, and you can also detect non-error conditions where gas==gasUsed ? Jul 30, 2016 at 23:05
  • 1
    Ok. Will update the answer to the get the exact error. Jul 30, 2016 at 23:28
4

Personally I have been using a different workaround than the traceTransaction method. It simply consists of having an event log (say Completed()) called at the end of the transactions (contract function). The process is then:

  • Send the transaction and get the TxHash
  • Start a setTimeout(maxtoWait, callback)
  • Register to "Completed()" on that contract address from currentBlock to currentBlock+5 (or more), with callback
  • In callback,
    • if called by timeout (no args) then tx failed (or assumed so!)
    • check if log.transactionHash correspond to your tx
    • if yes then the transaction succeded
    • if no, check if log.blockNumber >= initialBlock+5, then transaction failed.

It is more complex but it does not use traceTransaction which I consider being a debugging function rather than a made for production environment.

1
  • This is a good way to do it if you have access to the source code of the smart contract before its deployed. In my work, doing forensic account (i.e. accounting after the fact), your method is not possible. But, as I say, it's a nice way to do it if you have control of the source. Oct 20, 2017 at 14:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.