Avoiding Equivocation
On Sui, object versioning ensures that each object is referenced by a unique (ObjectId
, SequenceNumber
) pair, where SequenceNumber
refers to the object's version. Only a single transaction can modify an object at a specific version. After a transaction modifies an object, the version is incremented. Only the latest version can be used in a future transaction.
Object versioning tracks the state of objects across transactions and epochs.
What is equivocation?
Equivocation occurs when an owned object pair (ObjectId
, SequenceNumber
) is used concurrently in multiple non-finalized transactions. Any object used in multiple transactions can become a source of equivocation if not managed correctly.
Equivocation can occur when:
- A user or smart contract submits two transactions using the same (
ObjectId
,SequenceNumber
) before the first is finalized. - Inadvertent logic traps in smart contract code, for example, when handling sponsored transactions.
Even though both transactions aren't finalized, submitting equivocated transactions still causes other problems for both developers and users. For example, when equivocation is detected, the involved objects are locked until the end of the current epoch. Neither transaction can proceed, and the objects cannot be used in any other transaction. Bad actors could intentionally submit equivocated transactions to lock up important objects and intentionally disrupt dApps.
Double spending
Equivocation might also occur when a user attempts to pay for gas with the same coin object across multiple transactions.
Sui's versioning architecture enables the safe reuse of the same gas coin across a series of transactions. Among other processing, the first transaction advances the version number of the coin and returns it to the sender. The next transaction can then use the updated version of that coin to pay for gas. This prevents equivocation because each transaction references a different version of the same coin.
To prevent double spending, validators lock objects as they validate transactions.
Punishment for equivocation
The Sui network has safeguards to punish validators who engage in equivocation. While Sui's object versioning is designed to prevent equivocation, punishment is still necessary. Prevention does not stop users from submitting transactions that attempt equivocation, which lock objects and degrade network usability. Punishing equivocation also discourages both accidental and intentional abuse, motivating developers to ensure their code does not accidentally cause equivocation.
Common pitfalls to avoid
-
For most smart contracts, equivocation is not an intended behavior. A common source of unintentional equivocation comes from multiple transactions performed by the same address. If you don't take care to handle the gas fees properly, you could lock up your smart contract by trying to use the same coin (and version) for more than one transaction.
Recommendation: If using a single thread, serialize transactions that use the same owned object. PTBs allow your transactions to use multiple operations against the same owned object. A PTB is essentially a single, serialized transaction, which can prevent
SequenceNumber
errors.Recommendation: You should always take advantage of the inherent batching that PTBs provide. For example, consider an airdrop scenario where you want to mint and transfer an object to many users. Because PTBs allow up to 1,024 operations in a single PTB, you can mint and transfer the airdrop object to 512 users in a single transaction. This approach is much more cost efficient than looping over 512 individual transactions that mint and transfer to a single user each time. Batching transactions might remove the need for parallel execution, but you must consider the atomic nature of PTBs; if one instruction fails, the whole PTB fails. Consequently, parallel transactions might be preferred for some use cases.
-
Parallel transactions from multiple threads can cause unintentional equivocation errors if not managed properly. One way to avoid owned object equivocation is to create a separate owned object for each transaction thread. This ensures that each thread uses the correct version of its object input.
Recommendation: In cases where creating multiple owned objects is not practical or desired, you can create a wrapper around an object used across threads. The wrapper is a shared object that authorizes access to its object through an allowlist. Any time a transaction needs to access the wrapped object, it gets permission from the wrapper in the same PTB. When authorization needs transferring, the allowlist for the wrapper gets updated accordingly. This approach can create a latency bottleneck as the object wrapper creates sequentialized transactions that rely on its object for input. The Sui TypeScript SDK provides an executor class,
ParallelTransactionExecutor
, to process parallel transactions efficiently.
Sui SDK
The Sui SDK offers transaction executors to help process multiple transactions from the same address.
SerialTransactionExecutor
Use the SerialTransactionExecutor
when processing transactions one after another. The executor grabs all the coins from the sender and combines them into a single coin that is used for all transactions.
Using the SerialTransactionExecutor
prevents SequenceNumber
errors by handling the versioning of object inputs across a PTB.
ParallelTransactionExecutor
Use the ParallelTransactionExecutor
when you want to process transactions with the same sender at the same time. This class creates a pool of gas coins that it manages to ensure parallel transactions don't equivocate those coins. The class tracks objects used across transactions and orders their processing so that the object inputs are also not equivocated.
Debug tools
If you find your smart contracts unintentionally locking objects, there are some tools you can use to help fix the issue.
sui-tool
You can install the sui-tool
(https://github.com/MystenLabs/sui/tree/main/crates/sui-tool) utility and use the locked-object
command to check the locked status of the passed asset on a specific RPC network (--fullnode-rpc-url
value). If you provide an address, locked-object checks if all the gas objects owned by that address are locked. Pass an object ID to check if that specific object is locked.
$ cargo run --bin sui-tool
$ sui-tool locked-object --address 0x02a212de6a9dfa3a69e22387acfbafbb1a9e591bd9d636e7895dcfc8de05f331 --fullnode-rpc-url <https://rpc.mainnet.sui.io:443>
$ sui-tool locked-object --id 0xd4c3ecf5eaa211da58c36495613899e70349f6048baaeca99596f1682e89c837 --fullnode-rpc-url <https://rpc.mainnet.sui.io:443>
Include the --rescue
flag to try and unlock the object the command targets. Rescue is possible if the object isn't already locked by a majority of validators.
Related links
Using the Sui TypeScript SDK, you can create programmable transaction blocks to perform multiple commands in a single transaction.
Versioning provides the ability to upgrade packages and objects on the Sui network.
Sponsored transactions are a primitive on the Sui blockchain that enable the execution of a transaction where you pay the gas fee for your users transactions.
The Sui TypeScript SDK is a modular library of tools for interacting with the Sui blockchain.