Common questions

Common questions our customers have when getting started with Sui Move

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 two things:

  1. Submit your transaction with request_type: 'WaitForLocalExecution' to ask our Full Node to process the transaction locally before sending you the SuiTransactionBlockResponse. In cases where an immediate, up-to-date blockchain read is not a requirement, you can use request_type: WaitForEffectsCert to provide slightly lower latency for you and your end users. For more, see the Sui Developer Cheat Sheet.
  2. Wait for the transaction to return successfully. Then, read from the same Full Node you wrote to. Full Nodes you don't submit your write to won't know about the transaction for a few seconds. On Shinami, this means using the same (Shinami API access key, IP address) pair for your write and read:
    1. There are multiple Full Nodes behind Shinami's Node Service endpoint for redundancy so we can provide you excellent uptime. For read-after-write consistency, we provide sticky routing, which routes all requests from the same (Shinami API access key, IP address) pair to the same Full Node.

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. The Node can optionally run the transaction locally before returning the response to your sui_executeTransactionBlock request (ensuring a subsequent read will reflect the changes included in the transaction). Other Full Nodes won't know about the changes until the transaction is check-pointed and shared, which typically takes a few seconds. For more details, see Sui's documentation on the lifecycle of a transaction.

Code example

The code below uses the Shinami Clients SDK and Mysten's TypeScript SDK . It's a simple example that uses a Shinami Invisible Wallet. The wallet owns an NFT and transfers it to itself, bumping the version number. We print out the object version number before the transaction, and then immediately after - via both Shinami's sticky routing and a Mysten public Full Node. You can find a complete end-to-end example of how to create and execute a sponsored transaction with an Invisible Wallet in our TypeScript tutorial. As a spoiler, I'll show you the output first:

NFT object version before transaction:                            25907361
Shinami sticky-route NFT object version right after transaction:  25907362 (correct version)
Mysten NFT object version right after transaction:                25907361 (old version)
import { SuiClient } from '@mysten/sui.js/client';
import { TransactionBlock } from "@mysten/sui.js/transactions";
import { WalletClient, KeyClient, ShinamiWalletSigner, 
        createSuiClient, buildGaslessTransactionBytes } from "@shinami/clients";

// Client to fetch the object version from a Mysten node
const mystenClient = new SuiClient({url: 'https://fullnode.testnet.sui.io'});

// Clients for Shinami's services
const shinamiNodeClient = createSuiClient(TESTNET_NODE_ACCESS_KEY);
const keyClient = new KeyClient(WALLET_ONLY_KEY);
const walletClient = new WalletClient(ALL_SERVICES_TESTNET_ACCESS_KEY);

// Create a signer for the Invisible Wallet operations
const signer = new ShinamiWalletSigner(
  INVISIBLE_WAL_ID,
  walletClient,
  INVISIBLE_WAL_SECRET,
  keyClient
);

// Create the TransactionKind - ready for sponsorship
let txb = new TransactionBlock();
let gaslessPayloadBase64 = await buildGaslessTransactionBytes({
  sui: shinamiNodeClient,
  build: async (txb) => {
    txb.transferObjects([txb.object(NFT_OBJECT_ID)],txb.pure(INVISIBLE_WALLET_ADDRESS));
  }
});

// Check the object version pre-transaction
let nft = await shinamiNodeClient.getObject({id: NFT_OBJECT_ID});
if (nft.data){
  console.log("NFT object version before transaction: ", nft.data.version);
}
// printed: "NFT object version before transaction: 25907361"

// Sponsor, sign, and execute the transaction, using "WaitForLocalExecution"
const sponsorSignAndExecuteResponse = await signer.executeGaslessTransactionBlock(
  gaslessPayloadBase64,
  undefined, // setting gasBudget to undefined triggers our auto-budgeting feature
  { showEffects: true },
  "WaitForLocalExecution"
)

if (sponsorSignAndExecuteResponse.effects?.status.status == "success") {
  // Check the object version on the same Shinami node we wrote to
  nft = await shinamiNodeClient.getObject({id: NFT_OBJECT_ID});
  if (nft.data){
    console.log("Shinami sticky-route nft object version right after transaction: ", nft.data.version);
  }
  // printed: "Shinami sticky-route NFT object version right after transaction: 25907362"
  
  // Check the object version on a Mysten node (before the transaction
  //  has been checkpointed and synced across all Full Nodes)
  nft = await mystenClient.getObject({id: NFT_OBJECT_ID});
  if (nft.data){
    console.log("Mysten nft object version right after transaction: ", nft.data.version);
  }
  // printed: "Mysten NFT object version right after transaction: 25907361" (stale version)
}

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.