Terminating VM with exit code 33. Debugging this error

wonder

In this article I will try to debug and understand and error that I’ve encountered a couple of times, while sending transactions in the Telegram Open Network. I’ll describe you a little bit my steps for debugging this weird error, maybe is useful for someone else who faced the same issue. As always I’ll put a way to avoid this error. Let’s start.

TL;DR

The error is due to the seqno has been sent when making a transaction does not match the current seqno in the wallet smart contract. For more details, checkout the whole article.

Requirement

Some knowledge of FunC and patience to read assembly of the TVM :).

Context

I encountered this errors while trying to send several transactions to different Anonymous Numbers which I own. The transactions were meant to put all those numbers into auction in Fragment Marketplace. The error is not tied to the telemint contract itself. Let me describe you the error so you get a better grasp.

Error

The error logs that you will receive could be similar to this one


panic: failed to send message: lite server error, code 0: cannot apply external message to current state : External message was not accepted
Cannot run message on account: inbound external message rejected by transaction DCEFC589BFF751F2165B3381BB72552B36186CFDBAF06885BF4CE07BA677ECD0:
exitcode=33, steps=23, gas_used=0
VM Log (truncated):
...te NOW
execute LEQ
execute THROWIF 36
execute PUSH c4
execute CTOS
execute LDU 32
execute LDU 32
execute LDU 256
execute LDDICT
execute ENDS
execute XCPU s4,s3
execute EQUAL
execute THROWIFNOT 33
default exception handler, terminating vm with exit code 33

Now let’s go part by part trying to understand this error log. This message can be divided in two parts:

General description

In this part we can see that the main issue seems to be that the liteserver cannot apply external message to current state. But what does this really means? I’ll try to shed some lights to this, honestly will be all educated guesses because I’m not a core developer of TON blockchain, so no idea how liteservers are implemented.

Let’s remember what is the flow of a transaction when we make use of a library to send this transaction. Should be something like this:


[Library for example tonutils-go] ==> [liteserver] ==> [external message to wallet contract] ==> [from wallet contract to destination address]

From our error message we can guess that the issue is when the liteserver try to send the external message to the wallet contract. This can give us the idea that some check is failing in the wallet contract. Looking at the wallet contract code, you can check the following lines:


  throw_if(36, valid_until <= now());
  var ds = get_data().begin_parse();
  var (stored_seqno, stored_subwallet, public_key, plugins) = (ds~load_uint(32), ds~load_uint(32), ds~load_uint(256), ds~load_dict());
  ds.end_parse();
  throw_unless(33, msg_seqno == stored_seqno);
  

So our error is the 33, which seems to be related to the seqno. To be sure we are in the right path, you can try to analyze the assembly logs and see if it match with these code. You can skip the next part if you don’t want to know all that, can be useful tho, for future errors.

Truncated assembly execution

Let’s try to figure out how would be these instructions in FunC. Let’s break these instructions


execute NOW
execute LEQ
execute THROWIF 36

First we have a NOW instruction which will retrieve the current time, followed by LEQ which according to the TVM paper is just a <= comparison, and at the end a THROWIF 36. This can be put in this way


throw_if(36, some_value <= now());

The second part of the assembly language is as follows


execute PUSH c4
execute CTOS
execute LDU 32
execute LDU 32
execute LDU 256
execute LDDICT
execute ENDS

We start this part with a PUSH C4, here is quite interesting that the C4 register contains the persistent data of the smart contract. Then with PUSH C4 we are just accessing this persistent data of the smart contract.

Next to that we have a CTOS which will convert a cell into a slice followed by LDU, LDDICT and ENDS instructions. Here we can make an educated guess that these part could be something like this.


var ds = get_data().begin_parse()
ds.load_uint(v1, 32)
ds.load_uint(v2, 32)
ds.load_uint(v2, 256) ;; this is an address instead
ds.load_dict()
end_parse()

In general would be the parsing of the c4 register(contract data). The last part would be as follows


execute XCPU s4,s3
execute EQUAL
execute THROWIFNOT 33

In this part we have the XCPU instruction which is equivalent to XCHG s4 followed by a PUSH s3. The key here is to notice that the fourth value of the stack s4 will be exchange with s0(the element in the top now) and later s3 will be pushed in the top of the stack so will end up with s3 and s4 in the top of the stack. Take a look at this sketch, maybe can be helpful in this part

XCPU stack flow

Now having this, the following instruction would be EQUAL which will basically compare if the olds s3 and s4 are equal. In case they are not we will throw an error 33 with THROWIFNOT 33. In FunC this could be written as follows


throw_unless(33, s3 == s4);

Here is the key of our problem, we cannot infer anything else from this code so we have


;; check time
throw_if(36, some_value <= now());

;; parse contract data
var ds = get_data().begin_parse()
ds.load_uint(v1, 32)
ds.load_uint(v2, 32)
ds.load_uint(v2, 256) ;; this is an address
ds.load_dict()
end_parse()

throw_unless(33, s3 == s4);

Partial conclusion

From all this analysis we can assume the issue is that the seqno been sent in the message differs with the one stored in the wallet contract. This could happens in the following scenario, which was my case.

We have two clients sending a message to the wallet contract at the same time, both of them ask for a seqno to the seqno method. The contract returns the stored seqno at that moment.

seqno part 1

Now at this moment both clients have a seqno = 1, they both try to make a request with a seqno = 1. The faster of both will make the contract update the seqno to seqno = 2, so when the slower one will try to send the message the wallet contract will throw the following exception.


throw_unless(33, msg_seqno == stored_seqno);

seqno part 2

Conclusion

Just try to not send two messages with the same seqno 😂. The life of a programmer😂. I spent a hell debugging this error, to realize that I was making a basic error.

Wrong paths

While I was debugging this error I took a wrong path at first instance. I will describe it here, so you might face this issue and is not related to the seqno 😂. The other possibility that I analyzed was the possibility that I was sending a transaction that triggered this error.

action list too long

This was quite complicated because is the code of the ton blockchain, so it gave me a lot of headaches. This error is triggered when amount of output actions pass the limit, which is 255. In case this is your error, I suggest you some links that might be useful, but honestly try to analyze first if it is a seqno issue, because is way more simple.

Links:

  1. Result of tvm execution
  2. Outbound message and output action primitives

Rule of thumb

Go for the simple hypothesis, and apply the Occam’s razor. The problem with this principle is that I always remember it when I tried the hardest path. That’s all folks!