Saltearse al contenido

Activo Confidencial (CA)

El Estándar de Activo Confidencial (también conocido como “Confidential Asset” o “CA”) es un protocolo enfocado en privacidad para gestionar Activos Fungibles (FA). Permite a los usuarios realizar transacciones con cantidades ocultas de FA mientras mantiene las direcciones del remitente y destinatario públicamente visibles.

Este estándar permite que cualquier FA sea envuelto en un Activo Confidencial correspondiente, asegurando compatibilidad con tokens existentes. Soporta transferencias de 64-bit y balances de hasta 128 bits.

Las operaciones en balances de Activo Confidencial (balances confidenciales) requieren pruebas de conocimiento cero (ZKPs) para verificar la corrección de la transacción sin revelar cantidades ocultas y otros datos sensibles.

Para cada activo confidencial que un usuario registra, generan un par de claves único:

  • Una clave de encriptación (EK) almacenada en la cadena.
  • Una clave de desencriptación (DK) mantenida de forma segura por el usuario.

Estas claves son independientes y no deben confundirse con las claves de cuenta de Aptos del usuario.

Cada balance confidencial se divide en dos partes:

  • pending_balance - acumula todas las transacciones entrantes.
  • actual_balance - usado exclusivamente para transacciones salientes.

Ambos balances están encriptados con la misma EK del usuario, asegurando que las cantidades subyacentes permanezcan privadas.

El balance confidencial y su clave de encriptación asociada se almacenan en el recurso ConfidentialAssetStore. El ConfidentialAssetStore se instancia para cada activo confidencial que tiene el usuario y es gestionado por el módulo confidential_asset:

struct ConfidentialAssetStore has key {
pending_balance: confidential_balance::CompressedConfidentialBalance,
actual_balance: confidential_balance::CompressedConfidentialBalance,
ek: twisted_elgamal::CompressedPubkey,
// ...
}

Los balances confidenciales manejan cantidades de tokens dividiéndolas en unidades más pequeñas llamadas chunks. Cada chunk representa una porción de la cantidad total y se encripta individualmente usando la EK del usuario. Este diseño asegura una gestión eficiente de los balances.

El balance pendiente consiste en cuatro chunks que contienen todas las transferencias entrantes. Puede manejar hasta 2^16 transferencias de 64-bit antes de requerir un rollover al balance actual. Durante esta acumulación, los chunks del balance pendiente pueden crecer hasta 32 bits cada uno.

El balance actual consiste en ocho chunks, soportando valores de 128-bit. Después de cualquier operación el balance actual debe ser normalizado de vuelta a chunks de 16-bit para mantener una desencriptación eficiente.

La estructura ConfidentialBalance del módulo confidential_balance se usa para representar tanto los balances pendientes como actuales:

struct ConfidentialBalance has drop {
chunks: vector<twisted_elgamal::Ciphertext>,
}

La encriptación involucra:

  • Dividir la cantidad total en chunks de 16-bit.
  • Aplicar la EK del usuario para encriptar cada chunk individualmente.

La desencriptación involucra:

  • Aplicar la DK del usuario para desencriptar cada chunk.
  • Resolver un problema de logaritmo discreto (DL) para cada chunk para recuperar los valores originales.
  • Combinar los valores recuperados para reconstruir la cantidad total.

Normalización asegura que los chunks siempre se reduzcan a tamaños manejables (16 bits). Sin normalización, los chunks pueden crecer demasiado, lo que hace que el proceso de desencriptación (resolución de DL) sea significativamente más lento o incluso impráctico. Este mecanismo se aplica automáticamente al balance actual después de cada operación, asegurando que los usuarios puedan siempre desencriptar sus balances, incluso a medida que los balances crecen a través de múltiples transacciones. Solo después de un rollover, los usuarios son requeridos para normalizar el balance actual manualmente.

El protocolo utiliza encriptación homomórfica, lo que permite operaciones aritméticas en balances confidenciales sin su desencriptación. Esta capacidad es esencial para actualizar el balance pendiente del destinatario durante las transferencias y para rollovers, donde el balance pendiente del usuario se suma al actual.

El diagrama de abajo muestra la relación entre los módulos de Activo Confidencial:

CA Modules Relationship CA Modules Relationship

Los usuarios interactúan con el módulo confidential_asset para realizar operaciones en sus balances confidenciales. El módulo confidential_asset llama al módulo confidential_balance para gestionar los balances confidenciales y el módulo confidential_proof para verificar ZKPs. Debajo del capó, el módulo confidential_balance usa el módulo twisted_elgamal para operaciones en chunks.

public entry fun register(sender: &signer, token: Object<Metadata>, ek: vector<u8>)
#[view]
public fun has_confidential_asset_store(user: address, token: Object<Metadata>): bool

Los usuarios deben registrar un ConfidentialAssetStore para cada token que deseen transaccionar. Como parte de este proceso, los usuarios deben generar un par de claves (EK y DK) en su extremo.

Cuando un ConfidentialAssetStore se registra por primera vez, el balance confidencial se establece en cero, representado como cero chunks cifrados para ambos pending_balance y actual_balance.

También pueden verificar si un usuario tiene un ConfidentialAssetStore para un token específico usando la función has_confidential_asset_store.

#[test_only]
module confidential_asset_example::register_example {
/// ...
fun register(bob: &signer, token: Object<Metadata>) {
let bob_addr = signer::address_of(bob);
// Es una función de prueba, por lo que no necesitamos preocuparnos por la seguridad del par de claves.
let (_bob_dk, bob_ek) = twisted_elgamal::generate_twisted_elgamal_keypair();
let bob_ek = twisted_elgamal::pubkey_to_bytes(&bob_ek);
confidential_asset::register(bob, token, bob_ek);
print(&utf8(b"El balance pendiente de Bob es cero:"));
print(&confidential_asset::pending_balance(bob_addr, token));
print(&utf8(b"El balance actual de Bob es cero:"));
print(&confidential_asset::actual_balance(bob_addr, token));
print(&utf8(b"La clave de encriptación de Bob está configurada:"));
print(&confidential_asset::encryption_key(bob_addr, token));
}
}
public entry fun deposit(sender: &signer, token: Object<Metadata>, amount: u64)
public entry fun deposit_to(sender: &signer, token: Object<Metadata>, to: address, amount: u64)

Las funciones deposit y deposit_to traen tokens al protocolo, transfiriendo la cantidad pasada desde el almacén principal de FA del remitente al balance pendiente del destinatario.

La cantidad en esta función es visible públicamente, ya que agregar nuevos tokens al protocolo requiere una transferencia normal. Sin embargo, los tokens dentro del protocolo se vuelven confidenciales a través de transferencias confidenciales, asegurando la privacidad en las transacciones subsecuentes.

#[test_only]
module confidential_asset_example::deposit_example {
/// ...
fun deposit(bob: &signer, alice: &signer, token: Object<Metadata>) {
let bob_addr = signer::address_of(bob);
let alice_addr = signer::address_of(alice);
// Es una función de prueba, por lo que no necesitamos preocuparnos por la seguridad del par de claves.
let (bob_dk, bob_ek) = twisted_elgamal::generate_twisted_elgamal_keypair();
let (alice_dk, alice_ek) = twisted_elgamal::generate_twisted_elgamal_keypair();
let bob_ek = twisted_elgamal::pubkey_to_bytes(&bob_ek);
let alice_ek = twisted_elgamal::pubkey_to_bytes(&alice_ek);
confidential_asset::register(bob, token, bob_ek);
confidential_asset::register(alice, token, alice_ek);
print(&utf8(b"El balance de FA de Bob antes del depósito es 500:"));
print(&primary_fungible_store::balance(bob_addr, token));
assert!(primary_fungible_store::balance(bob_addr, token) == 500);
let bob_amount = 100;
let alice_amount = 200;
// El balance no está oculto todavía, porque pasamos la cantidad explícitamente a la función.
confidential_asset::deposit(bob, token, bob_amount);
confidential_asset::deposit_to(bob, token, alice_addr, alice_amount);
print(&utf8(b"El balance de FA de Bob después del depósito es 200:"));
print(&primary_fungible_store::balance(bob_addr, token));
assert!(primary_fungible_store::balance(bob_addr, token) == 200);
print(&utf8(b"El balance pendiente de Bob no es cero:"));
print(&confidential_asset::pending_balance(bob_addr, token));
// En el mundo real, no podríamos ver el balance de alguien más ya que requiere
// el conocimiento de la clave de desencriptación.
// La desencriptación del balance requiere resolver el problema de logaritmo discreto,
// por lo que solo verificamos si la cantidad pasada es correcta por simplicidad.
assert!(confidential_asset::verify_pending_balance(bob_addr, token, &bob_dk, bob_amount));
print(&utf8(b"El balance pendiente de Alice no es cero:"));
print(&confidential_asset::pending_balance(alice_addr, token));
assert!(confidential_asset::verify_pending_balance(alice_addr, token, &alice_dk, alice_amount));
}
}
public entry fun rollover_pending_balance(sender: &signer, token: Object<Metadata>)

La función rollover_pending_balance agrega el balance pendiente al actual, restableciendo el balance pendiente a cero. Funciona con ninguna prueba adicional ya que esta función utiliza las propiedades de la Encriptación Homomórfica utilizada en el protocolo.

#[test_only]
module confidential_asset_example::rollover_example {
/// ...
fun rollover(bob: &signer, token: Object<Metadata>) {
let bob_addr = signer::address_of(bob);
// Es una función de prueba, por lo que no necesitamos preocuparnos por la seguridad del par de claves.
let (bob_dk, bob_ek) = twisted_elgamal::generate_twisted_elgamal_keypair();
let bob_ek = twisted_elgamal::pubkey_to_bytes(&bob_ek);
let bob_amount = 100;
confidential_asset::register(bob, token, bob_ek);
confidential_asset::deposit(bob, token, bob_amount);
print(&utf8(b"El balance pendiente de Bob no es cero:"));
print(&confidential_asset::pending_balance(bob_addr, token));
print(&utf8(b"El balance actual de Bob es cero:"));
print(&confidential_asset::actual_balance(bob_addr, token));
assert!(confidential_asset::verify_pending_balance(bob_addr, token, &bob_dk, bob_amount));
assert!(confidential_asset::verify_actual_balance(bob_addr, token, &bob_dk, 0));
// No se requiere normalización explícita, ya que el balance actual ya está normalizado.
assert!(confidential_asset::is_normalized(bob_addr, token));
confidential_asset::rollover_pending_balance(bob, token);
print(&utf8(b"El balance pendiente de Bob es cero:"));
print(&confidential_asset::pending_balance(bob_addr, token));
print(&utf8(b"El balance actual de Bob no es cero:"));
print(&confidential_asset::actual_balance(bob_addr, token));
assert!(confidential_asset::verify_pending_balance(bob_addr, token, &bob_dk, 0));
assert!(confidential_asset::verify_actual_balance(bob_addr, token, &bob_dk, (bob_amount as u128)));
}
}
public entry fun confidential_transfer(
sender: &signer,
token: Object<Metadata>,
to: address,
new_balance: vector<u8>,
sender_amount: vector<u8>,
recipient_amount: vector<u8>,
auditor_eks: vector<u8>,
auditor_amounts: vector<u8>,
zkrp_new_balance: vector<u8>,
zkrp_transfer_amount: vector<u8>,
sigma_proof: vector<u8>)

La función confidential_transfer transfiere tokens del balance actual del remitente al balance pendiente del destinatario. El remitente encripta la cantidad transferida usando la clave de encriptación del destinatario, permitiendo que el balance confidencial del destinatario se actualice homomórficamente.

Para asegurar la transparencia, el remitente también podría encriptar la cantidad transferida usando las EKs de los auditores, permitiendo que los auditores descifren la cantidad transferida en su extremo.

#[test_only]
module confidential_asset_example::transfer_example {
/// ...
fun transfer(bob: &signer, alice: &signer, token: Object<Metadata>) {
let bob_addr = signer::address_of(bob);
let alice_addr = signer::address_of(alice);
// Es una función de prueba, por lo que no necesitamos preocuparnos por la seguridad del par de claves.
let (bob_dk, bob_ek) = twisted_elgamal::generate_twisted_elgamal_keypair();
let (alice_dk, alice_ek) = twisted_elgamal::generate_twisted_elgamal_keypair();
// Nota: Si el auditor específico del activo está configurado, necesitamos incluirlo en el vector `auditor_eks` como el PRIMER elemento.
//
// let asset_auditor_ek = confidential_asset::get_auditor(token);
// let auditor_eks = vector[];
// if (asset_auditor_ek.is_some()) {
// auditor_eks.push_back(asset_auditor_ek.extract());
// };
let (_, auditor_ek) = twisted_elgamal::generate_twisted_elgamal_keypair();
let auditor_eks = vector[auditor_ek];
let bob_ek_bytes = twisted_elgamal::pubkey_to_bytes(&bob_ek);
let alice_ek_bytes = twisted_elgamal::pubkey_to_bytes(&alice_ek);
confidential_asset::register(bob, token, bob_ek_bytes);
confidential_asset::register(alice, token, alice_ek_bytes);
// El balance actual de Bob es 300, y quiere transferir 50 a Alice, cuyo balance es cero.
let bob_current_amount = 300;
let bob_new_amount = 250;
let transfer_amount = 50;
let alice_current_amount = 0;
let alice_new_amount = 50;
confidential_asset::deposit(bob, token, bob_current_amount);
confidential_asset::rollover_pending_balance(bob, token);
print(&utf8(b"El balance actual de Bob es 300"));
assert!(confidential_asset::verify_actual_balance(bob_addr, token, &bob_dk, (bob_current_amount as u128)));
print(&utf8(b"El balance pendiente de Alice es cero"));
assert!(confidential_asset::verify_pending_balance(alice_addr, token, &alice_dk, alice_current_amount));
let current_balance = confidential_balance::decompress_balance(
&confidential_asset::actual_balance(bob_addr, token)
);
let (
proof,
// El nuevo balance es el balance después de la transferencia encriptado con la clave de encriptación del remitente.
// Se establecerá como el nuevo balance actual para el remitente.
new_balance,
// Cantidad transferida encriptada con la clave de encriptación del remitente.
// Usado solo para propósitos de indexación.
sender_amount,
// Cantidad transferida encriptada con la clave de encriptación del destinatario.
// Se agregará homomórficamente al balance pendiente del destinatario.
recipient_amount,
// Cantidad transferida encriptada con las claves de encriptación de los auditores.
// No se almacenará en la cadena, pero un auditor puede descifrar la cantidad transferida con su dk.
auditor_amounts
) = confidential_proof::prove_transfer(
&bob_dk,
&bob_ek,
&alice_ek,
transfer_amount,
bob_new_amount,
&current_balance,
&auditor_eks,
);
let (
sigma_proof,
zkrp_new_balance,
zkrp_transfer_amount
) = confidential_proof::serialize_transfer_proof(&proof);
confidential_asset::confidential_transfer(
bob,
token,
alice_addr,
confidential_balance::balance_to_bytes(&new_balance),
confidential_balance::balance_to_bytes(&sender_amount),
confidential_balance::balance_to_bytes(&recipient_amount),
confidential_asset::serialize_auditor_eks(&auditor_eks),
confidential_asset::serialize_auditor_amounts(&auditor_amounts),
zkrp_new_balance,
zkrp_transfer_amount,
sigma_proof
);
print(&utf8(b"El balance actual de Bob es 250"));
assert!(confidential_asset::verify_actual_balance(bob_addr, token, &bob_dk, bob_new_amount));
print(&utf8(b"El balance pendiente de Alice es 50"));
assert!(confidential_asset::verify_pending_balance(alice_addr, token, &alice_dk, alice_new_amount));
}
}
public entry fun withdraw(
sender: &signer,
token: Object<Metadata>,
amount: u64,
new_balance: vector<u8>,
zkrp_new_balance: vector<u8>,
sigma_proof: vector<u8>)
public entry fun withdraw_to(
sender: &signer,
token: Object<Metadata>,
to: address,
amount: u64,
new_balance: vector<u8>,
zkrp_new_balance: vector<u8>,
sigma_proof: vector<u8>)

Las funciones withdraw y withdraw_to permiten a un usuario retirar tokens del protocolo, transfiriendo la cantidad pasada del balance actual del remitente al almacén principal de FA del destinatario. Esta función permite a los usuarios liberar tokens mientras no revelan sus balances restantes.

#[test_only]
module confidential_asset_example::withdraw_example {
/// ...
fun withdraw(bob: &signer, alice: &signer, token: Object<Metadata>) {
let bob_addr = signer::address_of(bob);
let alice_addr = signer::address_of(alice);
// Es una función de prueba, por lo que no necesitamos preocuparnos por la seguridad del par de claves.
let (bob_dk, bob_ek) = twisted_elgamal::generate_twisted_elgamal_keypair();
let (_alice_dk, alice_ek) = twisted_elgamal::generate_twisted_elgamal_keypair();
let bob_ek_bytes = twisted_elgamal::pubkey_to_bytes(&bob_ek);
let alice_ek_bytes = twisted_elgamal::pubkey_to_bytes(&alice_ek);
confidential_asset::register(bob, token, bob_ek_bytes);
confidential_asset::register(alice, token, alice_ek_bytes);
let bob_current_amount = 500;
let bob_new_amount = 450;
let transfer_amount = 50;
// Bob retira todos los tokens disponibles
confidential_asset::deposit(bob, token, (bob_current_amount as u64));
confidential_asset::rollover_pending_balance(bob, token);
print(&utf8(b"El balance de FA de Alice antes del retiro es cero:"));
print(&primary_fungible_store::balance(alice_addr, token));
assert!(primary_fungible_store::balance(alice_addr, token) == 0);
print(&utf8(b"El balance actual de Bob antes del retiro es 500"));
assert!(confidential_asset::verify_actual_balance(bob_addr, token, &bob_dk, bob_current_amount));
let current_balance = confidential_balance::decompress_balance(
&confidential_asset::actual_balance(bob_addr, token)
);
let (proof, new_balance) = confidential_proof::prove_withdrawal(
&bob_dk,
&bob_ek,
transfer_amount,
bob_new_amount,
&current_balance
);
let new_balance = confidential_balance::balance_to_bytes(&new_balance);
let (sigma_proof, zkrp_new_balance) = confidential_proof::serialize_withdrawal_proof(&proof);
confidential_asset::withdraw_to(
bob,
token,
alice_addr,
transfer_amount,
new_balance,
zkrp_new_balance,
sigma_proof
);
print(&utf8(b"El balance de FA de Alice después del retiro es 50:"));
print(&primary_fungible_store::balance(alice_addr, token));
assert!(primary_fungible_store::balance(alice_addr, token) == 50);
print(&utf8(b"El balance actual de Bob después del retiro es 450"));
assert!(confidential_asset::verify_actual_balance(bob_addr, token, &bob_dk, bob_new_amount));
}
}
public entry fun rotate_encryption_key(
sender: &signer,
token: Object<Metadata>,
new_ek: vector<u8>,
new_balance: vector<u8>,
zkrp_new_balance: vector<u8>,
sigma_proof: vector<u8>)
public entry fun rotate_encryption_key_and_unfreeze(
sender: &signer,
token: Object<Metadata>,
new_ek: vector<u8>,
new_confidential_balance: vector<u8>,
zkrp_new_balance: vector<u8>,
rotate_proof: vector<u8>)
public entry fun rollover_pending_balance_and_freeze(sender: &signer, token: Object<Metadata>)

La función rotate_encryption_key modifica la EK del usuario y re-encripta el balance actual con la nueva EK. Esta función verifica que el balance pendiente sea cero antes de proceder, garantizando que el usuario no pierda fondos durante la rotación.

Para facilitar el proceso de rotación:

  • El balance pendiente debe ser rollado y congelado llamando a rollover_pending_balance_and_freeze. Esto previene que se procesen nuevas transacciones durante la rotación de la clave.
  • Luego, la EK puede ser rotada y descongelada usando rotate_encryption_key_and_unfreeze.
#[test_only]
module confidential_asset_example::rotate_example {
/// ...
fun rotate(bob: &signer, token: Object<Metadata>) {
let bob_addr = signer::address_of(bob);
// Es una función de prueba, por lo que no necesitamos preocuparnos por la seguridad del par de claves.
let (bob_current_dk, bob_current_ek) = twisted_elgamal::generate_twisted_elgamal_keypair();
let (bob_new_dk, bob_new_ek) = twisted_elgamal::generate_twisted_elgamal_keypair();
let bob_current_ek_bytes = twisted_elgamal::pubkey_to_bytes(&bob_current_ek);
let bob_new_ek_bytes = twisted_elgamal::pubkey_to_bytes(&bob_new_ek);
let bob_amount = 100;
confidential_asset::register(bob, token, bob_current_ek_bytes);
confidential_asset::deposit(bob, token, (bob_amount as u64));
// Necesitamos rollover el balance pendiente y congelar el token para prevenir que nuevos depósitos lleguen.
confidential_asset::rollover_pending_balance_and_freeze(bob, token);
print(&utf8(b"La clave de encriptación de Bob antes de la rotación:"));
print(&confidential_asset::encryption_key(bob_addr, token));
assert!(confidential_asset::verify_actual_balance(bob_addr, token, &bob_current_dk, bob_amount));
let current_balance = confidential_balance::decompress_balance(
&confidential_asset::actual_balance(bob_addr, token)
);
let (proof, new_balance) = confidential_proof::prove_rotation(
&bob_current_dk,
&bob_new_dk,
&bob_current_ek,
&bob_new_ek,
bob_amount,
&current_balance
);
let (
sigma_proof,
zkrp_new_balance
) = confidential_proof::serialize_rotation_proof(&proof);
// Después de rotar la clave de encriptación, descongelamos el token para permitir nuevos depósitos.
confidential_asset::rotate_encryption_key_and_unfreeze(
bob,
token,
bob_new_ek_bytes,
confidential_balance::balance_to_bytes(&new_balance),
zkrp_new_balance,
sigma_proof
);
print(&utf8(b"La clave de encriptación de Bob después de la rotación:"));
print(&confidential_asset::encryption_key(bob_addr, token));
// Nota: Aquí usamos la nueva clave de desencriptación para verificar el balance actual.
assert!(confidential_asset::verify_actual_balance(bob_addr, token, &bob_new_dk, bob_amount));
}
}
public entry fun normalize(
sender: &signer,
token: Object<Metadata>,
new_balance: vector<u8>,
zkrp_new_balance: vector<u8>,
sigma_proof: vector<u8>)
public fun is_normalized(user: address, token: Object<Metadata>): bool

La función normalize asegura que el balance actual se reduzca a chunks de 16-bit para desencriptación eficiente. Esto es necesario solo antes de la operación rollover_pending_balance, que requiere que el balance actual esté normalizado antes.

Todas las demás funciones, como withdraw o confidential_transfer, manejan la normalización implícitamente, haciendo que la normalización manual no sea necesaria en esos casos.

#[test_only]
module confidential_asset_example::normalize_example {
/// ...
fun normalize(bob: &signer, token: Object<Metadata>) {
let bob_addr = signer::address_of(bob);
// Es una función de prueba, por lo que no necesitamos preocuparnos por la seguridad del par de claves.
let (bob_dk, bob_ek) = twisted_elgamal::generate_twisted_elgamal_keypair();
let bob_ek_bytes = twisted_elgamal::pubkey_to_bytes(&bob_ek);
let bob_amount = 500;
confidential_asset::register(bob, token, bob_ek_bytes);
confidential_asset::deposit(bob, token, (bob_amount as u64));
// La función rollover es la única función que requiere que el balance actual esté normalizado
// antes y lo deja sin normalizar después, independientemente de cuál fuera el balance pendiente.
confidential_asset::rollover_pending_balance(bob, token);
assert!(!confidential_asset::is_normalized(bob_addr, token));
confidential_asset::deposit(bob, token, (bob_amount as u64));
// Antes de realizar un segundo rollover, el balance actual debe ser normalizado.
// Obtendrá un error si intenta rollover un balance no normalizado:
// confidential_asset::rollover_pending_balance(bob, token);
let current_balance = confidential_balance::decompress_balance(
&confidential_asset::actual_balance(bob_addr, token)
);
let (
proof,
new_balance
) = confidential_proof::prove_normalization(
&bob_dk,
&bob_ek,
bob_amount,
&current_balance
);
let (sigma_proof, zkrp_new_balance) = confidential_proof::serialize_normalization_proof(&proof);
confidential_asset::normalize(
bob,
token,
confidential_balance::balance_to_bytes(&new_balance),
zkrp_new_balance,
sigma_proof
);
assert!(confidential_asset::is_normalized(bob_addr, token));
assert!(confidential_asset::verify_actual_balance(bob_addr, token, &bob_dk, bob_amount));
// Un rollover puede ser realizado una vez que el balance esté normalizado.
// Nota: Las funciones como `withdraw` y `confidential_transfer` no requieren que el balance actual
// esté normalizado antes, ya que las pruebas zk garantizan que el balance actual es normalizado después
// de su ejecución.
confidential_asset::rollover_pending_balance(bob, token);
}
}