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. Execute your transaction withWaitForLocalExecution + query the same node you wrote to:
    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.
  2. 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 TypeScript SDK makes this easy).

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

We've created sample code with both solutions 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, and then immediately after - via both a Shinami and a Mysten Full node. The full sample code is on github. The key parts of executing the transaction and then reading the object version from a Full node are shown below. If you want to run the full example, see "Running the full code example" below.

Using Shinami sticky-routing

Note: The signer is an instance of a ShinamiWalletSigner - a class which makes it easy to act on behalf of a Shinami Invisible Wallet. It's using the same API Access key as the shinamiNodeClient.

// 1. Sponsor, sign, and execute the transaction with request_type: "WaitForLocalExecution"
const sponsorSignAndExecuteTxResponse = await signer.executeGaslessTransaction(
  tx,
  { showEffects: true },
  "WaitForLocalExecution"
);

if (sponsorSignAndExecuteTxResponse.effects?.status.status == "success") {
  // 2. Check the object version on the same Shinami node we wrote to, that executed the
  //  transaction locally. This will produce the correct, updated version number.
  const shinamiResponse = await shinamiNodeClient.getObject({id: OBJECT_TO_TRANSFER_ID});
  if (shinamiResponse.data){
    console.log("Shinami sticky-routing read object version right after the transaction: ", shinamiResponse.data.version);
  }

  // 3. Check the object version on a Mysten node (before the transaction
  //  has been checkpointed and synced across all Full nodes). This will 
  //  produce the pre-transaction version number.
  const mystenResponse = await mystenClient.getObject({id: OBJECT_TO_TRANSFER_ID});
  if (mystenResponse.data){
    console.log("Mysten public node read object version right after the transaction:     ", mystenResponse.data.version);
  }

Using polling

Note: The signer is an instance of a ShinamiWalletSigner - a class which makes it easy to act on behalf of a Shinami Invisible Wallet.

// 1. Sponsor, sign, and execute the transaction (transactions default to "WaitForEffectsCert")
const sponsorSignAndExecuteTxResponse = await signer.executeGaslessTransaction(
  tx
); 

// 2. Poll the Shinami Full node until it has a record of the transaction.
//    This will produce the correct, updated version number.
const shinamiResponse = await shinamiNodeClient.waitForTransaction({
  digest: sponsorSignAndExecuteTxResponse.digest,
  options: { showObjectChanges: true }
});
if (shinamiResponse.objectChanges){
  for (var change in shinamiResponse.objectChanges) {
    const objChange: SuiObjectChange = shinamiResponse.objectChanges[change];
    if (objChange.type == "mutated" && objChange.objectId == OBJECT_TO_TRANSFER_ID) {
      console.log("Shinami node object version after waiting for transasction propagation: ", objChange.version);
    } 
  }
}

// 3. Poll the Mysten Full node until it has a record of the transaction.
//    This will produce the correct, updated version number.
const mystenResponse = await mystenClient.waitForTransaction({
  digest: sponsorSignAndExecuteTxResponse.digest,
  options: { showObjectChanges: true }
});
if (mystenResponse.objectChanges) {
  for (var change in mystenResponse.objectChanges) {
    const objChange: SuiObjectChange = mystenResponse.objectChanges[change];
    if (objChange.type == "mutated" && objChange.objectId == OBJECT_TO_TRANSFER_ID) {
      console.log(" Mysten node object version after waiting for transasction propagation: ", objChange.version);
    } 
  }
}

Running the full code 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.

By default, the code runs the first solution (WaitForLocalExecution transaction execution mode + sticky-routing). To run the second solution (WaitForEffectsCert transaction execution mode + polling the Full node), change which function is un-commented in Step 9 to look like this:

//
// 9. Use one of the two methods of read after write
//
// await readAfterWriteConsistencyV1(gaslessTx);
await readAfterWriteConsistencyV2(gaslessTx);

Then, 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.