Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
Network & Protocol Guides14 min read

Sui Object Versions and Lamport Ordering: Optimistic Concurrency Over RPC

Learn how Sui versions objects with a Lamport clock, why stale-version transactions abort, and how to build RPC clients that detect version changes before signing.

TL;DR

Every Sui object carries an id, a monotonically increasing u64 version, and a digest. Owned object versions advance on each successful mutation, and a transaction consuming an owned object must reference its current version; otherwise it is rejected deterministically. Sui derives object versions from a logical Lamport clock tied to transaction ordering, making the version an optimistic-concurrency token rather than a wall-clock timestamp. Shared objects are ordered by consensus and their versions advance per consensus commit. This guide shows how to read authoritative versions with sui_getObject and sui_multiGetObjects, compare version and digest before building transactions, verify mutations via sui_getTransactionBlock with showObjectChanges, and design clients that refresh cached versions to avoid aborts.

Sui Object Model: Identity, Version, and Digest

Sui's object model assigns every on-chain object a stable id, a version, and a digest. The id is the object's permanent identity; the version is a u64 that increases monotonically with each successful mutation; the digest is a cryptographic commitment to the object's contents and metadata. The Sui object model documentation defines these fields as the canonical representation of an object's state at a point in the transaction ordering.

Because the digest commits to contents, two objects with the same id and version but different digests cannot exist in a consistent ledger. A client that reads only the version cannot prove the contents match what it expects; it must compare both version and digest before constructing a transaction that consumes the object. This distinction matters when caching object state across RPC calls or between user sessions.

Owned objects and shared objects follow different versioning rules. Owned objects are versioned by the transactions that consume them, while shared objects are ordered by consensus and their versions advance per consensus commit. The Sui JSON-RPC API reference documents the fields returned by sui_getObject, including version, digest, owner, type, and previousTransaction, which together let a client reconstruct an object's recent history.

  • id: permanent object identity, never changes.
  • version: monotonically increasing u64, advances on successful mutation.
  • digest: cryptographic commitment to contents; required for content verification.
  • owner: address, object, or shared; determines ordering path.
  • previousTransaction: the transaction that last mutated the object.

Lamport Ordering and the Optimistic-Concurrency Token

Sui versions owned objects using a logical Lamport clock derived from transaction ordering, not wall-clock time. When a transaction successfully mutates an owned object, the object's version increments to reflect its position in the logical sequence of transactions that touched it. This makes the version an optimistic-concurrency token: a client reads the current version, builds a transaction referencing that version, and submits it. If another transaction has already advanced the version, the submitted transaction is rejected deterministically because the object's version moved on.

This rejection is a feature, not a failure. It prevents double-spends on the same owned object by ensuring that only one transaction can consume a given version. The Lamport model also means version numbers are not globally comparable across objects; each object has its own version sequence. A high version on one object does not imply a high version on another, and version is not a global ledger height.

Shared objects behave differently. They are ordered by consensus, and their versions advance per consensus commit rather than per owned-object transaction. A client interacting with shared objects must account for consensus latency and cannot rely on the same optimistic-concurrency pattern used for owned objects. The Sui JSON-RPC guide covers the RPC surface for both object types.

  • Owned objects: version increments per successful consuming transaction.
  • Shared objects: version advances per consensus commit.
  • Version is per-object, not a global height.
  • Stale-version transactions abort deterministically.

Reading Authoritative Versions with sui_getObject and sui_multiGetObjects

The authoritative way to read an object's current version and digest is sui_getObject. The method accepts an object id and returns fields including version, digest, owner, type, and previousTransaction. A client should treat this response as the source of truth for building a transaction that consumes the object. For batch reads, sui_multiGetObjects accepts an array of object ids and returns an array of responses in the same order, reducing round trips when a transaction touches several objects.

When batching, preserve the mapping between requested ids and returned objects. If an object is missing or has been deleted, the response may omit it or return null depending on the provider; clients should handle both cases explicitly. The Reading Sui objects, dynamic fields, and pagination guide covers pagination and dynamic-field traversal for collections that exceed a single response.

Always compare both version and digest before building a transaction. Version alone tells you the object moved; digest tells you what it moved to. If the digest differs from your cached value but the version matches, your cache is inconsistent and must be refreshed. If the version differs, the object has been mutated and your transaction would abort.

  • sui_getObject: single-object authoritative read.
  • sui_multiGetObjects: batch read, order-preserving.
  • Compare version and digest together before signing.
  • Handle missing or deleted objects explicitly.

Runnable Node.js Example: Fetch, Print, and Detect Version Change

The following Node.js script fetches an object, prints its version and digest, builds a small read plan, and detects a version change on a second read. It uses the standard fetch API available in Node.js 18+ and a configurable RPC endpoint. Replace the endpoint and object id with values from your own environment. This example does not assert any provider-specific latency or rate behavior; it simply demonstrates the read-and-compare pattern.

The script performs two reads separated by a short delay. If the version changes between reads, it logs a warning and exits with a non-zero code, simulating the detection step a client should perform before building a transaction. In production, you would refresh the object state and rebuild the transaction rather than exiting.

const RPC_URL = process.env.SUI_RPC_URL || 'https://fullnode.mainnet.sui.io:443';
const OBJECT_ID = process.env.SUI_OBJECT_ID || '0x2';

async function rpc(method, params) {
  const res = await fetch(RPC_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
  });
  const json = await res.json();
  if (json.error) throw new Error(JSON.stringify(json.error));
  return json.result;
}

async function readObject(id) {
  const result = await rpc('sui_getObject', [id, { showType: true, showOwner: true }]);
  const data = result.data;
  return {
    id,
    version: data.version,
    digest: data.digest,
    owner: data.owner,
    type: data.type,
    previousTransaction: data.previousTransaction
  };
}

async function main() {
  const first = await readObject(OBJECT_ID);
  console.log('First read:', JSON.stringify(first, null, 2));

  const readPlan = [OBJECT_ID];
  const batch = await rpc('sui_multiGetObjects', [readPlan, { showType: true }]);
  console.log('Batch read count:', batch.length);

  await new Promise(r => setTimeout(r, 2000));
  const second = await readObject(OBJECT_ID);
  console.log('Second read:', JSON.stringify(second, null, 2));

  if (first.version !== second.version || first.digest !== second.digest) {
    console.warn('Version or digest changed between reads; refresh before building a transaction.');
    process.exitCode = 1;
  } else {
    console.log('Object state stable across reads.');
  }
}

main().catch(err => { console.error(err); process.exit(1); });

Verifying Mutations Against Effects with sui_getTransactionBlock

After submitting a transaction, verify that the mutation produced the expected new version by reading the transaction's effects. sui_getTransactionBlock with showObjectChanges returns an array of object changes, each including the object id, the change type (created, mutated, deleted, wrapped, unwrapped), and the new version and digest for mutated objects. This lets a client confirm that the version advanced as expected and that the digest matches the new contents.

The Parsing Sui objectChanges and balanceChanges guide covers the full shape of the effects response, including balance changes and gas. When verifying a mutation, compare the new version against the version you referenced in the transaction. A successful transaction should produce a version strictly greater than the consumed version for owned objects. If the version did not advance, the transaction may have been a no-op or the object may have been wrapped rather than mutated.

For clients that cache object state, the effects response is the authoritative signal to invalidate the cache. Do not rely on the transaction digest alone; read the object changes and update your local version and digest for every mutated object. This prevents the next transaction from referencing a stale version.

  • showObjectChanges returns per-object change type and new version.
  • Compare new version against the version consumed by the transaction.
  • Invalidate caches for every mutated object.
  • Use queryTransactionBlocks cursor pagination for historical verification.

Designing a Client That Avoids Stale-State Aborts

A robust Sui RPC client treats object version and digest as a single concurrency token. Before building a transaction, read the object with sui_getObject, store the version and digest, and reference the version in the transaction. After submission, read the effects and update the cached version and digest for every mutated object. If any read between build and submit shows a different version or digest, discard the transaction and rebuild.

For multi-object transactions, read all consumed objects in a single sui_multiGetObjects call to reduce the window between reads. If the transaction consumes both owned and shared objects, remember that shared objects are ordered by consensus and their versions advance per commit; the optimistic-concurrency pattern applies primarily to owned objects. The Sui checkpoint stream and ledger service can provide a consistent view of ledger state for clients that need to align reads with checkpoints.

Caching is the primary source of stale-state aborts. Any cache that stores an object version must be invalidated after any transaction that may have touched the object. This includes transactions submitted by other clients, background jobs, or the same user in a different session. When in doubt, re-read the object before building the transaction rather than trusting a cached version.

  • Treat version + digest as one concurrency token.
  • Batch reads with sui_multiGetObjects to shrink the race window.
  • Invalidate caches after any transaction that may touch the object.
  • Re-read before building rather than trusting cached versions.

Results Table: Measuring Version Behavior Against Your Endpoint

Because provider behavior varies, measure version behavior against your own RPC endpoint rather than assuming numbers. The table below is a template to fill with your own observations. Run the Node.js example above, or a curl equivalent, against your endpoint and record the version and digest for a known object at several points in time. Then submit a transaction that mutates the object and record the new version from the effects response.

Use the table to confirm that version advances monotonically for owned objects, that digest changes when contents change, and that a transaction referencing a stale version is rejected. Do not publish these numbers as universal; they are specific to your endpoint, network, and object. The RPC pricing page describes plan-level considerations, but it does not assert latency or throughput figures.

  • Columns: timestamp, object id, version, digest, previousTransaction, notes.
  • Rows: initial read, second read, post-mutation read, stale-version attempt.
  • Record the exact RPC method and parameters used for each row.
  • Note whether the endpoint is mainnet, testnet, or devnet.

Limitations and Tradeoffs of Version-Based Concurrency

Version is per-object and not a global height. You cannot compare versions across objects to determine which is newer in a global sense. A high version on one object and a low version on another tell you nothing about their relative recency. This limits the usefulness of version as a general-purpose ordering signal outside the object it belongs to.

Digest comparison is required because version alone does not prove contents. Two reads with the same version but different digests indicate an inconsistency that must be resolved before building a transaction. Clients that skip digest comparison may act on stale or corrupted state. Additionally, clients that cache an object version must refresh after any transaction that may have touched it, otherwise they will build transactions that abort. This is the central tradeoff of optimistic concurrency: it avoids locks and coordination but requires careful cache invalidation.

Shared objects introduce further complexity. Their versions advance per consensus commit, and the optimistic-concurrency pattern does not apply in the same way. Clients interacting with shared objects must account for consensus ordering and cannot rely solely on version comparison to detect staleness.

  • Version is per-object, not a global height.
  • Digest comparison is mandatory for content verification.
  • Cached versions must be refreshed after any touching transaction.
  • Shared objects follow consensus ordering, not owned-object versioning.

Troubleshooting Stale-Version and Digest Mismatch Errors

When a transaction aborts with a version-related error, the first step is to re-read the object with sui_getObject and compare the returned version and digest against the values you referenced. If the version differs, another transaction consumed the object first; rebuild the transaction with the new version. If the version matches but the digest differs, your cached contents are inconsistent; discard the cache and re-read.

If the object is missing or returns null, it may have been deleted or wrapped. Check the transaction effects for the object id to determine what happened. If the object was wrapped, it may reappear later with a new version; if deleted, it cannot be consumed. The API service documentation describes how to structure RPC calls for these cases.

For batch reads, verify that the response array length matches the request array length and that each returned object id matches the requested id. Some providers may reorder or omit entries; do not assume positional correspondence without checking. If you see repeated aborts on the same object, consider whether a background process or another client is mutating it concurrently.

  • Re-read with sui_getObject and compare version and digest.
  • Check effects for deleted or wrapped objects.
  • Verify batch response length and id correspondence.
  • Investigate concurrent mutators if aborts repeat.

Next Steps: Integrating Version Checks into Production Clients

To integrate version checks into production, wrap your RPC calls in a helper that reads version and digest, builds the transaction, and verifies the effects. Use the Sui JSON-RPC guide as a reference for method signatures and response shapes. For networks and endpoints, see the Sui networks page. The OnFinality Learn hub collects related guides on Sui RPC patterns.

Consider adding a retry loop that re-reads the object and rebuilds the transaction on version mismatch. Bound the retries to avoid infinite loops under high contention. For clients that need a consistent view across many objects, align reads with checkpoints using the Sui checkpoint stream and ledger service.

Finally, document your cache invalidation policy. Every place that stores an object version should have a clear trigger for refresh. This is the difference between a client that occasionally aborts and one that reliably succeeds under concurrency.

  • Wrap reads and effect verification in a helper.
  • Add bounded retries on version mismatch.
  • Align multi-object reads with checkpoints when consistency matters.
  • Document cache invalidation triggers.

Never Worry about Infrastructure Again

OnFinality takes away the heavy lifting of DevOps so you can build smarter and faster.

Get Started