Skip to content

confidential-assets: wallet integration spec details (multisig and other open questions) - #6

Closed
ganymedio wants to merge 5 commits into
confidential-asset-prodfrom
wallet-integration-details
Closed

confidential-assets: wallet integration spec details (multisig and other open questions)#6
ganymedio wants to merge 5 commits into
confidential-asset-prodfrom
wallet-integration-details

Conversation

@ganymedio

@ganymedio ganymedio commented May 5, 2026

Copy link
Copy Markdown
Collaborator

Confidential assets wallet integration — keyless backing, multisig, keystore additions, and wire-contract pin-downs

Updates confidential-assets/WALLET_INTEGRATION.md with:

  • A first-class multisig wallet entry, with shared per-asset decryption keys distributed out of band among co-owners (§1).
  • The keystore additions Motion Wallet needs for confidential assets — a new per-wallet-entry dk storage area, AES-GCM AAD binding for cross-entry isolation, a scoped loader function, and a new multisig variant of the existing WalletEntry union. Reuses Motion Wallet's existing vault crypto and runtime encryption key; does not redefine the existing keystore (§2).
  • Wire-contract decisions between dApp, wallet, and chain: confirmed auditor view names, pinned senderAuditorHint Fiat–Shamir binding, single-transaction deposit routing, withdraw/transfer never bundling rollover, structured error type, no concurrency serialization needed (§3).
  • Three required changes to @moveindustries/confidential-assets: remove auto-rollover from the *WithTotalBalance helpers, add a build-only API for multisig proof construction, export canonical derivation helpers — including a new keylessDecryptionKey HKDF helper (§4).
  • Dual-publish wallet-standard feature advertisement under both aptos:confidentialAssets and movement:confidentialAssets (§5).
  • Keyless accounts as a third dk backing alongside software (mnemonic) and hardware (device-signature), anchored on the keyless pepper via HKDF-SHA512 rather than the ephemeral keypair (§6).
  • A concrete branch integration plan for combining Motion Wallet's existing feat/keyless-wallet branch with the in-progress confidential-assets-local branch into a single shippable branch (§7).

What this design covers, end to end

A confidential-asset operation flows through four repos:

  • aptos-core holds the on-chain confidential_asset Move module. Already deployed; no changes needed for any of the decisions in this PR.
  • ts-sdk/confidential-assets (this repo) builds zero-knowledge proofs and entry-function payloads for Move calls. Most of the building blocks the wallet integration needs already exist (proofs accept an arbitrary sender address, the chain-level and per-asset auditor views are exposed, the deposit-side combined entrypoints are wrapped, the on-chain senderAuditorHint BCS binding matches what the SDK does today). Three SDK-side changes are required by this design and are listed below.
  • motion-wallet custodies decryption keys, builds proofs in its background service worker, and presents user confirmation screens. The doc specifies its keystore schema, its ca_* method namespace, and its UX rules. It already has a separate feat/keyless-wallet branch landing keyless authentication; this design specifies how keyless backings derive dk and how the two branches merge (§6, §7).
  • movement-wallet-adapter is what dApps import to talk to a wallet. It needs to expose the new ca_* methods to dApps and feature-detect whether a connected wallet supports them.
  • gmove-multisig is the dApp where users manage multisig vaults; it consumes the ca_* methods through the adapter.

The sections below summarize each decision and which repos are affected by it.

1. Multisig accounts as first-class wallet entries

A multisig account on Movement is a resource account: it holds funds but has no private key, so co-owners share a per-asset decryption key (dk[multisig, token]) out of band (e.g. through a password manager). Each owner imports the shared dk into their wallet, which can then construct proofs against the multisig's address. Approvals continue to use each owner's personal Ed25519 key on chain.

The previous draft of the doc handed off "is multisig a wallet entry, or only a dApp concept" as an open question. The decision in this PR: a multisig is a first-class wallet entry, alongside Ed25519-backed entries. The wallet's main UI (the popup's wallet switcher, balance screens, pending-proposal approvals) treats it as a normal account that the user can switch into and use. The dApp gmove-multisig remains the place where users manage a multisig (create it, change owners, etc.); the wallet is where they use it.

The wallet entry carries address, threshold, owners[] (the on-chain owner list), and ownedByWalletIds[] — a list, not a single id, of the device-local Ed25519 wallets that are also on-chain owners. The list is non-trivial: a user with two device-local wallets that are both on-chain owners of the same multisig has one multisig entry, not two, and the imported dk set is not duplicated across owner blobs. If the list is empty (e.g. all on-chain owners are on other devices), the multisig is shown view-only.

Repos affected: motion-wallet (new wallet-entry kind, popup UX). gmove-multisig (steers users to the wallet's import flow rather than handling decryption keys in the dApp).

2. Concrete keystore schema

The original doc specified the invariants a confidential-asset keystore must satisfy (one entry per (account, token), sealed at rest, scoped loader, no bulk export, etc.) but left the on-disk schema to the implementer. This PR commits a concrete schema for Motion Wallet:

  • Storage location. One chrome.storage.local key per wallet entry: mv_dk_store:${walletEntryId}. Multisig entries get their own store, separate from the owner's store, so a single shared dk exists in exactly one place.
  • What's stored at rest. Only imported decryption keys. Native keys (mnemonic-derived for software, device-signature-derived for hardware, pepper-HKDF-derived for keyless) are recomputed on demand and live only in an in-memory cache during the unlocked session. There is no on-disk state to enumerate for native keys.
  • In-memory cache lifecycle. The native-key cache shares its lifecycle with the wallet's existing Ed25519 signing-key cache: lazy on first use, retained for the unlocked session, zeroed on lock and idle timeout. No tighter eviction. A stricter "evict after each operation" policy would close no real privacy gap (the same window the signing key is loaded) and would make hardware-wallet UX unusable (a fresh device button-press per CA operation).
  • Encryption. Each entry is sealed with AES-GCM under the wallet's existing runtime second-layer key (PBKDF2-derived from the user's password, already lifecycle-bound to unlock and zeroed on lock). The AES-GCM AAD is utf8("mv-dk-v1") || 0x00 || accountAddress || tokenMetaAddr and is reconstructed at decrypt time from the loader's arguments — never read back from storage. Cross-store substitution fails AES-GCM tag verification, so per-entry isolation is enforced cryptographically rather than by code review. The version tag in AAD also makes future schema changes (e.g. mv-dk-v2) unforgeable from v1 ciphertexts.
  • Loader contract. A single function loadDk(accountAddress, tokenMetaAddr) handles all paths in order: in-memory cache → mnemonic derivation (software) → device signature (hardware) → HKDF over pepper (keyless) → imported store → throw. Multisig entries skip the three native-derivation paths and resolve only via the imported store, since a multisig has no private key, signer device, or pepper.
  • Backup/export. A whole-wallet backup includes every mv_dk_store blob (already sealed by the runtime key, useless without the password); excluding them would silently lose imported multisig material that mnemonic recovery cannot reproduce.

Repos affected: motion-wallet only. The schema does not appear on the wire — dApps and the SDK never see it.

3. Wire-contract decisions between dApp, adapter, wallet, and chain

Several pieces of the wallet ↔ dApp interface that the draft had punted are now committed.

Auditor view function names

Movement's confidential-asset module supports auditors at three levels: a chain-level auditor configured by chain governance, a per-asset auditor configured by the asset issuer, and per-transfer voluntary auditors supplied by the sender. The wallet must read the chain-level and per-asset auditors before constructing every transfer. The original doc named the read functions placeholders. This PR confirms them against confidential_asset.move:

  • get_chain_auditor() and get_chain_auditor_epoch() for the chain-level auditor.
  • get_asset_auditor(token) and get_asset_auditor_epoch(token) for the per-asset auditor.

The wallet's ca_getGlobalAuditor and ca_getAuditor responses now include the epoch alongside the encryption key, so dApps can detect a stale read.

Repos affected: ts-sdk/confidential-assets (verify the corresponding helpers in viewFunctions.ts use these names). motion-wallet (the read methods).

senderAuditorHint binding

ca_transfer accepts an optional opaque hint that gets bound into the transfer's Fiat–Shamir proof transcript and emitted on-chain. The previous doc left "exactly how the hint is encoded into the transcript" open. The on-chain verifier does bcs::to_bytes(sender_auditor_hint) (a BCS vector<u8>: ULEB128 length prefix followed by the bytes), which is what the SDK's bcsSerializeMoveVectorU8 already produces. The doc now says exactly this and notes that the wallet must read the on-chain max_sender_auditor_hint_bytes() view to enforce the length cap before proof construction.

Repos affected: none — this is a documentation correction; the SDK and on-chain module already agree on the format. Calling it out prevents a future divergence.

Deposit is always one on-chain transaction

The original doc described ca_deposit as a sequence of register then deposit when the user wasn't yet registered for that token. That was wrong. The on-chain module exposes combined entry functions (register_and_deposit_and_rollover_pending_balance, deposit_and_rollover_pending_balance, deposit_and_normalize_and_rollover_pending_balance) and the SDK exposes them as registerAndDepositAndRollover, depositAndRollover, and depositNormalizeAndRollover. The wallet picks one based on the account's current state and submits a single transaction. The user sees one approval in every case.

Repos affected: motion-wallet (route through the right SDK helper based on (registered?, normalized?)).

Withdraw and transfer never roll pending balance into actual

The Movement protocol distinguishes:

  • actual balance — what the user can spend.
  • pending balance — incoming transfers and deposits that have not been accepted yet. Not part of total balance and not spendable until the user authorizes a rollover transaction.

The original doc said that when the user tried to send more than actual, the wallet should bundle a rollover (and possibly a normalize) ahead of the spend in a single approval. This conflicts with the doc's own principle that rollover requires explicit user authorization — accepting incoming funds is a discrete decision, not an implementation detail of "send N USDC."

This PR fixes that. ca_withdraw and ca_transfer operate on actual only and return INSUFFICIENT_BALANCE when amount > actual, regardless of pending. If the user has pending funds that could cover the shortfall, the dApp (or the wallet's own send screen) prompts them to first activate "Accept incoming funds," after which they can retry the spend.

The principle applies to any caller, not just the wallet — a CLI tool or server-side automation that silently rolls pending into actual to make a spend succeed is also wrong. The SDK currently has helpers (withdrawWithTotalBalance, transferWithTotalBalance) that do this; this design requires they be fixed (see "SDK changes" below).

The result: ca_withdraw, ca_transfer, and ca_deposit are each always one on-chain transaction. The only multi-transaction CA flow is ca_rolloverPending, which submits at most two (normalize if needed, then rollover), silently chained inside one user approval since normalize is a protocol implementation detail of "accept incoming funds."

Repos affected: ts-sdk/confidential-assets (remove the auto-rollover behavior from the helpers, see SDK changes below). motion-wallet (return INSUFFICIENT_BALANCE rather than calling auto-rollover paths). gmove-multisig and any other dApp (handle the INSUFFICIENT_BALANCE response by prompting the user to accept incoming funds).

Structured error type

ca_* calls now return errors with a finite, versioned enum of 16 codes (USER_REJECTED, WALLET_LOCKED, NOT_CONNECTED, UNSUPPORTED_METHOD, UNSUPPORTED_MODE, CA_FEATURE_UNAVAILABLE, INVALID_REQUEST, TOKEN_NOT_REGISTERED, TOKEN_FROZEN, TOKEN_DISABLED, INSUFFICIENT_BALANCE, PENDING_COUNTER_LIMIT, NETWORK_ERROR, CHAIN_REJECTED, PROOF_FAILED, INTERNAL_ERROR) plus a human-readable message and a constrained details bag.

The details bag carries Move abort codes, abort symbol names, transaction hashes, and capability hints — all of which are already public chain state or already known to the dApp. It does not carry storage paths, KDF parameters, balance ciphertexts, decrypted balances outside the legitimate ca_getBalances flow, proof intermediates, or stack traces. INTERNAL_ERROR is intentionally opaque: internal failure modes are an implementation detail and pinning them in the wire contract would (a) prevent the wallet from being refactored without breaking dApps that pattern-match on error strings, and (b) turn the wallet into an oracle a malicious dApp could probe through differential error analysis.

Repos affected: motion-wallet (implement the error mapping in the message-handler dispatch). movement-wallet-adapter (re-export the type so dApps get a single import surface). dApps in general (switch on the enum to drive UX).

Concurrency

The original doc punted concurrency to "wallet should serialize per-(account, token)." This PR explains why no serialization is needed:

  • Every CA write requires explicit user authorization in a confirmation surface (a screen for in-wallet flows, an approval window for dApp-initiated flows).
  • The user can interact with one confirmation surface at a time. They cannot approve two operations in parallel.
  • The existing canOpenPopup rate limit and the chrome.windows.onRemoved "rejected on close" behavior already prevent dApps from stacking pending approvals.
  • The only window of overlap is between "user authorized transaction A" and "transaction A confirmed on chain." During that window, the wallet builds any new operation's proof against fresh on-chain state. If A confirms first, the new proof matches; if A hasn't confirmed and the new proof binds to pre-A state, the chain rejects the second submission with an abort, the wallet returns CHAIN_REJECTED, and the dApp retries against fresh state.

So no in-wallet locking is required. dApps do not need to model concurrency; they retry once on CHAIN_REJECTED.

Repos affected: none, beyond ensuring the implementations don't add unnecessary serialization.

4. SDK changes required by this design

The doc adds a new section listing three changes to @moveindustries/confidential-assets that are required to make the wallet integration implementable and to remove a footgun that contradicts the design's authorization model.

withdrawWithTotalBalance / transferWithTotalBalance must not auto-rollover

These helpers (api/confidentialAsset.ts:265,417) currently call a private checkSufficientBalanceAndRolloverIfNeeded that fetches the user's balance, sees actual < amount but actual + pending ≥ amount, and silently submits a rollover_pending_balance before the spend. They return Promise<CommittedTransactionResponse[]> because they can result in 1 or 2 on-chain transactions.

This contradicts the design principle that rollover requires explicit user authorization, and the principle applies to all callers — not only the wallet. Required change: remove the auto-rollover branch. Two acceptable forms:

  • Option A (recommended): Delete the helpers entirely. Without auto-rollover their only remaining behavior is a pre-flight balance check, which any caller can do directly via getBalance followed by withdraw / transfer. Deletion removes a confusing API surface (the names "withTotalBalance" suggest pending is part of total balance, which it isn't).
  • Option B: Keep the helpers but make them throw Insufficient balance whenever actual < amount, regardless of pending. Rename to remove the misleading "TotalBalance" framing.

Either way, no SDK code path silently accepts incoming funds.

Build-only API for proof construction

For multisig confidential-asset operations, the wallet must construct proofs and return raw EntryFunction BCS bytes rather than submitting a transaction. The dApp wraps those bytes in MultiSigTransactionPayload and proposes the transaction through multisig_account::create_transaction.

Today the high-level ConfidentialAsset class always submits via a signer. The lower-level ConfidentialAssetTransactionBuilder accepts an arbitrary sender and constructs the proofs, but does not expose serialized EntryFunction bytes directly. Each wallet implementer would need to bridge that gap themselves, which invites byte-level divergence.

Required change: add buildRegister, buildDeposit, buildWithdraw, buildConfidentialTransfer, buildRolloverPending, buildNormalize (either as new methods on ConfidentialAsset or as a sibling ConfidentialAssetBuilder class). Each takes the same arguments as its submitting counterpart but uses an explicit sender: AccountAddressInput (no signer) and a decryptionKey for proof construction, returning Uint8Array of BCS-encoded EntryFunction bytes.

Canonical derivation helpers

The doc fixes three derivation policies, one per backing kind:

  • Software: tokenIndex = u32_le(SHA-256(meta)[0..4]) & 0x7FFFFFFF, then derive at m/44'/637'/{accountIndex}'/1'/{tokenIndex}' from the mnemonic.
  • Hardware: sign the bytes decryptionKeyDerivationMessage ‖ ":" ‖ hex(meta) with the device, then derive dk from the signature.
  • Keyless: okm = HKDF-SHA512(pepper, salt = utf8("movement-ca/v1"), info = utf8("dk:") || accountAddress || tokenMetadataAddress, L = 64), then dk = TwistedEd25519PrivateKey.fromUniformBytes(okm). Binding accountAddress into info lets one keyless identity safely back its own account plus any number of multisigs the owner is the designated proposer for, without dk collisions.

These layouts are part of the wallet ↔ chain compatibility contract: a different tokenIndex formula, signed-message layout, or HKDF parameter set produces a different dk / ek and orphans every existing registration. Re-implementing the byte assembly in each wallet is a divergence risk.

Required change: export named helpers — tokenIndexFromMetadataAddress(addr), softwareDecryptionKeyDerivationPath(accountIndex, addr), hardwareDecryptionKeyDerivationMessage(addr), keylessDecryptionKey(pepper, accountAddress, tokenMetaAddr) — with test vectors. The keyless helper takes raw pepper bytes (not a higher-level keyless-account object) so the SDK does not need to model OIDC state. Wallet implementations call these instead of re-deriving the byte layouts.

The keyless helper requires one new primitive on the key class: TwistedEd25519PrivateKey.fromUniformBytes(bytes: Uint8Array): TwistedEd25519PrivateKey, which accepts ≥ 32 bytes of uniform input and reduces modulo the Ed25519 group order ℓ. It mirrors the reduction already performed inside fromSignature.

5. How the wallet advertises confidential-asset support to dApps

When a dApp connects to a wallet through the Movement wallet adapter, it sees a "features" map describing which methods the wallet supports — entries like aptos:signTransaction, movement:signTransaction, etc. The adapter (movement-wallet-adapter/packages/wallet-adapter-core) probes both aptos:X and movement:X for every standard feature, and Motion Wallet (and other Movement wallets) publish features under both prefixes pointing at the same handler. This dual prefix exists because Movement inherited its wallet-standard feature names from AIP-62 (Aptos's wallet standard) and added movement:* aliases on top.

Confidential-asset support follows the same convention: the wallet publishes both aptos:confidentialAssets and movement:confidentialAssets, both keys pointing at the same feature object. A dApp using the Movement adapter finds it under either name; a dApp built against the Aptos wallet-standard tooling finds it under aptos:confidentialAssets. Same single object exposed under two map keys; nothing is duplicated except the entry in the map.

No version suffix in v1 (matching every other feature in the Movement adapter, none of which carry suffixes). If a future version of the surface introduces incompatible changes, the wallet will publish a new key under whatever versioning convention the rest of the wallet-standard has adopted by then.

The "Future direction" subsection in the doc notes that the dual-publish convention is a transitional state. The Movement ecosystem should consider an ecosystem-coordinated migration to movement:*-only feature names — wallet, adapter, and major dApps moving in lockstep — at which point Motion Wallet would drop the aptos:* aliases. That migration is not part of the confidential-assets work; this PR just adopts the existing convention rather than getting ahead of it.

Repos affected: motion-wallet (publish under both keys). movement-wallet-adapter (expose a useConfidentialAssets() React hook that uses the existing dual-prefix probe to find the feature).

6. Keyless accounts as a third dk backing

Motion Wallet has an in-flight feat/keyless-wallet branch that adds OIDC + ephemeral-keypair authentication. Keyless accounts have no mnemonic and no long-lived signing key on the device, so neither the software (fromDerivationPath) nor the hardware (fromSignature) backing applies. This PR specifies a third backing kind so confidential assets work for keyless wallets on the same footing as the other two.

Why the pepper, not the ephemeral key

The keyless ephemeral keypair rotates per session / re-authentication. Using any function of it as dk[token] would orphan the registered ek[token] after every rotation and render the confidential balance for that asset unrecoverable. The keyless pepper — the per-identity secret the wallet already holds for address derivation — is the only client-side material that is both stable across rotations and unique per identity, so it is the correct anchor.

HKDF layout

dk[account, token] is derived from the pepper via HKDF-SHA512 with parameters fixed at the wallet:

  • salt = utf8("movement-ca/v1") (versioned domain separator)
  • info = utf8("dk:") || accountAddress || tokenMetadataAddress (each address as 32 raw bytes)
  • L = 64, scalar reduced via TwistedEd25519PrivateKey.fromUniformBytes

Binding accountAddress into info is what lets a single keyless identity safely back its own account plus any number of multisigs the owner is a designated proposer for, without dk collisions across them. It mirrors how software backings use accountIndex to distinguish multiple accounts under one mnemonic.

Security and recovery

Each natively derived dk[token] is recomputed from the pepper on every wallet unlock and is not persisted at rest; the pepper itself is held in the same trust class as the mnemonic (wallet process only, never returned to a web origin, never logged, never supplied by a dApp). Pepper recovery — the same path that recovers the keyless account's address — reproduces every natively derived dk[token], putting keyless on the same recovery footing as software (mnemonic) and hardware (device re-pairing). Imported dk[token] entries for multi-owner custody are not reproduced and must be retained out of band, identical to the other backings.

A wallet-process compromise during an active CA operation discloses the dk[token] for the asset being acted on (privacy loss for that token). Funds remain safe because moving them still requires a valid OIDC proof and an ephemeral-key signature, neither of which the wallet process can produce without the user re-authenticating.

Cross-device determinism

Keyless cross-device parity of confidential balances depends on the pepper service returning byte-identical pepper material for the same (iss, aud, uid_key, uid_val) tuple regardless of which device asks. This is the keyless analog of "same mnemonic → same dk on every device" for software, and the doc calls it out as an implicit dependency the keyless backing inherits from its address derivation.

Why the OIDC identity tuple is not bound into HKDF info

The pepper is already a deterministic function of (iss, aud, uid_key, uid_val) (the pepper service derives one from the other), so binding it again into info would be redundant. Avoiding the redundant binding also keeps the HKDF input free of OIDC-specific structure, preserving the option of pepper recovery from a non-OIDC source (e.g. a future social-recovery scheme) without forcing an HKDF-policy change.

Coupling between CA pepper and address-derivation pepper

The same raw pepper drives both the keyless account's on-chain address derivation and every dk[account, token] for that account. HKDF's versioned salt provides cryptographic domain separation, so a leak of one derivation does not leak the other beyond what direct pepper exposure already implies. Operationally, however, the two are coupled: a pepper rotation event affects both simultaneously, and pepper recovery has to succeed for either to be usable. This coupling is intentional — the alternative (a separate CA-only pepper) would double the recovery surface — but the doc surfaces it because future protocol changes that touch only one side would still require coordinated handling.

Repos affected: motion-wallet (extend dk derivation to a third branch keyed on backing kind, retain pepper in the unlocked session). ts-sdk/confidential-assets (export keylessDecryptionKey and TwistedEd25519PrivateKey.fromUniformBytes, see §4).

7. Branch integration plan (Motion Wallet)

The doc adds a final section grounding the design in the actual state of Motion Wallet's two in-flight branches as observed:

  • feat/keyless-wallet (head 6bb7a61) — implements full keyless authentication via @eigerco/movement-keyless. Notably, the pepper is fetched on every unlock and currently discarded (initializeFromKeyless destructures only { account }), so CA work has to widen this destructure and stash the pepper in the unlocked-session struct.
  • confidential-assets-local (head 3019c40) — implements CA registration, send, balances; uses accountIndex-keyed software-backed dk derivation and explicitly throws "Unsupported account type for confidential assets" for keyless entries. Includes localnet-specific config and a stale docs/CONFIDENTIAL_ASSETS_INTEGRATION_PLAN.md.

The recommended strategy is to cut feat/confidential-assets off feat/keyless-wallet and port CA forward onto it (rather than the reverse), since pepper lifecycle is the most invasive change and already lives on the keyless branch. The doc lays out an eight-step plan: cut the branch; retain the pepper in the unlocked session and zero it on lock alongside cachedSigners; generalize dk derivation by adding a third (keyless) branch that calls keylessDecryptionKey against the active pepper and the keyless account address, and rename getEd25519SigningAccountgetCaSenderAddress since the keyless signer is not an Ed25519Account; port the CA service module and UI hooks; drop localnet-only bits and the stale planning doc; add unit and integration tests; land the SDK additions in @moveindustries/confidential-assets first.

Risks and order-of-operations are called out: the SDK PR must land before the wallet PR (or the wallet branch strands on a broken import); the keyless HKDF policy strings must be locked before public-testnet keyless registrations make them irreversible; and multisig CA + hardware-backed CA are explicitly out of scope for this merge (multisig as a separate effort, hardware CA gated on the open question about account-address binding in the signed-message layout).

Repos affected: motion-wallet (cut and port the branch); ts-sdk/confidential-assets (land the SDK additions first). No wire-contract change.

Open-questions table

The doc's "Open questions" section now has 10 numbered rows. Six are still open and four (all keyless-related) are resolved-in-table by checking the keyless branch and the pepper-service docs:

# Question Status
1 Per-transfer auditor address UX Open. Concerns voluntary per-transfer auditors only; chain-level and per-asset auditors are mandatory and not in scope of this question. Options (a) per-transfer entry, (b) wallet-managed address book, (c) dApp provides + wallet confirms. (a) or (c) is likely sufficient for v1.
2 Spam token rollover and surfacing Open, with a suggested v1 answer in the doc: per-token rollover only (no "accept all"), unfamiliar tokens shown with a warning badge (not hidden, not blocked), no allowlist dependency in v1.
3 Hardware-backing account-address binding Open. The hardware signed-message layout binds only the token metadata address. A hardware-backed owner who is the designated proposer for two multisigs both registered for the same token would derive identical dk for both — a collision the keyless backing avoids by binding accountAddress into HKDF info. Amending the layout breaks every existing hardware-backing registration; not amending leaves the footgun. Decision needed before any hardware-backed multisig registrations are written on chain.
4 Pepper rotation policy Resolved. The pepper from @eigerco/movement-keyless is documented as deterministic per (sub, aud); CA implementation can assume no rotation. If a future pepper-service version introduces rotation, this row reopens and the keyless §Recovery prose (the v2-HKDF rotation procedure) becomes the playbook.
5 Pepper-service availability at unlock Resolved. The keyless branch already requires interactive OAuth + a prover round-trip on every unlock; if the pepper service is unreachable, the wallet cannot unlock at all, which subsumes "CA cannot decrypt." No CA-specific degradation mode is needed.
6 Federated keyless Open. Movement supports both vanilla and federated keyless. Does the federated-keyless pepper have the same lifecycle / rotation policy / per-identity stability? If lifecycles diverge, federated keyless may need its own salt namespace (e.g. movement-ca/federated/v1) to prevent cross-policy dk aliasing.
7 Pepper byte format Resolved. The Movement pepper service returns a hex string (KeylessLoginResult.pepper) that decodes to 31 raw bytes. The wallet feeds those 31 bytes verbatim as the HKDF ikm.
8 Pepper at-rest storage Resolved. The keyless branch does not persist the pepper at rest; it is re-fetched from the prover on every unlock. CA inherits this — pepper lives only in keylessActiveSession for the unlocked-session lifetime. The WalletEntry.kind = 'keyless' shape need not gain an at-rest pepper field.
9 Ephemeral-key expiry mid-proof Open. A confidential transfer's proof construction takes seconds in-browser; if the keyless ephemeral key expires between proof start and submission, the user faces a re-auth round-trip. The proof binds to senderAddress via Fiat–Shamir (not the ephemeral key), so it survives re-auth and can be wrapped in a freshly-signed transaction. Open: does the wallet (a) silently trigger keyless re-auth and re-sign, or (b) surface a dedicated error and ask the user to retry?
10 Loss of OIDC provider access Open. Permanent loss of access to the OIDC account (deleted Google account, IdP shutdown, employer revoking workforce IdP, etc.) means the user cannot re-authenticate, cannot fetch the pepper, and every dk[token] derived from that pepper becomes unrecoverable. The on-chain keyless account itself faces the same fate for fund movement; CA recovery inherits that. The doc calls this out as a keyless-specific tail risk, parallel to mnemonic loss for software backings, that Motion Wallet UX should make obvious before users sink meaningful confidential balances into a keyless-only account.

Four additional questions came up during this PR's review cycle and were resolved before landing (they do not appear in the table because they were resolved into the design itself):

  • Multisig as a wallet entry. Resolved as a first-class wallet entry — see §1.
  • Native-key in-memory cache lifetime. Resolved as "same lifecycle as the existing Ed25519 signing-key cache" — see §2.
  • Chain-level auditor view function name. Resolved by checking the on-chain module: get_chain_auditor() / get_chain_auditor_epoch() — see §3.
  • senderAuditorHint Fiat–Shamir binding. Resolved by checking the on-chain verifier and SDK: BCS vector<u8> — see §3.

The six remaining open questions split into one product/UX question (#1), one product-policy question (#2), one cryptographic-compatibility question that gates hardware-backed multisig CA (#3), one keyless-coverage question (#6), and two keyless-UX/risk questions (#9, #10). None block the immediate implementation path (motion-wallet's keyless+software-backed CA merge per §7); #3 specifically blocks hardware-backed multisig CA, and #6 blocks claiming federated-keyless coverage.

Out of scope for this PR

  • Implementation in motion-wallet, movement-wallet-adapter, or gmove-multisig. This PR updates the design doc only — including the branch integration plan in §7, which describes the merge but doesn't perform it.
  • Implementation of the SDK changes listed in §4. This PR specifies them; the work itself is a follow-up SDK PR.
  • Any changes to the on-chain confidential_asset Move module or to the SDK's proof construction. The on-chain module is unchanged; the SDK changes in §4 are API-surface fixes, not new cryptography.
  • Key rotation. The doc continues to explicitly exclude ca_rotateEncryptionKey from the wallet ↔ dApp surface; rotation is a power-user procedure that runs through the SDK directly in a trusted environment.
  • The broader migration from aptos:* to movement:* wallet-standard feature names. Noted as ecosystem-coordinated future work in §5.

@ganymedio ganymedio changed the title confidential-assets: multisig and other open confidential-assets: wallet integration details (multisig and other open questions) May 6, 2026
@ganymedio ganymedio changed the title confidential-assets: wallet integration details (multisig and other open questions) confidential-assets: wallet integration spec details (multisig and other open questions) May 6, 2026
ganymedio and others added 4 commits May 7, 2026 16:19
Replaces the out-of-band hex-share flow with an encrypted on-chain
envelope channel via a new dk_inbox Move module. Folds Recovery
from a shared dk leak in as a subsection of DK sharing among
co-owners and updates the algorithm-choice table to reflect on-chain
delivery and the retroactive-disclosure tradeoff.
@ganymedio

Copy link
Copy Markdown
Collaborator Author

closed in favor of MoveIndustries/MIP#1

@ganymedio ganymedio closed this May 18, 2026
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