confidential-assets: wallet integration spec details (multisig and other open questions) - #6
Closed
ganymedio wants to merge 5 commits into
Closed
confidential-assets: wallet integration spec details (multisig and other open questions)#6ganymedio wants to merge 5 commits into
ganymedio wants to merge 5 commits into
Conversation
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.
Collaborator
Author
|
closed in favor of MoveIndustries/MIP#1 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Confidential assets wallet integration — keyless backing, multisig, keystore additions, and wire-contract pin-downs
Updates
confidential-assets/WALLET_INTEGRATION.mdwith:dkstorage area, AES-GCM AAD binding for cross-entry isolation, a scoped loader function, and a newmultisigvariant of the existingWalletEntryunion. Reuses Motion Wallet's existing vault crypto and runtime encryption key; does not redefine the existing keystore (§2).senderAuditorHintFiat–Shamir binding, single-transaction deposit routing, withdraw/transfer never bundling rollover, structured error type, no concurrency serialization needed (§3).@moveindustries/confidential-assets: remove auto-rollover from the*WithTotalBalancehelpers, add a build-only API for multisig proof construction, export canonical derivation helpers — including a newkeylessDecryptionKeyHKDF helper (§4).aptos:confidentialAssetsandmovement:confidentialAssets(§5).dkbacking alongside software (mnemonic) and hardware (device-signature), anchored on the keyless pepper via HKDF-SHA512 rather than the ephemeral keypair (§6).feat/keyless-walletbranch with the in-progressconfidential-assets-localbranch into a single shippable branch (§7).What this design covers, end to end
A confidential-asset operation flows through four repos:
aptos-coreholds the on-chainconfidential_assetMove 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 arbitrarysenderaddress, the chain-level and per-asset auditor views are exposed, the deposit-side combined entrypoints are wrapped, the on-chainsenderAuditorHintBCS binding matches what the SDK does today). Three SDK-side changes are required by this design and are listed below.motion-walletcustodies decryption keys, builds proofs in its background service worker, and presents user confirmation screens. The doc specifies its keystore schema, itsca_*method namespace, and its UX rules. It already has a separatefeat/keyless-walletbranch landing keyless authentication; this design specifies how keyless backings derivedkand how the two branches merge (§6, §7).movement-wallet-adapteris what dApps import to talk to a wallet. It needs to expose the newca_*methods to dApps and feature-detect whether a connected wallet supports them.gmove-multisigis the dApp where users manage multisig vaults; it consumes theca_*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 shareddkinto 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-multisigremains 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), andownedByWalletIds[]— 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 importeddkset 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:chrome.storage.localkey per wallet entry:mv_dk_store:${walletEntryId}. Multisig entries get their own store, separate from the owner's store, so a single shareddkexists in exactly one place.utf8("mv-dk-v1") || 0x00 || accountAddress || tokenMetaAddrand 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.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.mv_dk_storeblob (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-walletonly. 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()andget_chain_auditor_epoch()for the chain-level auditor.get_asset_auditor(token)andget_asset_auditor_epoch(token)for the per-asset auditor.The wallet's
ca_getGlobalAuditorandca_getAuditorresponses 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 inviewFunctions.tsuse these names).motion-wallet(the read methods).senderAuditorHintbindingca_transferaccepts 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 doesbcs::to_bytes(sender_auditor_hint)(a BCSvector<u8>: ULEB128 length prefix followed by the bytes), which is what the SDK'sbcsSerializeMoveVectorU8already produces. The doc now says exactly this and notes that the wallet must read the on-chainmax_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_depositas a sequence ofregisterthendepositwhen 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 asregisterAndDepositAndRollover,depositAndRollover, anddepositNormalizeAndRollover. 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:
actualbalance — what the user can spend.pendingbalance — incoming transfers and deposits that have not been accepted yet. Not part of total balance and not spendable until the user authorizes arollovertransaction.The original doc said that when the user tried to send more than
actual, the wallet should bundle arollover(and possibly anormalize) 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_withdrawandca_transferoperate onactualonly and returnINSUFFICIENT_BALANCEwhenamount > 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, andca_depositare each always one on-chain transaction. The only multi-transaction CA flow isca_rolloverPending, which submits at most two (normalizeif needed, thenrollover), silently chained inside one user approval sincenormalizeis 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(returnINSUFFICIENT_BALANCErather than calling auto-rollover paths).gmove-multisigand any other dApp (handle theINSUFFICIENT_BALANCEresponse 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 constraineddetailsbag.The
detailsbag 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 legitimateca_getBalancesflow, proof intermediates, or stack traces.INTERNAL_ERRORis 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:
canOpenPopuprate limit and thechrome.windows.onRemoved"rejected on close" behavior already prevent dApps from stacking pending approvals.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-assetsthat are required to make the wallet integration implementable and to remove a footgun that contradicts the design's authorization model.withdrawWithTotalBalance/transferWithTotalBalancemust not auto-rolloverThese helpers (
api/confidentialAsset.ts:265,417) currently call a privatecheckSufficientBalanceAndRolloverIfNeededthat fetches the user's balance, seesactual < amountbutactual + pending ≥ amount, and silently submits arollover_pending_balancebefore the spend. They returnPromise<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:
getBalancefollowed bywithdraw/transfer. Deletion removes a confusing API surface (the names "withTotalBalance" suggest pending is part of total balance, which it isn't).Insufficient balancewheneveractual < 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
EntryFunctionBCS bytes rather than submitting a transaction. The dApp wraps those bytes inMultiSigTransactionPayloadand proposes the transaction throughmultisig_account::create_transaction.Today the high-level
ConfidentialAssetclass always submits via a signer. The lower-levelConfidentialAssetTransactionBuilderaccepts an arbitrarysenderand constructs the proofs, but does not expose serializedEntryFunctionbytes 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 onConfidentialAssetor as a siblingConfidentialAssetBuilderclass). Each takes the same arguments as its submitting counterpart but uses an explicitsender: AccountAddressInput(no signer) and adecryptionKeyfor proof construction, returningUint8Arrayof BCS-encodedEntryFunctionbytes.Canonical derivation helpers
The doc fixes three derivation policies, one per backing kind:
tokenIndex = u32_le(SHA-256(meta)[0..4]) & 0x7FFFFFFF, then derive atm/44'/637'/{accountIndex}'/1'/{tokenIndex}'from the mnemonic.decryptionKeyDerivationMessage ‖ ":" ‖ hex(meta)with the device, then derivedkfrom the signature.okm = HKDF-SHA512(pepper, salt = utf8("movement-ca/v1"), info = utf8("dk:") || accountAddress || tokenMetadataAddress, L = 64), thendk = TwistedEd25519PrivateKey.fromUniformBytes(okm). BindingaccountAddressintoinfolets one keyless identity safely back its own account plus any number of multisigs the owner is the designated proposer for, withoutdkcollisions.These layouts are part of the wallet ↔ chain compatibility contract: a different
tokenIndexformula, signed-message layout, or HKDF parameter set produces a differentdk/ekand 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 insidefromSignature.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 bothaptos:Xandmovement:Xfor 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 addedmovement:*aliases on top.Confidential-asset support follows the same convention: the wallet publishes both
aptos:confidentialAssetsandmovement: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 underaptos: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 theaptos:*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 auseConfidentialAssets()React hook that uses the existing dual-prefix probe to find the feature).6. Keyless accounts as a third
dkbackingMotion Wallet has an in-flight
feat/keyless-walletbranch 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 registeredek[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 viaTwistedEd25519PrivateKey.fromUniformBytesBinding
accountAddressintoinfois what lets a single keyless identity safely back its own account plus any number of multisigs the owner is a designated proposer for, withoutdkcollisions across them. It mirrors how software backings useaccountIndexto 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 deriveddk[token], putting keyless on the same recovery footing as software (mnemonic) and hardware (device re-pairing). Importeddk[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 → samedkon 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
infoThe 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 intoinfowould 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 versionedsaltprovides 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(extenddkderivation to a third branch keyed on backing kind, retain pepper in the unlocked session).ts-sdk/confidential-assets(exportkeylessDecryptionKeyandTwistedEd25519PrivateKey.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(head6bb7a61) — implements full keyless authentication via@eigerco/movement-keyless. Notably, the pepper is fetched on every unlock and currently discarded (initializeFromKeylessdestructures only{ account }), so CA work has to widen this destructure and stash the pepper in the unlocked-session struct.confidential-assets-local(head3019c40) — implements CA registration, send, balances; usesaccountIndex-keyed software-backeddkderivation and explicitly throws"Unsupported account type for confidential assets"for keyless entries. Includes localnet-specific config and a staledocs/CONFIDENTIAL_ASSETS_INTEGRATION_PLAN.md.The recommended strategy is to cut
feat/confidential-assetsofffeat/keyless-walletand 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 alongsidecachedSigners; generalizedkderivation by adding a third (keyless) branch that callskeylessDecryptionKeyagainst the active pepper and the keyless account address, and renamegetEd25519SigningAccount→getCaSenderAddresssince the keyless signer is not anEd25519Account; 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-assetsfirst.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:
dkfor both — a collision the keyless backing avoids by bindingaccountAddressinto HKDFinfo. 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.Pepper rotation policy@eigerco/movement-keylessis 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 (thev2-HKDF rotation procedure) becomes the playbook.Pepper-service availability at unlockmovement-ca/federated/v1) to prevent cross-policydkaliasing.Pepper byte formatKeylessLoginResult.pepper) that decodes to 31 raw bytes. The wallet feeds those 31 bytes verbatim as the HKDFikm.Pepper at-rest storagekeylessActiveSessionfor the unlocked-session lifetime. TheWalletEntry.kind = 'keyless'shape need not gain an at-rest pepper field.senderAddressvia 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?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):
get_chain_auditor()/get_chain_auditor_epoch()— see §3.senderAuditorHintFiat–Shamir binding. Resolved by checking the on-chain verifier and SDK: BCSvector<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
motion-wallet,movement-wallet-adapter, orgmove-multisig. This PR updates the design doc only — including the branch integration plan in §7, which describes the merge but doesn't perform it.confidential_assetMove 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.ca_rotateEncryptionKeyfrom the wallet ↔ dApp surface; rotation is a power-user procedure that runs through the SDK directly in a trusted environment.aptos:*tomovement:*wallet-standard feature names. Noted as ecosystem-coordinated future work in §5.