Terminating VM with exit code 33. Debugging this error
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.
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.
Some knowledge of FunC and patience to read assembly of the TVM :).
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.
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:
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
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
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);
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.
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);
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.
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.
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.
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!