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 one of following two things:

  1. WaitForEffectsCert + polling the Full node:
    1. Submit your transaction with request_type: 'WaitForEffectsCert' (the default).
    2. Poll the Full node you read from with sui_getTransactionBlock  to determine when the transaction is indexed by the Full node. 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
        });
        
  2. (Deprecated) Execute your transaction withWaitForLocalExecution + query the same node you wrote to
    1. Deprecation notice: Any release now, JSON-RPC nodes will ignore WaitForLocalExecution. In anticipation, recent versions of the Mysten and Shinami SDKs wait for the transaction to propagate by polling the Fullnode if you provide this value. See full details in the description of the request_type parameter in our API doc .
    2. Steps:
      1. Submit your transaction with request_type: 'WaitForLocalExecution' to ask our Full node to process the transaction locally before sending you the SuiTransactionBlockResponse. Note that using this option can increase your request latency. In cases where an immediate, up-to-date blockchain read is not a requirement, see solution 2. 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. Other Full nodes won't know about the transaction for a few seconds (until the transaction is checkpointed and shared). 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 your requests from the same (Shinami API access key, IP address) pair to the same Full node. Make sure your read and write are from the same (Shinami API access key, IP address) pair.

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. Full nodes won't know about the changes until the transaction is check-pointed and shared, which takes a couple seconds. 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.