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 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
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
has 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);
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.
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:
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!
Top comments (0)