Hi there guys, I'm going to describe to you a known issue that you might encounter when you start developing on the TON Blockchain. In this way you could avoid the same mistakes I already made. Is an issue that is documented. Here is the documentation for this special case.
I'll try to explain to you when this error happens, unfortunately at this moment I don't have a full grasp of the reason for this behavior. Anyway, maybe this is useful for you also.
Context
When you use a library to send a message(transaction) into the ton blockchain, the flow of that message, in a simplified way is as follows:
External message flow
What happens behind the scene is that the library, in this case I took tonutils-go as reference, perform the following actions:
- Get fresh block information.
- Retrieve account information, basically information about the wallet from which we are sending the request.
- Build the external message to be sent. Adding in the payload of this message, the message you want to send.
- Send the external message.
- On the wallet contract, in the recv_external, the wallet contract unpack the message in the payload, and send it.
The 4th step, is actually a query to a Liteserver asking it to delivery an external message to your wallet. Your wallet that at the end is a smart contract, process this external message and perform several checks to make sure that is really the owner of the wallet who sent this. This is according to the smart contract of the wallet, you can check out its source code here. More precisely these checks are performed on these lines.
After these checks are performed another instruction takes place, and is the accept_message() instruction
Then is in this line where the wallet contract sends your intended messages in the blockchain.
[Go library] ==> [Liteserver] ==> [wallet smart contract] ==> [destination address]
The problem
Now the issue that you might face, is if you try to send a request like the following
package main
import (
"context"
"strings"
"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/liteclient"
"github.com/xssnick/tonutils-go/tlb"
"github.com/xssnick/tonutils-go/ton"
"github.com/xssnick/tonutils-go/ton/wallet"
"github.com/xssnick/tonutils-go/tvm/cell"
)
const (
MainnetConfig = "https://ton-blockchain.github.io/global.config.json"
WalletVersion = wallet.V4R2
)
func main() {
client := liteclient.NewConnectionPool()
ctx := client.StickyContext(context.Background())
err := client.AddConnectionsFromConfigUrl(ctx, MainnetConfig)
if err != nil {
panic(err)
}
api := ton.NewAPIClient(client)
seed := strings.Split("<SEED>", " ")
w, err := wallet.FromSeed(api, seed, WalletVersion)
if err != nil {
panic(err)
}
addrStr := "EQC9n6aFb2oxQPMTPrHOnZDFcvvC2YLYIgBUms2yAB_LcAtv"
addr, err := address.ParseAddr(addrStr)
if err != nil {
panic(err)
}
payload := cell.BeginCell().MustStoreUInt(0, 32).EndCell()
// if we supply an amount which we don't have
// instead of receiving an error like "insuficient balance blabla..."
// the current behaviour will be that your wallet contract, will
// try to process failed transaction again and again. Like if
// these failed transaction are being replied.
// So is absolutely necessary for the health of your money
// to check your balance before trying to make a transaction
msg := wallet.SimpleMessage(addr, tlb.MustFromTON("1"), payload)
err = w.Send(ctx, msg, true)
if err != nil {
panic(err)
}
}
Which has nothing wrong, the problem will come if you try to send this transaction with a balance you don't have. For example, check out these transactions in tonviewer, that I made to reproduce this issue(feature 😉)
https://tonviewer.com/transaction/513b1d183a3f4b034b11e0bc5159c41ca4dc8ad965139d707df4a3ea7da8494e
Or check the latest transactions of this wallet:
https://tonviewer.com/EQC9n6aFb2oxQPMTPrHOnZDFcvvC2YLYIgBUms2yAB_LcAtv
You will notice that there's a bunch of failed transactions that were sent over and over. Originally I only sent one transaction, with an invalid balance, the others are consequence of this error. Which leads you to permanent loose of funds if you don't stop it on time. I'll tell you how to stop it, I discovered a work around by pure instinct, because I still don't know why this action stops this behavior.
Documentation
As I said before this is a documented behavior. Basically as long as you have an error in ComputePhase and ActionPhase, you will face this issue. I'll cite the doc here bellow so you can get an idea by yourself.
Explanation in the documentation
Note that if after accept_message some error is thrown (both in ComputePhase or ActionPhase) transaction will be written to the blockchain and fees will be deducted from the contract balance, but storage will not be updated and actions will not be applied as in any transaction with an error exit code. As a result, if the contract accepts an external message and then throws an exception due to an error in the message data or sending an incorrectly serialized message, it will pay for processing but have no way of preventing message replay. The same message will be accepted by contract over and over until it consumes the entire balance.
Solution
I discovered this solution by chance really, at the beginning I freaked out to be honest. So the solution is just to send another transaction but this time with a correct amount, according to your balance. Why this stops the replay? Honestly no idea, I'm quite new to blockchain in general and more to the TON Blockchain. Simply right?
The best solution is just to check your balance before making a transaction, to see if you actually have enough balance to do so.
Debugging
I spent quite amount of time debugging this, my knowledge of C++ is zero, I know C but not C++, I know is weird but that's how I am. Even if I knew C++ I possibly couldn't identify the reason, given that is a quite complex code and don't know the ins and outs of the TON Blockchain. Nevertheless I read a lot of C++ code, and could notice that the error is possible triggered in this if statement:
if (err_code == 37 || err_code == 38)
Now this is where the error code it's thrown, but I have no idea how this affects validators processing of these failed transactions. The documentation is not so clear on why this happens, it would be nice if they put a flow explaining why this happens.
Conclusion
Maybe you don't have the why in this article, but I least you know how to stop the behavior. That's all folks!!
Top comments (2)
Think of TON as follows: while external message is valid, it is applied. If wallet's seqno does not change and deadline is not met, the external message gets replayed arbitrarily many times. This is common for any error code.
When the message gets through, it allows wallet's seqno to change. Thus, previous external becomes invalid and is discarded.
Hi there, thanks for clarifying this, when I wrote this I couldn't makes sense of the reason why this happens. Your explanation makes the whole sense.