Logo
Nuevos usuarios de RPC: 35% de descuento el primer mesVer oferta
OnFinality Learn
Guías de red y protocolo14 min de lectura

Versiones de objetos en Sui y ordenamiento Lamport: concurrencia optimista sobre RPC

Aprende cómo Sui versiona los objetos con un reloj Lamport, por qué las transacciones con versiones obsoletas se abortan y cómo construir clientes RPC que detecten cambios de versión antes de firmar.

TL;DR

Cada objeto de Sui lleva un id, una versión u64 que aumenta monotónicamente y un digest. Las versiones de los objetos propios avanzan con cada mutación exitosa, y una transacción que consume un objeto propio debe referenciar su versión actual; de lo contrario, se rechaza de forma determinista. Sui deriva las versiones de los objetos de un reloj lógico Lamport vinculado al ordenamiento de transacciones, lo que convierte la versión en un token de concurrencia optimista en lugar de una marca de tiempo de reloj de pared. Los objetos compartidos se ordenan por consenso y sus versiones avanzan por cada confirmación de consenso. Esta guía muestra cómo leer versiones autoritativas con sui_getObject y sui_multiGetObjects, comparar versión y digest antes de construir transacciones, verificar mutaciones mediante sui_getTransactionBlock con showObjectChanges y diseñar clientes que actualicen las versiones en caché para evitar abortos.

Modelo de objetos de Sui: identidad, versión y digest

El modelo de objetos de Sui asigna a cada objeto en cadena un id estable, una versión y un digest. El id es la identidad permanente del objeto; la versión es un u64 que aumenta monotónicamente con cada mutación exitosa; el digest es un compromiso criptográfico con el contenido y los metadatos del objeto. La documentación del modelo de objetos de Sui define estos campos como la representación canónica del estado de un objeto en un punto del ordenamiento de transacciones.

Dado que el digest compromete el contenido, no pueden existir dos objetos con el mismo id y versión pero distinto digest en un libro contable consistente. Un cliente que solo lee la versión no puede probar que el contenido coincide con lo que espera; debe comparar tanto la versión como el digest antes de construir una transacción que consuma el objeto. Esta distinción importa al almacenar en caché el estado de un objeto entre llamadas RPC o entre sesiones de usuario.

Los objetos propios y los objetos compartidos siguen reglas de versionado diferentes. Los objetos propios se versionan según las transacciones que los consumen, mientras que los objetos compartidos se ordenan por consenso y sus versiones avanzan por cada confirmación de consenso. La referencia de la API JSON-RPC de Sui documenta los campos que devuelve sui_getObject, incluidos version, digest, owner, type y previousTransaction, que en conjunto permiten a un cliente reconstruir el historial reciente de un objeto.

  • id: identidad permanente del objeto, nunca cambia.
  • version: u64 que aumenta monotónicamente, avanza con cada mutación exitosa.
  • digest: compromiso criptográfico con el contenido; necesario para verificar el contenido.
  • owner: dirección, objeto o compartido; determina la ruta de ordenamiento.
  • previousTransaction: la transacción que mutó el objeto por última vez.

Ordenamiento Lamport y el token de concurrencia optimista

Sui versiona los objetos propios usando un reloj lógico Lamport derivado del ordenamiento de transacciones, no del tiempo de reloj de pared. Cuando una transacción muta con éxito un objeto propio, la versión del objeto se incrementa para reflejar su posición en la secuencia lógica de transacciones que lo tocaron. Esto convierte la versión en un token de concurrencia optimista: un cliente lee la versión actual, construye una transacción que referencia esa versión y la envía. Si otra transacción ya ha avanzado la versión, la transacción enviada se rechaza de forma determinista porque la versión del objeto ya cambió.

Este rechazo es una característica, no un fallo. Evita el doble gasto sobre el mismo objeto propio al garantizar que solo una transacción pueda consumir una versión dada. El modelo Lamport también implica que los números de versión no son comparables globalmente entre objetos; cada objeto tiene su propia secuencia de versiones. Una versión alta en un objeto no implica una versión alta en otro, y la versión no es una altura global del libro contable.

Los objetos compartidos se comportan de manera diferente. Se ordenan por consenso y sus versiones avanzan por cada confirmación de consenso en lugar de por cada transacción de objeto propio. Un cliente que interactúa con objetos compartidos debe tener en cuenta la latencia del consenso y no puede confiar en el mismo patrón de concurrencia optimista que se usa para objetos propios. La guía JSON-RPC de Sui cubre la superficie RPC para ambos tipos de objetos.

  • Objetos propios: la versión se incrementa por cada transacción consumidora exitosa.
  • Objetos compartidos: la versión avanza por cada confirmación de consenso.
  • La versión es por objeto, no una altura global.
  • Las transacciones con versión obsoleta se abortan de forma determinista.

Leer versiones autoritativas con sui_getObject y sui_multiGetObjects

La forma autoritativa de leer la versión y el digest actuales de un objeto es sui_getObject. El método acepta un id de objeto y devuelve campos como version, digest, owner, type y previousTransaction. Un cliente debe tratar esta respuesta como la fuente de verdad para construir una transacción que consuma el objeto. Para lecturas por lotes, sui_multiGetObjects acepta un arreglo de ids de objetos y devuelve un arreglo de respuestas en el mismo orden, lo que reduce los viajes de ida y vuelta cuando una transacción toca varios objetos.

Al hacer lecturas por lotes, conserva la correspondencia entre los ids solicitados y los objetos devueltos. Si un objeto falta o ha sido eliminado, la respuesta puede omitirlo o devolver null según el proveedor; los clientes deben manejar ambos casos explícitamente. La guía Leer objetos de Sui, campos dinámicos y paginación cubre la paginación y el recorrido de campos dinámicos para colecciones que exceden una sola respuesta.

Compara siempre tanto la versión como el digest antes de construir una transacción. La versión por sí sola te dice que el objeto se movió; el digest te dice a qué se movió. Si el digest difiere de tu valor en caché pero la versión coincide, tu caché es inconsistente y debe actualizarse. Si la versión difiere, el objeto ha sido mutado y tu transacción se abortaría.

  • sui_getObject: lectura autoritativa de un solo objeto.
  • sui_multiGetObjects: lectura por lotes que preserva el orden.
  • Compara versión y digest juntos antes de firmar.
  • Maneja explícitamente los objetos faltantes o eliminados.

Ejemplo ejecutable en Node.js: obtener, imprimir y detectar cambios de versión

El siguiente script de Node.js obtiene un objeto, imprime su versión y digest, construye un pequeño plan de lectura y detecta un cambio de versión en una segunda lectura. Usa la API fetch estándar disponible en Node.js 18+ y un endpoint RPC configurable. Reemplaza el endpoint y el id del objeto con valores de tu propio entorno. Este ejemplo no afirma ningún comportamiento de latencia o tasa específico del proveedor; simplemente demuestra el patrón de leer y comparar.

El script realiza dos lecturas separadas por un breve retardo. Si la versión cambia entre lecturas, registra una advertencia y sale con un código distinto de cero, simulando el paso de detección que un cliente debe realizar antes de construir una transacción. En producción, actualizarías el estado del objeto y reconstruirías la transacción en lugar de salir.

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); });

Verificar mutaciones contra los efectos con sui_getTransactionBlock

Después de enviar una transacción, verifica que la mutación produjo la nueva versión esperada leyendo los efectos de la transacción. sui_getTransactionBlock con showObjectChanges devuelve un arreglo de cambios de objetos, cada uno con el id del objeto, el tipo de cambio (created, mutated, deleted, wrapped, unwrapped) y la nueva versión y digest para los objetos mutados. Esto permite a un cliente confirmar que la versión avanzó como se esperaba y que el digest coincide con el nuevo contenido.

La guía Analizar objectChanges y balanceChanges de Sui cubre la forma completa de la respuesta de efectos, incluidos los cambios de saldo y el gas. Al verificar una mutación, compara la nueva versión con la versión que referenciaste en la transacción. Una transacción exitosa debería producir una versión estrictamente mayor que la versión consumida para objetos propios. Si la versión no avanzó, la transacción puede haber sido una operación nula o el objeto puede haber sido envuelto en lugar de mutado.

Para los clientes que almacenan en caché el estado de los objetos, la respuesta de efectos es la señal autoritativa para invalidar la caché. No confíes solo en el digest de la transacción; lee los cambios de objetos y actualiza tu versión y digest locales para cada objeto mutado. Esto evita que la siguiente transacción referencie una versión obsoleta.

  • showObjectChanges devuelve el tipo de cambio por objeto y la nueva versión.
  • Compara la nueva versión con la versión consumida por la transacción.
  • Invalida las cachés de cada objeto mutado.
  • Usa la paginación por cursor de queryTransactionBlocks para verificación histórica.

Diseñar un cliente que evite abortos por estado obsoleto

Un cliente RPC de Sui robusto trata la versión y el digest del objeto como un único token de concurrencia. Antes de construir una transacción, lee el objeto con sui_getObject, almacena la versión y el digest, y referencia la versión en la transacción. Después del envío, lee los efectos y actualiza la versión y el digest en caché para cada objeto mutado. Si alguna lectura entre la construcción y el envío muestra una versión o un digest diferentes, descarta la transacción y vuelve a construirla.

Para transacciones con múltiples objetos, lee todos los objetos consumidos en una sola llamada a sui_multiGetObjects para reducir la ventana entre lecturas. Si la transacción consume tanto objetos propios como compartidos, recuerda que los objetos compartidos se ordenan por consenso y sus versiones avanzan por cada confirmación; el patrón de concurrencia optimista se aplica principalmente a los objetos propios. El flujo de checkpoints y servicio de libro contable de Sui puede proporcionar una vista consistente del estado del libro contable para clientes que necesitan alinear las lecturas con los checkpoints.

El almacenamiento en caché es la principal fuente de abortos por estado obsoleto. Cualquier caché que almacene la versión de un objeto debe invalidarse después de cualquier transacción que pueda haber tocado el objeto. Esto incluye transacciones enviadas por otros clientes, trabajos en segundo plano o el mismo usuario en una sesión diferente. En caso de duda, vuelve a leer el objeto antes de construir la transacción en lugar de confiar en una versión en caché.

  • Trata versión + digest como un solo token de concurrencia.
  • Agrupa las lecturas con sui_multiGetObjects para reducir la ventana de carrera.
  • Invalida las cachés después de cualquier transacción que pueda tocar el objeto.
  • Vuelve a leer antes de construir en lugar de confiar en versiones en caché.

Tabla de resultados: medir el comportamiento de versiones contra tu endpoint

Dado que el comportamiento del proveedor varía, mide el comportamiento de las versiones contra tu propio endpoint RPC en lugar de asumir números. La tabla a continuación es una plantilla para completar con tus propias observaciones. Ejecuta el ejemplo de Node.js anterior, o un equivalente con curl, contra tu endpoint y registra la versión y el digest de un objeto conocido en varios momentos. Luego envía una transacción que mute el objeto y registra la nueva versión de la respuesta de efectos.

Usa la tabla para confirmar que la versión avanza monotónicamente para objetos propios, que el digest cambia cuando cambia el contenido y que una transacción que referencia una versión obsoleta se rechaza. No publiques estos números como universales; son específicos de tu endpoint, red y objeto. La página de precios de RPC describe consideraciones a nivel de plan, pero no afirma cifras de latencia ni de rendimiento.

  • Columnas: marca de tiempo, id de objeto, versión, digest, previousTransaction, notas.
  • Filas: lectura inicial, segunda lectura, lectura posterior a la mutación, intento con versión obsoleta.
  • Registra el método RPC exacto y los parámetros usados para cada fila.
  • Anota si el endpoint es mainnet, testnet o devnet.

Limitaciones y compensaciones de la concurrencia basada en versiones

La versión es por objeto y no una altura global. No puedes comparar versiones entre objetos para determinar cuál es más reciente en un sentido global. Una versión alta en un objeto y una versión baja en otro no te dicen nada sobre su recencia relativa. Esto limita la utilidad de la versión como señal de ordenamiento de propósito general fuera del objeto al que pertenece.

La comparación de digest es necesaria porque la versión por sí sola no prueba el contenido. Dos lecturas con la misma versión pero distinto digest indican una inconsistencia que debe resolverse antes de construir una transacción. Los clientes que omiten la comparación de digest pueden actuar sobre un estado obsoleto o corrupto. Además, los clientes que almacenan en caché la versión de un objeto deben actualizarla después de cualquier transacción que pueda haberlo tocado; de lo contrario, construirán transacciones que se aborten. Esta es la compensación central de la concurrencia optimista: evita bloqueos y coordinación, pero requiere una invalidación de caché cuidadosa.

Los objetos compartidos introducen más complejidad. Sus versiones avanzan por cada confirmación de consenso y el patrón de concurrencia optimista no se aplica de la misma manera. Los clientes que interactúan con objetos compartidos deben tener en cuenta el ordenamiento por consenso y no pueden confiar únicamente en la comparación de versiones para detectar obsolescencia.

  • La versión es por objeto, no una altura global.
  • La comparación de digest es obligatoria para verificar el contenido.
  • Las versiones en caché deben actualizarse después de cualquier transacción que toque el objeto.
  • Los objetos compartidos siguen el ordenamiento por consenso, no el versionado de objetos propios.

Solución de problemas de versión obsoleta y errores de discrepancia de digest

Cuando una transacción se aborta con un error relacionado con la versión, el primer paso es volver a leer el objeto con sui_getObject y comparar la versión y el digest devueltos con los valores que referenciaste. Si la versión difiere, otra transacción consumió el objeto primero; reconstruye la transacción con la nueva versión. Si la versión coincide pero el digest difiere, tu contenido en caché es inconsistente; descarta la caché y vuelve a leer.

Si el objeto falta o devuelve null, puede haber sido eliminado o envuelto. Revisa los efectos de la transacción para el id del objeto para determinar qué ocurrió. Si el objeto fue envuelto, puede reaparecer más tarde con una nueva versión; si fue eliminado, no se puede consumir. La documentación del servicio de API describe cómo estructurar las llamadas RPC para estos casos.

Para lecturas por lotes, verifica que la longitud del arreglo de respuesta coincida con la longitud del arreglo de solicitud y que cada id de objeto devuelto coincida con el id solicitado. Algunos proveedores pueden reordenar u omitir entradas; no asumas correspondencia posicional sin verificarla. Si ves abortos repetidos en el mismo objeto, considera si un proceso en segundo plano u otro cliente lo está mutando de forma concurrente.

  • Vuelve a leer con sui_getObject y compara versión y digest.
  • Revisa los efectos para objetos eliminados o envueltos.
  • Verifica la longitud de la respuesta por lotes y la correspondencia de ids.
  • Investiga mutadores concurrentes si los abortos se repiten.

Próximos pasos: integrar verificaciones de versión en clientes de producción

Para integrar verificaciones de versión en producción, envuelve tus llamadas RPC en un helper que lea la versión y el digest, construya la transacción y verifique los efectos. Usa la guía JSON-RPC de Sui como referencia para las firmas de métodos y las formas de respuesta. Para redes y endpoints, consulta la página de redes de Sui. El centro de aprendizaje de OnFinality reúne guías relacionadas sobre patrones RPC de Sui.

Considera agregar un bucle de reintentos que vuelva a leer el objeto y reconstruya la transacción ante una discrepancia de versión. Limita los reintentos para evitar bucles infinitos bajo alta contención. Para clientes que necesitan una vista consistente de muchos objetos, alinea las lecturas con los checkpoints usando el flujo de checkpoints y servicio de libro contable de Sui.

Por último, documenta tu política de invalidación de caché. Cada lugar que almacene la versión de un objeto debe tener un disparador claro para la actualización. Esta es la diferencia entre un cliente que se aborta ocasionalmente y uno que tiene éxito de forma fiable bajo concurrencia.

  • Envuelve las lecturas y la verificación de efectos en un helper.
  • Agrega reintentos acotados ante discrepancias de versión.
  • Alinea las lecturas de múltiples objetos con los checkpoints cuando la consistencia importe.
  • Documenta los disparadores de invalidación de caché.

Nunca te preocupes por la infraestructura nuevamente

OnFinality elimina la carga pesada de DevOps para que puedas construir de forma más inteligente y rápida.

Comenzar