VM31 UTXO Vault (SDK)

The VM31 vault is Obelysk's second privacy track. Instead of ElGamal-encrypted balances, it uses a UTXO model with Poseidon2 hashing over the Mersenne-31 (M31) field. This provides stronger correlation resistance by splitting deposits into fixed denominations, making all UTXOs of the same denomination indistinguishable.

How It Works

When you deposit into the VM31 vault, your amount is split into standard denominations (like physical cash). Each denomination becomes a UTXO committed to a Merkle tree using Poseidon2-M31 hashing. Withdrawals and transfers consume UTXOs by revealing nullifiers, without linking back to the original deposit.

A relayer service processes VM31 operations. Your transaction data is encrypted with ECIES before being sent to the relayer, so even the relayer cannot see the details of your operation.

Deposit

const result = await obelysk.vm31.deposit({
  token: 'eth',
  amount: '1.0',
});

// Save all vault notes --- each represents a UTXO
for (const note of result.notes) {
  console.log(`UTXO: ${note.denomination} ${note.token}`);
  console.log(`  Secret: ${note.secret}`);
  console.log(`  Nullifier: ${note.nullifier}`);
}

Save your vault notes securely. Each note is needed to spend the corresponding UTXO.

Withdraw

const result = await obelysk.vm31.withdraw({
  token: 'eth',
  amount: '0.5',
  notes: savedNotes,       // The UTXO notes to spend
  recipient: '0xYOUR_ADDRESS',
});

console.log('Batch ID:', result.batchId);
console.log('Status:', result.status);

Transfer Between UTXOs

Transfer UTXOs to a new owner without withdrawing from the vault. This creates new UTXOs for the recipient while nullifying the old ones.

const result = await obelysk.vm31.transfer({
  token: 'sage',
  amount: '1000',
  notes: savedNotes,
  recipientPublicKey: '0xRECIPIENT_PUB_KEY',
});

console.log('Transfer batch:', result.batchId);

Check Batch Status

VM31 operations are processed in batches by the relayer. You can poll for completion.

const status = await obelysk.vm31.getBatchStatus(result.batchId);

console.log('Status:', status.status);  // 'pending' | 'confirmed' | 'failed'
console.log('Tx hash:', status.transactionHash);

Denomination Splitting

The SDK provides a helper to split any amount into valid denominations. Each token has a predefined set of denominations.

import { splitIntoDenominations, getDenominations } from '@bitsage/sdk/obelysk';

// See available denominations for ETH
const denoms = getDenominations('eth');
console.log('ETH denominations:', denoms);

// Split 1.5 ETH into standard denominations
const splits = splitIntoDenominations('1.5', 'eth');
for (const s of splits) {
  console.log(`${s.count}x ${s.denomination} ETH`);
}

Denomination validation ensures you only create UTXOs with amounts that match the pool's accepted set, preserving the anonymity set.

Bridge to Confidential Transfer

The VM31Bridge connects the UTXO track (Track 2) with the ElGamal-encrypted balance track (Track 1). You can bridge VM31 withdrawals into ConfidentialTransfer balances and vice versa.

// Bridge from VM31 vault to encrypted balance
const bridgeResult = await obelysk.bridge.bridgeToConfidential({
  token: 'eth',
  amount: '0.5',
  notes: savedNotes,
});

console.log('Bridged to CT balance:', bridgeResult.transactionHash);

This is useful when you want to move funds between the two privacy tracks --- for example, depositing via VM31 for maximum correlation resistance, then bridging to an encrypted balance for use in shielded swaps or confidential transfers.

Supported Tokens

TokenAsset IDDenominations
wBTC0Predefined BTC-scale denominations
SAGE1Predefined SAGE-scale denominations
ETH2Predefined ETH-scale denominations
STRK3Predefined STRK-scale denominations
USDC4Predefined USDC-scale denominations

Next Steps