Skip to content

Confidential Assets: On-Chain Production Readiness - #299

Open
ganymedio wants to merge 48 commits into
m1from
confidential-asset-prod
Open

Confidential Assets: On-Chain Production Readiness#299
ganymedio wants to merge 48 commits into
m1from
confidential-asset-prod

Conversation

@ganymedio

@ganymedio ganymedio commented Mar 27, 2026

Copy link
Copy Markdown

Confidential Assets: On-Chain Production Readiness

Summary

Upgrades Movement's confidential asset modules toward production readiness. Key changes vs m1:

  • Domain-bound Fiat–Shamir transcripts — chain ID, sender, and module address are prepended to every proof transcript via prepend_domain_context, with MovementConfidentialAsset/... DST prefixes
  • Registration proof — Schnorr ZKPoK of the decryption key required on register
  • Transfer auditor hints — optional sender_auditor_hint bound into the transfer sigma transcript and emitted on Transferred, alongside ek_volun_auds
  • Formal verification — Move bytecode model in Lean 4 with 227 differential tests (0 failures), kernel-checked refinement proofs for vector::contains, vector::index_of, std::error, and bit_vector::length, plus registration L0 crypto proofs and L2 bytecode refinement
  • Test coverage — Rust VM e2e tests, expanded Move unit/negative tests, #[test_only] pack helpers
  • Drops aptos-experimental trading subtree (Move 2.2 dependent) for clean CI

243 files changed, +40648 / −5345

What changed

Fiat–Shamir & crypto

All sigma challenges use ristretto255::new_scalar_from_sha2_512(DST || domain_context || public_inputs). prepend_domain_context() prepends chain_id (1 byte), sender (BCS address), and contract_address (BCS address) so proofs are bound to the deployed module, not just the user and chain. MSM gamma scalars use the same SHA2-512 derivation.

Registration proof

verify_registration_proof() performs a standard Schnorr check (s * H + e * ek == R) with the challenge derived from the domain-prefixed transcript. register now requires registration_proof_commitment and registration_proof_response arguments.

Transfer events

confidential_transfer accepts a sender_auditor_hint: vector<u8> that gets hashed into the transfer sigma transcript. The Transferred event now includes both sender_auditor_hint and ek_volun_auds (serialized voluntary-auditor sigma commitments).

API changes

All proof verification and #[test_only] prove_* helpers take chain_id, sender, and contract_address. register gains registration proof vectors. These are breaking changes vs m1.

Package layout

Trading / order-book sources removed from aptos-experimental (Move 2.2 dependencies). Only confidential_* modules remain.

Scripts & localnet

  • scripts/start-localnet-confidential-assets.sh — starts localnet with Docker, enables feature flag 87, optionally publishes aptos-experimental
  • scripts/enable-confidential-assets-feature-87.move — enables BULLETPROOFS_BATCH_NATIVES

Documentation

  • whitepaper.md — Movement confidential-assets design
  • REGISTRATION_VERIFY_REVIEW.md — auditor-facing review of verify_registration_proof
  • CONFIDENTIAL_ASSETS_FORMAL_VERIFICATION_PLAN.md — L0–L5 formal verification roadmap
  • CONFIDENTIAL_ASSETS_DIFFERENTIAL_TESTING_PLAN.md — difftest tiers and coverage plan

How this differs from Aptos

We do not use Aptos's generic sigma-protocol framework (sigma_protocol*.move from their v1.1). We keep per-proof verification in confidential_proof.move, use SHA2-512 with MovementConfidentialAsset/... DSTs and prepend_domain_context for domain separation, and implement registration as inline Schnorr.

Cryptographic primitives

Primitive Reference IP status
SHA2-512 NIST FIPS 180-4 NIST standard, royalty-free
Schnorr ZKPoK Schnorr, J. Cryptology 1991 Public domain
Fiat–Shamir Fiat & Shamir, CRYPTO 1986 Public domain
Ristretto255 RFC 9496 Open standard
Bulletproofs (batch) Bünz et al., IEEE S&P 2018 Patent-free
BCS Diem/Libra, Apache-2.0 Permissive

Test plan

Move unit tests

cd aptos-move/framework/aptos-experimental
movement move test --named-addresses aptos_experimental=0xabcd

Covers registration / withdraw / transfer / normalize / rotate happy paths and wrong-input failures (wrong chain, sender, contract, balances, EKs, auditor lists, sender_auditor_hint).

Rust VM e2e

RUST_MIN_STACK=8388608 cargo test -p e2e-move-tests confidential_asset_e2e -- --nocapture

Full entrypoint tests: register, deposit, rollovers, confidential_transfer, withdraw_to, rotate_encryption_key, set_auditor, voluntary auditors, negatives, gas spot-checks.

TS SDK (cross-repo)

Companion SDK work lives in MoveIndustries/ts-sdk. To test end-to-end on localnet:

# 1. Start localnet (from this repo, with Docker running)
./scripts/start-localnet-confidential-assets.sh

# 2. In MoveIndustries/ts-sdk
pnpm install && cd confidential-assets && pnpm build
CONFIDENTIAL_MODULE_ADDRESS=<published-address-from-above-script-output> MOVEMENT_NETWORK=local pnpm e2e-test

Checklist

  • Move unit tests pass
  • Rust confidential_asset_e2e passes
  • lake build passes (exit code 0)
  • difftest.sh passes (227 passed, 0 failed)
  • TS SDK e2e flow passes on localnet
  • Move and SDK Fiat–Shamir transcripts match for all proof types
  • Registration proof: valid accepts, malformed rejects

musitdev

This comment was marked as outdated.

@ganymedio
ganymedio marked this pull request as ready for review April 14, 2026 03:03

@musitdev musitdev left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About the events, we should do the same as Aptos V1.1. We emit an event for each operation that change the balance (need to add rotate, rollover, normalize) and we put the new changed balances in the event.
We need to put event for each admin operation so we can track them of chain to trigger alerts.

@ganymedio

Copy link
Copy Markdown
Author

About the events, we should do the same as Aptos V1.1. We emit an event for each operation that change the balance (need to add rotate, rollover, normalize) and we put the new changed balances in the event.
We need to put event for each admin operation so we can track them of chain to trigger alerts.

added:

  • Registered — emitted on register
  • Normalized — emitted on normalize, includes new available balance
  • RolledOver — emitted on rollover, includes new available balance
  • KeyRotated — emitted on key rotation, includes new EK and new available balance
  • FreezeChanged — emitted on freeze/unfreeze
  • AllowListChanged — emitted on enable/disable allow list
  • TokenAllowChanged — emitted on enable/disable token
  • AuditorChanged — emitted on set/remove auditor
  • Deposited (enriched) — added new_pending_balance field
  • Withdrawn (enriched) — added new_available_balance field

ganymedio and others added 4 commits April 14, 2026 20:01
…st (#305)

# Formal: Move bytecode model, refinement proofs, Move VM ↔ Lean
difftest

## Summary

Adds a **Lean 4 bytecode interpreter** (`AptosFormal.Move.*`),
**refinement proofs** (including a kernel-checked `vector::contains`
theorem in `Refinement/Vector.lean`), a **Rust ↔ Lean differential
harness** (`move-lean-difftest`) comparing the real Move VM to the Lean
evaluator on shared oracles, and supporting **stdlib vector specs** plus
**`AptosStd`** layout for crypto/hash modules. Workspace `Cargo.toml`
registers the difftest crate.

---

## What changed (concise)

| Area | Notes |
| ---- | ----- |
| **Move semantics** | `Value`, `Instr`, `State`, `Step`, `Native`,
`Programs` (Core + Vector bytecode, including real disassembly-backed
programs). |
| **Refinement** | `Refinement/Core.lean` (`rfl` programs);
`Refinement/Vector.lean` — `vector::contains` vs `Std.Vector.contains`.
|
| **Specs / tests** | `Std/Vector/Operations.lean`; `Tests/Defs`,
`Tests/Vector`. |
| **Difftest** | Crate `move-lean-difftest` (suites: vector, BCS, hash),
JSON oracle, Lean `DiffTest` runner;
[`difftest/README.md`](https://github.com/movementlabsxyz/aptos-core/blob/move-lean/aptos-move/framework/formal/difftest/README.md).
|
| **Layout** | `AptosStd` for Ristretto / SHA3-512; ConfidentialAsset
imports updated. |
| **Docs** |
[`formal/README.md`](https://github.com/movementlabsxyz/aptos-core/blob/move-lean/aptos-move/framework/formal/README.md)
(entry),
[`lean/README.md`](https://github.com/movementlabsxyz/aptos-core/blob/move-lean/aptos-move/framework/formal/lean/README.md)
(full runbook),
[`Move/README.md`](https://github.com/movementlabsxyz/aptos-core/blob/move-lean/aptos-move/framework/formal/lean/AptosFormal/Move/README.md)
(bytecode model + roadmap). |

---

## How to run — Lean proofs

Prerequisites: [elan](https://github.com/leanprover/elan) (Lean 4.24.0 +
Mathlib are pinned; see
[`lean/README.md`](https://github.com/movementlabsxyz/aptos-core/blob/move-lean/aptos-move/framework/formal/lean/README.md)).

```bash
cd aptos-move/framework/formal/lean
lake build
```

Optional: confirm no proof holes:

```bash
cd aptos-move/framework/formal/lean
grep -r "sorry" AptosFormal/ --include="*.lean"
```

(Axiom inventory and registration proofs: still in
[`lean/README.md`](https://github.com/movementlabsxyz/aptos-core/blob/move-lean/aptos-move/framework/formal/lean/README.md).)

---

## How to run — differential tests (VM vs Lean)

**All suites** (from repo root):

```bash
./aptos-move/framework/formal/difftest.sh
```

Per-suite runs, oracle paths, and flags:
**[`difftest/README.md`](https://github.com/movementlabsxyz/aptos-core/blob/move-lean/aptos-move/framework/formal/difftest/README.md)**.

---

## How to run — Move golden tests & byte consistency

Companion Move tests and golden-byte drift check (optional for this PR,
but part of the formal workflow). Commands use the **Movement** CLI
(`movement`), not `aptos`:

```bash
movement move test --package-dir aptos-move/framework/move-stdlib --filter formal_goldens
movement move test --package-dir aptos-move/framework/aptos-experimental --filter formal_goldens
bash aptos-move/framework/formal/check_golden_consistency.sh
```

Details:
[`lean/README.md`](https://github.com/movementlabsxyz/aptos-core/blob/move-lean/aptos-move/framework/formal/lean/README.md)
(sections *Companion Move golden tests* and *Checking Move / Lean golden
consistency*).

---

## Editor

Open the Lake project at **`aptos-move/framework/formal/lean`** for Lean
4 tooling — see
[`lean/README.md`](https://github.com/movementlabsxyz/aptos-core/blob/move-lean/aptos-move/framework/formal/lean/README.md)
(*Editor setup*).
## Summary

Extend the Move stdlib formal verification in Lean 4: add a complete
`vector::index_of` bytecode refinement proof, and add new specification
modules for `error`, `option`, `signer`, `fixed_point32`, and
`bit_vector` with bytecode programs, native models, and refinement
proofs connecting them to the existing Move bytecode interpreter.

## Changes

**`Refinement/Vector.lean` — add `index_of` refinement proof (+927
lines)**

Building on the existing `vector::contains` refinement, adds a full
correctness proof for `vector::index_of` (bytecode index 19 in
`stdModuleEnv`), covering:
- Loop setup (7 steps from `evalProg` to loop header)
- Found path (element match → return `(true, k)`)
- Iteration path (no match → advance to `k+1`)
- Top-level theorems: `vectorIndexOf_returnValues_found` and
`vectorIndexOf_returnValues_notFound`
- Partial `vector::reverse` proof sketch (`sorry`)

Key proof techniques:
- `indexOfLoopFrame` uses `(List.map MoveValue.u64 xs).length.toUInt64`
to match the VM's `step` computation, enabling `rfl` proofs (mirrors the
`contains` pattern)
- `hfail` / `hio` inductive proofs use `suffices` to generalize over the
`indexOf.go` offset parameter, since naive list induction doesn't track
offsets correctly
- `iterN13` outputs `k.toUInt64 + 1` (what the kernel computes) with a
`contains_uint64_succ` conversion in the run chain

**New stdlib specification modules**
- `Std/Error.lean` — canonical error codes and abort-code arithmetic
- `Std/Option.lean` — `option::swap_or_fill` specification and
properties
- `Std/Signer.lean` — signer address extraction
- `Std/FixedPoint32.lean` — fixed-point arithmetic
(`create_from_rational`, `floor`, `ceil`, `round`, `min`, `max`)
- `Std/BitVector.lean` — bit-vector operations (`new`, `set`,
`shift_left`, `is_index_set`)

**Bytecode & native plumbing**
- `Move/Programs/StdPrimitives.lean` — bytecode arrays for
`std::error::canonical` + 13 wrappers and `std::bit_vector::length`,
using the existing instruction set from `Move/Instr.lean`
- `Move/Native/StdPrimitives.lean` — native function models for
`signer`, `fixed_point32`, `option`, and `bit_vector` operations
- `Refinement/StdPrimitives.lean` — refinement proofs connecting
bytecode/natives to specs (proved via `rfl` — the Lean kernel fully
reduces `eval` for concrete non-looping programs)
- `Tests/StdPrimitives.lean` — `native_decide` smoke tests for stdlib
operations

## File changes summary

| File | Description |
|------|-------------|
| `Refinement/Vector.lean` | Add `vector::index_of` refinement proof |
| `Std/Error.lean` | New: error code specs |
| `Std/Option.lean` | New: option swap/fill specs |
| `Std/Signer.lean` | New: signer specs |
| `Std/FixedPoint32.lean` | New: fixed-point arithmetic specs |
| `Std/BitVector.lean` | New: bit-vector operation specs |
| `Move/Programs/StdPrimitives.lean` | New: stdlib bytecode programs |
| `Move/Native/StdPrimitives.lean` | New: stdlib native function models
|
| `Refinement/StdPrimitives.lean` | New: stdlib refinement proofs |
| `Tests/StdPrimitives.lean` | New: stdlib smoke tests |
| `README.md` | Document new modules |
| `lakefile.lean` | Register new modules |

## Testing

- **`lake build`** — passes (exit code 0). Only pre-existing `sorry`
warnings in unrelated files (plus tracked `sorry` in new
`FixedPoint32.lean`, `BitVector.lean`, and the `reverse` sketch).
- **`lake build difftest`** — Lean difftest executable compiles
successfully (2172/2172 jobs).
- **`difftest.sh`** — full differential test pipeline: **227 passed, 0
failed, 0 skipped**.
  - Step 0: Rust corpus verification ✅
  - Step 1: Move VM oracle generation ✅
  - Step 2: Lean evaluator vs oracle ✅

## Key Areas to Review

- **`Refinement/Vector.lean`** lines 814–824 (`indexOfLoopFrame`): uses
`(List.map MoveValue.u64 xs).length.toUInt64` to match the `contains`
pattern, enabling ~20 downstream `rfl` proofs.
- **`hfail` / `hio` proofs** in `vectorIndexOf_returnValues_notFound` /
`_found`: use `suffices` to generalize over the `indexOf.go` offset
parameter.
- **`sorry` declarations** in `Std/FixedPoint32.lean` (2),
`Std/BitVector.lean` (1), and `Refinement/Vector.lean` (`reverse`
sketch) are tracked for future work.

## Type of Change

- [x] New feature (non-breaking change which adds functionality)

---------

Co-authored-by: andygolay <andygolay@users.noreply.github.com>
## Description

### Background: how batch verification works

A confidential transfer proof contains multiple algebraic equations that
the on-chain verifier must check (e.g. "sender's new balance is
correct", "recipient's amount is correct", "each auditor's ciphertext is
correct"). Checking them one by one is expensive.

**Multi-scalar multiplication (MSM)** batching is an optimization: the
verifier combines all equations into a single equation by multiplying
each one by a different random weight. If the combined equation holds,
all individual ones hold — with overwhelming probability. These random
weights are called **gammas**.

Each gamma is derived as `SHA2-512(rho || i || j)` where **rho** is a
fresh random scalar and **(i, j)** is a unique index pair. The
uniqueness of `(i, j)` is what guarantees every gamma is distinct. If
two different proof relations accidentally receive the same `(i, j)` —
and therefore the same gamma — an attacker could craft values where
errors in one relation cancel out errors in the other, bypassing the
batch check.

### The bug

The function
[`msm_transfer_gammas`](aptos-move/framework/aptos-experimental/sources/confidential_asset/confidential_proof.move)
assigns gamma index groups as follows:

| Group | Count | `i` index | What it checks |
|-------|-------|-----------|----------------|
| g1 | 1 | 1 | Overall balance consistency |
| g2s | 8 | 2 | Sender's new balance (8 chunks of 16 bits each) |
| g3s | 4 | 3 | Recipient's amount (4 chunks) |
| g4s | 4 | 4 | Transfer amount range proof binding |
| g5 | 1 | 5 | Sender encryption key consistency |
| g6s | 8 | 6 | Current balance (8 chunks) |
| g7s | 4 per auditor | 7+k | Auditor `k`'s ciphertext correctness |
| g8s | 4 | **was 8** | Sender's copy of the transfer amount |

With 2+ auditors, `g7s` for auditor `k=1` uses index `7+1 = 8` — the
same `i` as `g8s`. Two different proof relations get identical gammas,
weakening batch soundness.

With 0 or 1 auditors there is no collision (g7s uses only index 7, g8s
uses 8).

### The fix

`g8s` now uses `i = auditors_count + 7` instead of the hardcoded `8`, so
it is always one past the last `g7s` entry:

- `g7s[k]`: index `(7+k, j)` for `k ∈ [0, n)`
- `g8s`: index `(7+n, j)`

This is a verifier-only change. Gammas are not part of the proof — they
are computed on-chain after the proof is submitted. **No change to proof
generation, no client SDK impact.**

Whitepaper §9 updated with a new "Batch Soundness" subsection
documenting the gamma layout.

## How Has This Been Tested?

- 78 Move unit tests pass (`move_experimental_unit_tests`), including
multi-auditor transfer tests and a new regression test
(`test_g8s_gamma_no_collision_with_g7s`)
- 143 Rust e2e tests pass (`e2e-move-tests -- confidential_asset`),
including `confidential_transfer_with_voluntary_auditors_only` (1-3
auditors) and
`confidential_transfer_asset_auditor_plus_voluntary_auditors` (asset
auditor + 0-3 voluntary)
- The regression test proves the collision was real (`msm_gamma_2(rho,
8, 0) == msm_gamma_2(rho, 1+7, 0)`) and that the fix eliminates it for
all sub-indices

## Key Areas to Review

-
[`confidential_proof.move:1605-1609`](aptos-move/framework/aptos-experimental/sources/confidential_asset/confidential_proof.move#L1605-L1609)
— the one-line fix in `msm_transfer_gammas`
-
[`confidential_proof.move:2453-2493`](aptos-move/framework/aptos-experimental/sources/confidential_asset/confidential_proof.move#L2453-L2493)
— regression test proving the collision existed and the fix works
- [`whitepaper.md` §9 Batch
Soundness](aptos-move/framework/aptos-experimental/whitepaper.md) — new
subsection documenting gamma index layout

## Type of Change

- [ ] New feature
- [x] Bug fix
- [ ] Breaking change
- [ ] Performance improvement
- [ ] Refactoring
- [ ] Dependency update
- [x] Documentation update
- [x] Tests

## Which Components or Systems Does This Change Impact?

- [ ] Validator Node
- [ ] Full Node (API, Indexer, etc.)
- [ ] Move/Aptos Virtual Machine
- [x] Aptos Framework
- [ ] Aptos CLI/SDK
- [ ] Developer Infrastructure
- [ ] Move Compiler
- [ ] Other (specify)

## Checklist

- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I tested both happy and unhappy path of the functionality
- [x] I have made corresponding changes to the documentation
@ganymedio
ganymedio requested review from Primata and areshand April 16, 2026 15:55
…ty-point pubkeys (#330)

# confidential_asset: fix `sub_balances_mut` and reject identity-point
pubkeys

Two hardening fixes in `aptos_experimental::confidential_asset`.

## Fix 1 — `confidential_balance::sub_balances_mut` was performing
addition

The body called `twisted_elgamal::ciphertext_add_assign` instead of
`ciphertext_sub_assign`, so `sub_balances_mut` behaved identically to
`add_balances_mut`. No in-tree caller today, but the function is
`public` and exported; any future caller debiting via `sub_balances_mut`
would silently credit instead.

Deleted `sub_balances_mut` function as it is not used.

## Fix 2 — `ristretto255_twisted_elgamal::new_pubkey_from_bytes`
accepted the identity point

There is no scalar `dk` such that `dk⁻¹ · H = identity`, so an identity
public key does not correspond to any keypair. Accepting it admits two
degenerate states:

- **Registration sigma is forgeable.** The proof `s·H + e·ek == R`
collapses to `s·H == R` when `ek = identity`, so a registrant can pass
`register` without holding any `dk`. The resulting account is
unspendable — anything transferred to it is permanently locked.
- **Auditor channel can be silently disabled.** If `set_asset_auditor`
or `set_chain_auditor` (gated by
`system_addresses::assert_aptos_framework`) is given an identity key,
every subsequent transfer's auditor ciphertext is undecryptable and the
auditor-binding term in the transfer proof becomes degenerate, letting
senders submit auditor amounts unrelated to the actual transfer.

All `CompressedPubkey` values flow through `new_pubkey_from_bytes`
(`register`, `set_asset_auditor`, `set_chain_auditor`,
`deserialize_auditor_eks`), so a single check at the deserializer covers
every entry path.

Changes in `ristretto255_twisted_elgamal.move`:
- `new_pubkey_from_bytes` returns `None` for the identity point in
addition to the existing `None` for non-canonical encodings. The check
is a byte equality against `point_identity_compressed()` (the canonical
encoding of identity is uniquely 32 zero bytes).
- New public helper `is_identity_pubkey(&CompressedPubkey): bool` for
callers that obtain a `CompressedPubkey` through other means and want to
re-validate.

## Tests

New module `tests/confidential_asset/audit_regression_tests.move` — 6
tests:

- `sub_balances_mut_self_yields_zero` — subtracting a balance from
itself decrypts to 0.
- `sub_balances_mut_zero_is_identity` — subtracting an encryption of
zero preserves the amount.
- `new_pubkey_from_bytes_rejects_identity` — 32 zero bytes ⇒ `None`.
- `new_pubkey_from_bytes_accepts_real_key` — generated keypair
round-trips; `is_identity_pubkey` returns `false`.
- `new_pubkey_from_bytes_rejects_non_canonical` — pre-existing rejection
preserved.
- `identity_rejection_composes_with_is_some` — callers using
`assert!(... .is_some())` now reject identity with no caller-side
change.

## Testing:

```
cd aptos-move/framework/aptos-experimental
aptos move test --skip-fetch-latest-git-deps \
    --named-addresses aptos_experimental=0x7 \
    --filter confidential
```

Expected: 64 tests pass (58 existing + 6 new).

## Notes

- No state schema or proof-format changes. Existing accounts and
ciphertexts continue to verify.
- The only other `CompressedPubkey` constructor is
`pubkey_from_secret_key`, which is `#[test_only]`.
ganymedio added a commit that referenced this pull request Apr 30, 2026
## Description
View function added in `fungible_asset.move` (see diff). Required for
the changes in #299
Confidential Assets, so would be good to include for testing against
testnet.

## How Has This Been Tested?
Two unit tests:
- test_is_asset_type_dispatchable_false_for_plain_fa
- test_is_asset_type_dispatchable_true_with_derived_supply
ganymedio and others added 3 commits April 30, 2026 17:01
# Two-tier auditor model + epoch tracking for confidential assets

## Summary

The Confidential Asset module previously supported a single optional
per-asset auditor. This PR introduces a **chain-level auditor** (always
required, governed at the framework address) layered alongside the
existing per-asset auditor, plus **monotonic epoch tracking and
append-only history** for both layers. Every confidential transfer now
stamps the active chain and asset auditor epochs onto its `Transferred`
event, enabling reconstruction of which historical key covered which
transfer years after a rotation.

## Behavior changes

### Auditor slot layout

`auditor_eks` (and the matching `auditor_amounts`) now have a fixed
prefix:

```
[0]   chain-level auditor       (always required)
[1]   asset-specific auditor    (required iff get_asset_auditor(token).is_some())
[2..] voluntary auditors        (sender's choice; ordered)
```

Slot identity is bound into the transfer's Fiat–Shamir transcript via
the order of `auditor_eks` (existing mechanism in
`confidential_proof::fiat_shamir_transfer_sigma_proof_challenge`), so a
sender cannot substitute one slot for another.

### Validation

`validate_auditors` now:

- Aborts with new error `ECHAIN_AUDITOR_NOT_SET` if no chain auditor is
configured.
- Rejects the transfer if `auditor_eks[0]` ≠ active chain auditor.
- Rejects the transfer if an asset auditor is set and `auditor_eks[1]` ≠
it.
- Otherwise behaves as before (length checks, ciphertext consistency).

### Epoch + history

`FAController` and `FAConfig` each gain:

- `*_auditor_epoch: u64` — bumped on every set/rotate/clear (including
clears).
- `*_auditor_history: vector<AuditorEntry>` — append-only; each entry
carries `[activated_at_epoch, deactivated_at_epoch)` (`0` while still
active).

`Transferred` event gains `chain_auditor_epoch` and
`asset_auditor_epoch` (`0` when the asset has no auditor).

### Renamed / new module API

| Before | After |
| ----------------------------------- |
------------------------------------------- |
| `set_auditor` | `set_asset_auditor` |
| `get_auditor` | `get_asset_auditor` |
| `AuditorChanged` | `AssetAuditorChanged` (+`new_epoch` field) |
| `FAConfig.auditor_ek` | `FAConfig.asset_auditor_ek` |
| — | `set_chain_auditor` |
| — | `get_chain_auditor` / `*_epoch` / `*_history` |
| — | `get_asset_auditor_epoch` / `*_history` |
| — | `ChainAuditorChanged` |
| — | `AuditorEntry` struct |
| — | `ECHAIN_AUDITOR_NOT_SET` |

### In-flight proof invalidation on rotation

Auditor identity is bound into the proof's Fiat–Shamir transcript, so
rotating either layer invalidates any user transactions signed against
the old key but not yet executed. Documented inline on
`set_chain_auditor` / `set_asset_auditor`.

## Tests

### Move unit tests (`confidential_asset_tests.move`) — 64/64 passing

New coverage:

- `fail_transfer_if_chain_auditor_unset` — abort when chain auditor
missing.
- `fail_transfer_if_slot0_not_chain_auditor` — slot 0 mismatch rejected.
- `fail_transfer_if_asset_auditor_required_but_missing` — slot 1 absent
when required.
- `success_voluntary_auditors_without_asset_auditor` — slot 2+ accepted
with no asset auditor.
- `fail_transfer_after_chain_auditor_rotation` — pre-rotation old-key
proof rejected.
- `success_chain_auditor_rotation_tracks_history` — rotations bump
epochs, history records prior keys, `Transferred` stamps current epoch.

Existing transfer tests updated for the new slot layout (chain auditor
auto-prepended via `get_chain_auditor()` in shared helpers).

### Rust e2e tests (`confidential_asset_e2e.rs`) — 7/7 passing

New coverage:

- `confidential_transfer_rejects_when_chain_auditor_unset`
- `confidential_transfer_rejects_when_slot0_not_chain_auditor`
- `confidential_transfer_rejects_after_chain_auditor_rotation`

`fresh_harness` now installs a default chain auditor; existing tests
pass through unchanged because the test-only
`pack_confidential_transfer_proof_*` helpers auto-prepend the chain
auditor. A `fresh_harness_no_chain_auditor` variant and
`pack_transfer_audited_verbatim` helper support the rejection-path
tests.

> Note: `cargo test -p e2e-move-tests confidential_asset` requires
`RUST_MIN_STACK=33554432` (or higher) — pre-existing requirement,
documented in this module's file header.

## Follow-ups / out of scope

- **Chain-level key custody.** The contract enforces that the
chain-level auditor *exists* and is bound into every transfer, but it
can't enforce *who* holds the private key or *how*. A separate decision
is needed before launch on whether the key sits with a qualified
third-party custodian and whether custody is single-party or
threshold/MPC. Strictly an off-chain operational call.
- **History sizing.** `chain_auditor_history` and
`asset_auditor_history` are unbounded `vector<AuditorEntry>` on
`FAController` / `FAConfig`. Fine pre-launch; if rotation cadence ends
up high in production, follow up with a paginated view or periodic
archival. Not a correctness or compliance gap — just a storage-growth
concern.
- **Per-user auditor epoch tracking on `ConfidentialAssetStore`.** Not
needed in this codebase — auditors only receive ciphertexts at transfer
time, not on the balance, so rollover-staleness (the issue addressed in
zkSecurity finding #1 upstream) does not apply here.
- **Documentation regeneration.**
`aptos-experimental/doc/confidential_asset.md` is left untouched in this
PR; regenerate via `movement move document` against a locally-built
`movement` binary so the markup style stays consistent.

## Files changed

-
`aptos-move/framework/aptos-experimental/sources/confidential_asset/confidential_asset.move`
-
`aptos-move/framework/aptos-experimental/sources/confidential_asset/confidential_gas_e2e_helpers.move`
-
`aptos-move/framework/aptos-experimental/tests/confidential_asset/confidential_asset_tests.move`
- `aptos-move/e2e-move-tests/src/tests/confidential_asset_e2e.rs`
…rimental` to `aptos-framework` (#366)

# Move confidential asset code from `aptos-experimental` to
`aptos-framework`

## Summary

Promotes the Confidential Asset Standard from `aptos-experimental`
(`0x7`) to `aptos-framework` (`0x1`). The module family is no longer
experimental — it now lives alongside the rest of the framework and is
published at the framework address.

## What moved

Source modules (all `0x7::*` → `0x1::*`):

- `confidential_asset` + `.spec.move`
- `confidential_balance` + `.spec.move`
- `confidential_proof` + `.spec.move`
- `ristretto255_twisted_elgamal` + `.spec.move`
- `confidential_gas_e2e_helpers`

Unit tests:

- `confidential_asset_tests.move`
- `confidential_proof_tests.move`
- `ristretto255_twisted_elgamal_tests.move`

Everything under `aptos-experimental/sources/confidential_asset/` and
`aptos-experimental/tests/confidential_asset/` is gone; the equivalents
now live under `aptos-framework/sources/confidential_asset/` and
`aptos-framework/tests/confidential_asset/`.

## Code changes

- Module declarations re-addressed: `module aptos_experimental::*` →
`module aptos_framework::*`.
- All `use aptos_experimental::*` imports inside the module family
rewritten to `use aptos_framework::*`.
- Re-rendered framework reference docs: removed the four `0x7::*`
entries from `aptos-experimental/doc/overview.md` and added the
corresponding `0x1::*` entries to `aptos-framework/doc/overview.md`. The
generated per-module docs (`confidential_asset.md`,
`confidential_balance.md`, `confidential_proof.md`,
`ristretto255_twisted_elgamal.md`) now live under
`aptos-framework/doc/`.
- `aptos_framework_sdk_builder.rs` regenerated with entries for the new
framework entry functions.

## Downstream call-site updates

- `aptos-move/move-examples/confidential_asset/Move.toml` — drop the
`aptos-experimental` dep; all examples already use
`aptos_framework::confidential_asset`.
- `aptos-move/move-examples/confidential_asset/tests/*.move` — rewrite
`use aptos_experimental::*` → `use aptos_framework::*` across the seven
example tests (deposit, normalize, register, rollover, rotate, transfer,
withdraw).
- `scripts/chain-auditor-bootstrap/Move.toml` +
`sources/set_chain_auditor_admin.move` — repoint at the framework
module.

## Rust e2e test update

`aptos-move/e2e-move-tests/src/tests/confidential_asset_e2e.rs` →
`confidential_asset.rs` (registered as `mod confidential_asset;` in
`tests/mod.rs`).

The module-injection harness was retooled to match the new layout:

- Stops compiling `aptos-experimental` and looking for `0x7::*` modules.
- Test-compiles `aptos-framework` and overlays only the
confidential-asset module family (`confidential_asset`,
`confidential_balance`, `confidential_proof`,
`confidential_gas_e2e_helpers`, `ristretto255_twisted_elgamal`) plus
`event`. It deliberately does **not** replace other `0x1` framework
modules — doing so would invalidate state that genesis already published
(account/fungible-store/transaction-validation layouts) and break the
gas-fee prologue.
- `assert_kept_failure` tightened to require `Keep`-with-non-success
instead of "anything that isn't Success" — a fee-discard was previously
masquerading as a kept failure and hiding real bugs.
- Dropped 13 unused helper functions that were carryovers from the
experimental harness.

## Verification

All 14 confidential-asset e2e tests pass against the new layout:

```
RUST_MIN_STACK=67108864 cargo test --release -p e2e-move-tests confidential_asset
...
test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 249 filtered out
```

## Breaking changes

Any off-chain caller or downstream Move package referencing the previous
`0x7::confidential_asset` (and friends) must be updated to
`0x1::confidential_asset`. The module functions, struct shapes, and
entry signatures are unchanged.

## Test plan

- [x] `cargo test --release -p e2e-move-tests confidential_asset` —
14/14 pass
- [ ] Re-run framework Move unit tests for the relocated modules
- [ ] Spot-check the move-examples build against the framework dep
- [ ] Verify generated docs render correctly under
`aptos-framework/doc/`
@rubujubi

rubujubi commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

Just flagging - I think experimental_usecases does not compile on this branch. order_book_example.move still imports aptos_experimental::{active_order_book, order_book, order_book_types},
but those were removed from aptos-experimental in cc5b1c5b

cd testsuite/module-publish/src/packages/experimental_usecases
movement move compile

Unbound module: '(aptos_experimental=0x7)::active_order_book'
....

If we want to clean up those dependent on move 2.2 maybe we drop order_book_example.move as well

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intentional here? It is relevant to fun get_asset_auditor, see below

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no i think it's from aptos, updated

{
let fa_config_address = get_fa_config_address(token);

if (!is_allow_list_enabled() && !exists<FAConfig>(fa_config_address)) {

@rubujubi rubujubi Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allow_list_enabled: chain_id::get() == MAINNET_CHAIN_ID, therefore if MAINNET_CHAIN_ID is set to 126, the first gate !is_allow_list_enabled() will always be false leading to this branch unreachable

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, pushed a fix

@rubujubi

rubujubi commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Several docs under aptos-move/framework/aptos-experimental/doc/ can be removed since they are in aptos-framework/doc/ as well:

confidential_asset.md
confidential_balance.md
confidential_proof.md
ristretto255_twisted_elgamal.md

@@ -0,0 +1,17 @@
// Enables on-chain feature flag 87 (BULLETPROOFS_BATCH_NATIVES) and reconfigures.
// Sender must be the core resources account (localnet: key in <test-dir>/mint.key).
script {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file can be removed from here to make the worktree cleaner. This script is in the governance proposal repo already

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pushed commit removing it

@ganymedio

Copy link
Copy Markdown
Author

Several docs under aptos-move/framework/aptos-experimental/doc/ can be removed since they are in aptos-framework/doc/ as well:

confidential_asset.md
confidential_balance.md
confidential_proof.md
ristretto255_twisted_elgamal.md

removed

@ganymedio

Copy link
Copy Markdown
Author

Just flagging - I think experimental_usecases does not compile on this branch. order_book_example.move still imports aptos_experimental::{active_order_book, order_book, order_book_types}, but those were removed from aptos-experimental in cc5b1c5b

cd testsuite/module-publish/src/packages/experimental_usecases
movement move compile

Unbound module: '(aptos_experimental=0x7)::active_order_book'
....

If we want to clean up those dependent on move 2.2 maybe we drop order_book_example.move as well

thanks, pushed commit dropping it

const FIAT_SHAMIR_NORMALIZATION_SIGMA_DST: vector<u8> = b"MovementConfidentialAsset/Normalization";
const FIAT_SHAMIR_REGISTRATION_SIGMA_DST: vector<u8> = b"MovementConfidentialAsset/Registration";

const BULLETPROOFS_DST: vector<u8> = b"AptosConfidentialAsset/BulletproofRangeProof";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be changed to MovementConfidentialAsset/BulletproofRangeProof ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, thanks, changed

@blacksmith-sh

blacksmith-sh Bot commented Jun 25, 2026

Copy link
Copy Markdown

Found 1 test failure on Blacksmith runners:

Failure

Test View Logs
aptos-executor::internal_indexer_test/test_db_indexer_data View Logs

Fix in Cursor

@blacksmith-sh

blacksmith-sh Bot commented Jun 25, 2026

Copy link
Copy Markdown

Found 1 test failure on Blacksmith runners:

Failure

Test View Logs
aptos-executor::internal_indexer_test/test_db_indexer_data View Logs

Fix in Cursor

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.

4 participants