Overview
This doc covers some key Sui Move programming questions you might face when using Node Service to read and write to the Sui blockchain. Make sure to also:- See our Move Developer Resources doc for a list of helpful developer resources.
- Join the Suinami Riders Telegram group, where developers ask other developers and Sui engineers questions. Searching that channel for an error message can be very fruitful, since others have likely seen it in the past.
Questions
How do I get read-after-write consistency
Problem
Your reads aren’t reflecting the changes made by thesui_executeTransactionBlock
write you just made but you need them to. This could lead to a stale UI for your user who just make a DEx swap or upgraded a game hero NFT. It could also lead to an error on a subsequent transaction - e.g. trying to use a stale object version (SequenceNumber):
Solution
Do the following:- Submit your transaction.
-
Poll with
sui_getTransactionBlock
to determine when the transaction’s effects have been processed the Full node you’re polling. ThewaitForTransactionBlock
method in our and Mysten’s TypeScript SDKs makes this easy, e.g. -
Then, read from the same Full node that’s processed the transaction’s effects by using Shinami sticky-routing. Other Full nodes may not know about the transaction for a couple more seconds as they don’t all process the same transaction checkpoint at the exact same time. There are multiple Full nodes behind Shinami’s Node Service endpoint so we can provide you excellent uptime. For read-after-write consistency, we provide sticky routing, which routes all your requests from the same (
API key
,IP address
) pair to the same Full node. So, if you make asuix_getOwnedObjects
request in step 3 from the same (API key
,IP address
) pair that performed step 2, you can be sure the response will include any effects from the transaction in step 1. For a longer explanation of sticky-routing, with examples, see here
1
Clone the
shinami-examples
github repo.2
cd
into shinami-examples/sui/typescript/backend_examples
and run npm install
to install the dependencies.3
Set the following values in the
src/read_after_write_consistency.ts
file:ALL_SERVICES_TESTNET_ACCESS_KEY
: an Testnet access key with rights to all services : Gas Station, Invisible Wallet, and Node Services.INVISIBLE_WAL_ID
andINVISIBLE_WAL_SECRET
: the (id, secret) pair for an Invisible Wallet you create just for use with the sample code. For more on wallet id and secret pairs, see our Invisible Wallet API doc.OBJECT_TO_TRANSFER_ID
: the id of something owned by the Invisible wallet. An easy way to do this is to run the code once without setting this value - you’ll get an error but also will have the Invisible Wallet’s address printed to the console. Then, use the faucet to send 1 SUI to that address. Find the wallet address in Suivision (make sure you’re on Testnet). Finally, copy the SUI coin’s object id and paste it as the value forOBJECT_TO_TRANSFER_ID
.
4
Save your changes, run
tsc
to transpile the code into JavaScript, and then run node build/read_after_write_consistency.js
to run the resulting code.How should I handle concurrency to avoid equivocation and object version (SequenceNumber) errors
Problem
You need high throughput without equivocation or other errors. Common types of objects where the same version number can unintentionally be used concurrently include gas coins and admin caps (e.g. for minting NFTs). You can see one of two errors because of this: Stale object version (SequenceNumber) This happens when:- Valid Transactions A and B are built with the same owned object id and version number.
- Transaction A is successfully executed by validators.
- Transaction B is submitted.
- Valid Transactions A and B are built with the same owned object id and version number.
- They are submitted to the network at the same time. Different validators accept different transactions (an honest validator will accept the first one it sees and reject the other).
- Neither transaction reaches a 2/3 quorum of validators and the object is locked until the end of the epoch.
Solutions
Gas coins specifically Use Shinami’s Gas Station to sponsor your transactions. We dynamically manage a pool of gas coins for you so you don’t need to worry about these issues. All owned objects in general-
If you’re using a single thread:
- Serialize your transactions that use the same owned object. To ensure you’ll always use the correct, latest object version, follow the solution steps in our section on read-after-write consistency.
- Or, if it makes sense for your use case, consider combining multiple operations using the same owned object into one Programmable Transaction Block. Essentially, here you’re serializing multiple operations within one transaction instead of across multiple transactions.
-
If you’re using two or more threads:
- Create as many versions of the owned-object in question as you have threads. This will ensure that each thread will only have to worry about the version of the object it’s using and can ignore the operations of other threads. For each thread, to ensure you’ll always use the correct, latest version of it’s object, follow the solution steps in our section on read-after-write consistency.
- Or, create a shared object wrapper around the original object, that restricts access to an allowlist of addresses. Every time a transaction needs to access the original object, it’ll need to first obtain an reference from the shared object wrapper in the same programmable transaction block. If you need to transfer the authorization, instead of simply transferring the original owned-object, you will update the allowlist on the shared object wrapper instead. Note that this approach relies on the shared object wrapper to sequentialize all of your transactions, which may result in higher latency and lower throughput compared to using owned objects when implemented properly.
Notes:Err on the safe side: if you don’t get a definite success or failure response from the network for a transaction, assume the transaction might have gone through and do not re-use any of its (
owned objectId
,version
) pairs for different transactions.SequenceNumber(X)... is not available for consumption
).
When you submit a transaction to a Full node, it checks the validity of the transaction with validators. Once the Node gets signatures from a quorum of validators, it knows that the transaction will be successful. However, if two transactions are submitted simultaneously with the same owned object id and version number, it’s possible that neither transaction reaches the necessary quorum of approval from 2/3 of validators. That’s because an honest validator will accept the first valid transaction it sees with the proper, current object version number, and reject any subsequent transactions it sees with the same version number. If a quorum is not reached, this is equivocation, and the owned object is locked until the start of the next epoch when it can be used again. As a result, sending concurrent requests using the same owned object is not safe.
More reading:
Code example
Check out Mysten’s Sui Owned Object Pools (SuiOOP) TypeScript library . It provides a set of tools for managing multiple concurrent transactions on the Sui network to help avoid object equivocation and locking.