Skip to content

# Confidential Assets: Production Readiness - #2

Open
ganymedio wants to merge 58 commits into
mainfrom
confidential-asset-prod
Open

# Confidential Assets: Production Readiness#2
ganymedio wants to merge 58 commits into
mainfrom
confidential-asset-prod

Conversation

@ganymedio

@ganymedio ganymedio commented Mar 27, 2026

Copy link
Copy Markdown
Collaborator

Confidential Assets: Production Readiness

Summary

Upgrades Movement's confidential-asset TS SDK to production readiness by adding proof-replay and key-registration protections, restoring batch range proofs, and pointing the SDK at the framework module address.

Changes

  1. Domain-separated Fiat-Shamir with chain ID + address binding — new src/crypto/fiatShamir.ts. Challenges are SHA2-512 over a DST prefix (MovementConfidentialAsset/<protocol>) followed by chainId, senderAddress, and the remaining public inputs (matching the on-chain new_scalar_from_sha2_512). Binding chainId and the addresses prevents proof replay across chains and transaction contexts. The old genFiatShamirChallenge in helpers.ts (raw concatenation, no domain separation) is deprecated and unused.
  2. Registration proof (ZKPoK of decryption key) — new src/crypto/confidentialRegistration.ts. Registration now requires a Schnorr proof that the registrant controls the decryption key, preventing registration of an encryption key you don't own.
  3. Batch range proofs restored — all four proof types (transfer, withdrawal, normalization, key rotation) use genBatchRangeZKP, matching the on-chain verifier which expects a single batched Bulletproof per component.
  4. Module address is the framework (0x1)DEFAULT_CONFIDENTIAL_COIN_MODULE_ADDRESS and the e2e test-helper default are both 0x1; confidential asset now lives in the framework, not a standalone deploy address.
  5. WASM bindings — pinned to @moveindustries/confidential-asset-wasm-bindings@0.0.5 (package.json plus the WASM URLs in rangeProof.ts and twistedElGamal.ts).

CI & localnet fixes

  • CA E2E workflow pins APTOS_CORE_COMMIT to the aptos-core commit whose framework + Bulletproof DST match the wasm-bindings version (older pins fail with ERANGE_PROOF_VERIFICATION_FAILED). Since confidential_asset now lives at 0x1, the old publish-address extraction was removed.
  • Localnet no longer depends on devnet. src/cli/localNode.ts previously set ENABLE_KEYLESS_DEFAULT=1, which made the node fetch the keyless Groth16 VK from devnet at genesis — a devnet outage crashed every e2e localnet. It now seeds the VK from a checked-in fixture (src/cli/keylessGroth16Vk.json) via INSTALL_KEYLESS_GROTH16_VK_FROM_PATH.
  • Two pre-existing e2e tests fixed, surfaced once the localnet booted again: api/staking.test.ts coerces num_active_delegator (the indexer returns it as a string) before numeric comparison; client/get.test.ts deterministically verifies request overrides against an unreachable host instead of relying on a live network.

Test Plan

Requires a local Movement node from the aptos-core branch that ships the confidential asset framework and the localnet helper script (e.g. movementlabsxyz/aptos-core#299). The script enables feature flag 87 (BULLETPROOFS_BATCH_NATIVES), publishes the framework, and sets the chain auditor (required for transfers under #328).

1. Start the localnet + configure the chain auditor (in aptos-core):

cd ../aptos-core
./scripts/start-localnet-confidential-assets.sh
# Optional: override the default test auditor key
# CHAIN_AUDITOR_EK=0x<pubkey> ./scripts/start-localnet-confidential-assets.sh

2. Run the e2e tests against it (in this repo):

cd confidential-assets
MOVEMENT_NETWORK=local pnpm jest tests/e2e --globalSetup= --globalTeardown=

MOVEMENT_NETWORK=local targets the external node; --globalSetup= --globalTeardown= disables the SDK's own localnet-spawning hooks so it doesn't start a second node. The module address defaults to 0x1, so no extra env var is needed.

Results: the full pnpm jest run is 102 passing, 23 skipped, 0 failing across 8 suites, including all confidential-asset e2e tests (43 across confidentialAsset.test.ts and confidentialAssetTxnBuilder.test.ts). The skipped tests are legacy from a previous version of confidential assets and can be ignored.

Note: the contract now rejects self-transfers (ESELF_TRANSFER), so the transfer e2e tests send to a distinct registered recipient, and there is a dedicated test asserting that a self-transfer aborts.

@ganymedio
ganymedio force-pushed the confidential-asset-prod branch from af8f478 to ac22fc8 Compare March 27, 2026 09:28
@ganymedio
ganymedio marked this pull request as ready for review April 14, 2026 03:04
@ganymedio
ganymedio requested review from musitdev and rubujubi June 16, 2026 11:47
ganymedio added 10 commits June 25, 2026 19:14
Implements the MIP-001 vault envelope layer in @moveindustries/confidential-assets:

- vaultDecryptionKey(dkVault, multisigAddress, tokenMetaAddr): HKDF-SHA512 from
  the shared vault root to dk[Vault, token] (salt "movement-ca-vault/v1"). Shares
  a single HKDF core with keylessDecryptionKey so the two cannot drift.
- sealVaultDk / openVaultDk: per-recipient AES-GCM-256 envelope, keyed by an
  X25519 ECDH (ephemeral <-> recipient's Ed25519 owner key via the RFC 7748
  birational map) and HKDF-SHA256, with AAD binding tag/multisig/dealer/recipient/
  ephemeral key.
- encodeVaultDkRaw / decodeVaultDkRaw: mv-dk-vault-raw-v1: codec for the raw vault
  root (manual-recovery fallback). Coexists with the per-asset mv-dk-v1: codec
  (re-documented, not removed) per the MIP.
- Adds @noble/ciphers for AES-GCM.

Tests pin the vaultDecryptionKey vector, the Ed25519->X25519 agreement, a
byte-exact envelope vector, and a decrypt-only vector, plus negative cases.

Note: the envelope header carries dealerOwnerAddress, which the MIP's AAD/open
require but its envelope diagram omits — see vault.ts for the reconciliation.
The MIP multisig model derives dk[Vault, token] from a shared random
dk[Vault] (vaultDecryptionKey + sealVaultDk/openVaultDk), not from each
proposer's own root material. Drop the now-superseded helpers, which are
not part of the MIP's required derivation surface (spec line 519):
multisigAccountIndexFromAddress, softwareDecryptionKeyDerivationPathForMultisig,
hardwareDecryptionKeyDerivationMessageForMultisig.

The per-asset mv-dk-v1: codec is unaffected and retained (MIP per-asset format).
…pers

Per MIP §"Required SDK Changes" #1 (recommended option: delete). After the
auto-rollover was stripped, these were just withdraw/transfer + an actual-balance
pre-flight check, while the "…WithTotalBalance" name wrongly implied they spend
available + pending. Callers use withdraw / transfer directly; to spend pending,
rolloverPendingBalance first (explicit accept). Also drops the now-unused private
assertSufficientActualBalance helper. No in-repo or wallet callers.
The withdraw/transfer proof builders already decrypt the available balance to
range-prove the remainder, and already threw on an over-spend — but as a plain
Error message string. Replace with a typed InsufficientBalanceError carrying a
stable code 'INSUFFICIENT_BALANCE' (+ available/requested), so callers can
branch programmatically per MIP §"Required SDK Changes" #1. Fail-fast, in the
existing build path (no extra balance round-trip). Pending balance is still not
counted — rolloverPendingBalance remains the explicit accept step.
…/vault-envelope-layer

# Conflicts:
#	confidential-assets/src/crypto/index.ts
feat(ca): multisig vault dk[Vault] envelope layer (MIP-001 §1.4)
Follow-up to 51d1d7c, which replaced the plain "Insufficient balance" Error
message with the typed InsufficientBalanceError (message prefixed
"INSUFFICIENT_BALANCE: ..."). The withdraw/transfer over-balance e2e cases
still asserted the old substring and broke. Match the new named token.
…ount

The envelope's u16-LE recipientCount was decoded via readU16LE but encoded
inline with hand-rolled bit ops. Add a matching writeU16LE helper and use it in
sealVaultDk so the read/write sides are symmetric and self-documenting.
Byte-identical output (the pinned envelope test vector is unchanged).
…elopes

The dk[Vault] envelope encrypted to the birational map of the recipient's
Ed25519 owner key, which opens only with the Ed25519 private scalar — a hardware
wallet never exposes that. Replace the recipient key with a per-owner
vault-envelope key (vek), an X25519 keypair whose private half every backing can
reconstruct locally and whose public half is published + ownership-authenticated
(MIP-001 §"Vault-envelope key").

- Add vaultEnvelopeKeyFromSeed / FromSignature (hardware) / FromPepper (keyless)
  and vaultEnvelopeKeyDerivationPath (software m/44'/637'/{i}'/2'/0'), plus the
  VAULT_ENVELOPE_KEY_DERIVATION_MESSAGE constant.
- Add vaultEnvelopeKeyOwnershipMessage + sign/verifyVaultEnvelopeKeyOwnership
  (Ed25519 over DST ‖ vekPub) so a dealer verifies a published vek_pub against
  the owner's on-chain key before sealing.
- Rework sealVaultDk/openVaultDk to take the X25519 vek directly (no Ed25519
  birational map).
- Regenerate pinned envelope vectors; add vek + ownership tests (52 pass).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant