Common questions

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 the sui_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):

JsonRpcError: Transaction execution failed due to issues with transaction inputs, 
              please review the errors and try again: 
              Object (0x173d5e68dfed7fbd3fc59862e70f0ce1c5b98709b9554457f8b76331db3a5550, 
                      SequenceNumber(27734940), o#facgsR4PEd2AwzETwTr4jjNC3M2U3zRnogNeEx6ygge) 
              is not available for consumption, its current version: SequenceNumber(27735055)
  ...
  code: -32002,
  type: 'ServerError'

or

JsonRpcError: Error checking transaction input objects: 
    ObjectNotFound { object_id: 0x4414ba3a323e5fac4c56cd6d6b1e30ce28cf00fab3549a99b01cb67e8685a030, 
    version: Some(SequenceNumber(27944000)) }
  ...
  code: -32602,
  type: 'InvalidParams'
}

etc.

Solution

Do the following:

  1. Submit your transaction.
  2. Poll with sui_getTransactionBlock  to determine when the transaction's effects have been processed the Full node you're polling. The waitForTransactionBlock method in our and Mysten's TypeScript SDKs makes this easy, e.g.
    1. let txData = await shinamiNodeClient.waitForTransaction({
        digest: SOME_DIGEST,
        options: {
          // the tx options you need, e.g.
          showObjectChanges: true
      });
      
  3. 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 a suix_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

More details

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 and sends it to validators again for execution.

Full nodes - including the node that sent the transaction - won't reflect the transaction's effects when you read from them for another second or two, until the transaction is check-pointed and shared by the validators, and then processed by the full node. Any given full node can be a checkpoint or two behind the latest blockchain state at any given time, which is why these read inconsistencies occur. For more details, see Sui's documentation on the lifecycle of a transaction.

Code example

We've created sample code demonstrating the first solution above that uses the Shinami Clients SDK and Mysten's TypeScript SDK. It's a simple example that uses a Shinami Invisible Wallet. The wallet has a transaction where it transfers an object it owns to itself, bumping the version number. We print the object version number before the transaction, immediately after, and then after polling the node until the transaction has been processed. The full sample code is on github. To run the full example:

  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:
    1. ALL_SERVICES_TESTNET_ACCESS_KEY: an Testnet access key with rights to all services : Gas Station, Invisible Wallet, and Node Services.
    2. INVISIBLE_WAL_ID and INVISIBLE_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.
    3. 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 for OBJECT_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:

  1. Valid Transactions A and B are built with the same owned object id and version number.
  2. Transaction A is successfully executed by validators.
  3. Transaction B is submitted.

Transaction B's stale version number can be because the transactions are built at the same time, or because Transaction A isn't yet reflected in all the Full nodes when B is built. Example error:

JsonRpcError: Transaction execution failed due to issues with transaction inputs, 
              please review the errors and try again: 
              Object (0x173d5e68dfed7fbd3fc59862e70f0ce1c5b98709b9554457f8b76331db3a5550, 
                      SequenceNumber(27734940), o#facgsR4PEd2AwzETwTr4jjNC3M2U3zRnogNeEx6ygge) 
              is not available for consumption, its current version: SequenceNumber(27735055)..
  code: -32002,
  type: 'ServerError'

Equivocation

This happens when:

  1. Valid Transactions A and B are built with the same owned object id and version number.
  2. 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).
  3. Neither transaction reaches a 2/3 quorum of validators and the object is locked until the end of the epoch.

Example error:

JsonRpcError: Failed to sign transaction by a quorum of validators because of locked objects. 
               Retried a conflicting transaction 
               Some(TransactionDigest(9Ny3VnDAGnKaXeXPjASudT4dxU4DqJBvHL1zgDVMyGqb)), success: Some(false)
    at SuiHTTPTransport.request...
  code: -32002,
  type: 'ServerError'
}

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.

Note: 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.

More details

When owned objects are included in transaction blocks, both their id and their version (sequence) number is included in the block. This version number, which gets bumped every time an owned object is in a transaction, ensures that a transaction with stale knowledge isn't allowed to execute. This is what can lead to the stale object version errors (i.e. 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.


Handling the ExecutionCancelledDueToSharedObjectCongestion Error

As of March 10, 2025, Mysten is working on improving their congestion control system to allow more throughput for shared objects and expects this situation to be improved over the next few releases. So, perhaps by the end of April, 2025 (not a firm ETA, just helping to give you a rough expectation of the timeline).

Advice:

  • If you are using a "public" shared object (e.g. trading on deepbook) increasing gas price will get your transaction through. ("increasing to what" is a good question, but if your are sending it at 1x the reference gas price, try 2x, then 3x, and so on). Retrying with the same gas price may also work, it just depends on whether the current congestion is temporary.
  • If you are sending lots of transactions to an object only you use (e.g. in a minting scenario) then you should set your gas budget via dry run estimation. Beyond that, you just have to accept whatever throughput you can get for the time being until their updates roll out.