Skip to content

Support for Confidential MPT (XLS-0096) - #3364

Open
kuan121 wants to merge 25 commits into
mainfrom
confidential-mpts
Open

Support for Confidential MPT (XLS-0096)#3364
kuan121 wants to merge 25 commits into
mainfrom
confidential-mpts

Conversation

@kuan121

@kuan121 kuan121 commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

High Level Overview of Change

This PR adds support for Confidential Multi-Purpose Tokens (XLS-0096).

Confidential MPT represents an MPT balance on-ledger as EC-ElGamal ciphertexts and uses zero-knowledge proofs, allowing validators to verify transfers (no overdraft, conservation of value) without learning the amounts. This change adds the wire-format definitions, the transaction and ledger models, field validation, fee handling, and a high-level builder API that encapsulates the cryptography. All changes are additive and do not alter existing behavior.

Components:

  • @xrplf/mpt-crypto (new package). A hex-in/hex-out TypeScript API over a vendored WebAssembly build of the reference C crypto library, providing ElGamal encryption, Pedersen commitments, blinding factors, context hashes, and the four proof builders (convert, convert-back, send, clawback). It is an optional peer dependency of xrpl, loaded lazily, and is intended to be published to the npm registry.
  • ripple-binary-codec. Definitions for the five new transaction types (ConfidentialMPTConvert, MergeInbox, ConvertBack, Send, Clawback), their fields, and the tecBAD_PROOF and temBAD_CIPHERTEXT result codes.
  • Transaction models (xrpl). The five ConfidentialMPT* interfaces and validate* functions, the tfMPTCanConfidentialAmount flag on MPTokenIssuanceCreate, and the IssuerEncryptionKey and AuditorEncryptionKey fields on MPTokenIssuanceSet.
  • Ledger models (xrpl). Confidential fields on MPToken (inbox, spending, and version balances; issuer- and auditor-encrypted balances; holder encryption key) and on MPTokenIssuance (issuer and auditor keys, outstanding amount).
  • Builder API (xrpl/confidential). The prepareConfidential* builders, which read the required ledger state, generate the ciphertexts, commitments, and proofs, and return an unsigned transaction. getConfidentialBalance decrypts a holder's spendable balance.
  • Fees (autofill). Confidential transactions carry a surcharge; the autofilled fee is base × (10 + signerCount), matching rippled.

Context of Change

This implements the client side of XLS-0096, tracking rippled PR #5860 and the xrpl-py reference implementation (branch confidential-mpt).

Key design decisions:

  • Cryptography is isolated in an optional package. The proof system requires a large WebAssembly library. Keeping it in @xrplf/mpt-crypto as an optional peer dependency, loaded lazily by the xrpl/confidential subpath, ensures that xrpl users who do not use Confidential MPT incur no additional dependency or load cost.
  • Encryption keys reuse the existing key infrastructure. The ElGamal encryption key is a distinct secp256k1 keypair, derived with ripple-keypairs. No new key type is introduced, the key is recoverable from a seed, and the approach is consistent with the Java SDK (xrpl4j).
  • Builders return sequence-pinned transactions. Each proof's context hash is bound to the transaction's identity (account, issuance, sequence, destination, balance version). The builders therefore pin Sequence, which must not be changed before submission.
  • The vendored WASM is version-pinned. The committed build must match the mpt-crypto version pinned by rippled; a mismatch results in tecBAD_PROOF.

Type of Change

  • New feature (non-breaking change which adds functionality)
  • Tests (You added tests for code that already exists, or your new feature included in this PR)

Did you update HISTORY.md?

  • Yes

Added Confidential MPT entries to the Unreleased sections of packages/xrpl/HISTORY.md and packages/ripple-binary-codec/HISTORY.md.

Test Plan

Unit, codec, and fee tests — run in CI with no additional setup (npm test):

  • packages/xrpl/test/models/ConfidentialMPT*.test.ts and the MPTokenIssuanceCreate / MPTokenIssuanceSet additions: each validate* function accepts well-formed transactions and rejects malformed fields.
  • packages/ripple-binary-codec/test/confidential-mpt.test.ts: encode/decode round-trips for the new transaction types and fields.
  • packages/xrpl/test/client/autofill.test.ts: the confidential fee calculation.

Integration testspackages/xrpl/test/integration/transactions/confidentialMPT*.test.ts: one file per transaction type plus a four-party lifecycle file, sharing confidentialMPTUtils.ts. These require a rippled with the MPTokensV1, Clawback, and ConfidentialTransfer amendments enabled. They run as part of npm run test:integration against the rippleci/xrpld develop image once #5860 is merged, and were verified against a local standalone rippled built from #5860: all five transaction types and the full four-party lifecycle (convert → merge → send → convert-back → clawback, with auditor disclosure) pass.

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR adds Confidential Multi-Purpose Token support across XRPL.js, including WASM-backed cryptography, codec definitions, transaction models and validators, confidential builders, optional loading, fee handling, amendment configuration, and unit/integration coverage.

Changes

Confidential MPT implementation

Layer / File(s) Summary
Cryptography package and WASM runtime
packages/mpt-crypto/*
Adds typed WASM loading, memory marshalling, hex conversion, cryptographic primitives, context hashes, proof generation, package configuration, and public exports.
Binary codec definitions
packages/ripple-binary-codec/src/enums/definitions.json, packages/ripple-binary-codec/test/confidential-mpt.test.ts
Adds Confidential MPT fields, transaction types, result codes, and encode/decode round-trip tests.
XRPL models and validation
packages/xrpl/src/models/ledger/*, packages/xrpl/src/models/transactions/*, packages/xrpl/test/models/*
Adds confidential ledger fields, five transaction models, validators, issuance flags and key validation, transaction dispatch wiring, and model tests.
Ledger integration and builders
packages/xrpl/src/confidential/*, packages/xrpl/package.json, packages/xrpl/tsconfig.build.json
Adds lazy optional crypto loading, ledger queries, balance decryption, parameter types, builders for convert, convert-back, send, clawback, and merge-inbox transactions, and build/package wiring.
Fees and end-to-end verification
packages/xrpl/src/sugar/autofill.ts, packages/xrpl/test/client/*, packages/xrpl/test/integration/*, .ci-config/xrpld.cfg
Adds confidential transaction fee handling, amendment configuration, and integration coverage for individual operations and the full multi-party lifecycle.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • XRPLF/xrpl.js#3081: Extends the same MPTokenIssuance flag and mutability machinery used by the confidential issuance changes.

Suggested reviewers: achowdhry-ripple, khancode

Poem

🐰 Confidential coins hop through the night,
Wrapped in proofs and encrypted light.
Builders prepare each secret flow,
While hidden balances safely grow.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and clearly matches the PR’s main change: adding Confidential MPT support.
Description check ✅ Passed The description covers the required sections and is mostly complete, including overview, context, change type, history, and test plan.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch confidential-mpts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kuan121
kuan121 marked this pull request as ready for review June 5, 2026 13:51
/** Free every allocation made through this marshaller. */
public dispose(): void {
for (const ptr of this.ptrs) {
this.mod._free(ptr)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: LOW

The dispose() method frees WASM heap allocations containing private keys and blinding factors without first zeroing the memory. Private key bytes (written via allocBytes in every proof/decrypt call) remain readable in the freed WASM heap until overwritten by later allocations, extending the window for key material extraction by same-process code (e.g., a compromised dependency reading HEAPU8).
Helpful? Add 👍 / 👎

💡 Fix Suggestion

Suggestion: To zero sensitive key material before freeing, the Marshaller must track the size of each allocation alongside its pointer. Change the ptrs field from number[] to an array of {ptr: number, size: number} tuples (line 45). Update alloc() (line 55) and allocBytes() (line 63) to push {ptr, size} instead of just ptr. Then, in dispose(), zero each region before freeing:

public dispose(): void {
  for (const { ptr, size } of this.ptrs) {
    this.mod.HEAPU8.fill(0, ptr, ptr + size)
    this.mod._free(ptr)
  }
  this.ptrs.length = 0
}

Specifically:

  1. Line 45: private readonly ptrs: Array<{ptr: number, size: number}> = []
  2. Line 55: this.ptrs.push({ptr, size})
  3. Line 63: this.ptrs.push({ptr, size: data.length})
  4. Lines 132-137: Update dispose() as shown above to HEAPU8.fill(0, ...) before _free().

This ensures private keys and blinding factors written via allocBytes are overwritten with zeros on the WASM heap before the memory is released back to the allocator.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
packages/ripple-binary-codec/test/confidential-mpt.test.ts (1)

36-139: 💤 Low value

ESLint func-names violations: consider using named function expressions.

The project's ESLint configuration requires named functions in test callbacks. All nine it() and one describe() callback currently use anonymous functions, triggering the func-names rule.

🎨 Example fix for consistency
-  it('round-trips ConfidentialMPTConvert (all fields)', function () {
+  it('round-trips ConfidentialMPTConvert (all fields)', function roundTripConfidentialMPTConvert() {
     assertRoundTrip({
       TransactionType: 'ConfidentialMPTConvert',

Apply the same pattern to the remaining eight tests and the describe() block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ripple-binary-codec/test/confidential-mpt.test.ts` around lines 36 -
139, The test callbacks currently use anonymous functions (describe(...) and
each it(...)) which violates ESLint func-names; replace the anonymous callbacks
with named function expressions (e.g., name the describe callback like function
confidentialMptSuite() and each it callback with descriptive names such as
function roundTripsConfidentialMPTConvert(), function
roundTripsConfidentialMPTConvertBack(), function
roundTripsConfidentialMPTSend(), function roundTripsConfidentialMPTClawback(),
function roundTripsConfidentialMPTMergeInbox(), function
roundTripsMPTokenLedgerEntryWithConfidentialFields(), function
roundTripsMPTokenIssuanceLedgerEntryWithConfidentialFields(), and function
decodesTecBadProofInTransactionMetadata()) so the symbols describe and the nine
it calls keep the same behavior but satisfy func-names.
packages/mpt-crypto/package.json (1)

26-31: 💤 Low value

Consider declaring devDependencies explicitly.

The scripts reference tsc, jest, and eslint, but the package manifest does not declare these as devDependencies. While they may be hoisted from the workspace root in a Lerna monorepo, explicitly declaring them improves clarity and ensures the package's tooling requirements are documented.

📦 Suggested devDependencies block
   "engines": {
     "node": ">= 18"
-  }
+  },
+  "devDependencies": {
+    "typescript": "*",
+    "jest": "*",
+    "eslint": "*"
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mpt-crypto/package.json` around lines 26 - 31, The package.json
scripts ("build", "test", "lint", "clean") call tsc, jest and eslint but those
tools are not declared in this package's devDependencies; add explicit
devDependencies entries for at least typescript, jest, and eslint (and any
companion types/test runtime packages you require such as `@types/jest` or
ts-jest) to package.json so the package documents and installs its tooling
requirements even if the workspace root hoists them.
packages/xrpl/test/models/ConfidentialMPTConvert.test.ts (1)

1-172: ⚡ Quick win

Consider adding test coverage for missing IssuerEncryptedAmount.

The test suite covers most required fields, but IssuerEncryptedAmount (a required field per line 45 in ConfidentialMPTConvert.ts) has no corresponding "missing field" test. While the existing coverage is reasonable, adding this test would ensure all required encryption fields are verified.

✅ Suggested test case
+  it(`throws w/ missing IssuerEncryptedAmount`, function () {
+    assertInvalid(
+      {
+        TransactionType: 'ConfidentialMPTConvert',
+        Account: ACCOUNT,
+        MPTokenIssuanceID: MPT_ISSUANCE_ID,
+        MPTAmount: '100',
+        HolderEncryptedAmount: CIPHERTEXT,
+        BlindingFactor: BLINDING,
+      },
+      'ConfidentialMPTConvert: missing field IssuerEncryptedAmount',
+    )
+  })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/xrpl/test/models/ConfidentialMPTConvert.test.ts` around lines 1 -
172, Add a new test case in the ConfidentialMPTConvert suite that asserts a
missing IssuerEncryptedAmount triggers the expected validation error; use the
existing assertInvalid helper with a tx object containing TransactionType:
'ConfidentialMPTConvert', Account: ACCOUNT, MPTokenIssuanceID: MPT_ISSUANCE_ID,
MPTAmount: '100', HolderEncryptedAmount: CIPHERTEXT, BlindingFactor: BLINDING
(omit IssuerEncryptedAmount) and expect the message 'ConfidentialMPTConvert:
missing field IssuerEncryptedAmount' to mirror the other "missing field" tests
for consistency with validateConfidentialMPTConvert.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/mpt-crypto/package.json`:
- Around line 6-10: Add provenance documentation for the packaged WASM by
creating or updating a short README or inline comment that explains how
packages/mpt-crypto/wasm/mpt_crypto.wasm was produced or obtained: state the
upstream source/repo and commit/tag, the exact build command/tooling (e.g.,
Emscripten build command and environment), or the download URL and version, and
include integrity details such as a SHA256 checksum and any verification steps;
ensure this note is referenced from packages/mpt-crypto/src/module.ts (where the
Emscripten glue expects mpt_crypto.wasm) and that
packages/mpt-crypto/package.json's "files" list remains accurate.

In `@packages/mpt-crypto/src/marshal.ts`:
- Around line 52-57: The alloc function does not check for _malloc failure and
may use ptr==0; update alloc (and allocBytes) to verify const ptr =
this.mod._malloc(size) is non-zero before using it, throw or return a clear
error if ptr === 0, and avoid calling this.mod.HEAPU8.fill or pushing ptr into
this.ptrs when allocation failed (referencing the alloc and allocBytes methods,
this.mod._malloc, this.mod.HEAPU8.fill, and this.ptrs to locate the changes).

In `@packages/mpt-crypto/src/module.ts`:
- Line 124: The call to instance._mpt_secp256k1_context() returns a numeric
status that must be validated; update the code where
instance._mpt_secp256k1_context() is invoked to capture its return value, check
for non-zero (failure) and throw or return an error (or call process exit) when
initialization fails, following the same pattern used in context.ts (check
return === 0 for success). Ensure the check references the exact function name
_mpt_secp256k1_context and provides a clear error message denoting secp256k1
context initialization failure.

In `@packages/ripple-binary-codec/src/enums/definitions.json`:
- Around line 3492-3500: The BlindingFactor field currently uses nth: 39 which
collides with ReferenceHolding's nth: 39 for the Hash256 type; update
BlindingFactor's nth to the next unused ordinal (e.g., 40) in definitions.json
so each Hash256 field has a unique nth, ensuring the FieldLookup key
((Hash256_typeOrdinal << 16) | nth) no longer collides between BlindingFactor
and ReferenceHolding.

---

Nitpick comments:
In `@packages/mpt-crypto/package.json`:
- Around line 26-31: The package.json scripts ("build", "test", "lint", "clean")
call tsc, jest and eslint but those tools are not declared in this package's
devDependencies; add explicit devDependencies entries for at least typescript,
jest, and eslint (and any companion types/test runtime packages you require such
as `@types/jest` or ts-jest) to package.json so the package documents and installs
its tooling requirements even if the workspace root hoists them.

In `@packages/ripple-binary-codec/test/confidential-mpt.test.ts`:
- Around line 36-139: The test callbacks currently use anonymous functions
(describe(...) and each it(...)) which violates ESLint func-names; replace the
anonymous callbacks with named function expressions (e.g., name the describe
callback like function confidentialMptSuite() and each it callback with
descriptive names such as function roundTripsConfidentialMPTConvert(), function
roundTripsConfidentialMPTConvertBack(), function
roundTripsConfidentialMPTSend(), function roundTripsConfidentialMPTClawback(),
function roundTripsConfidentialMPTMergeInbox(), function
roundTripsMPTokenLedgerEntryWithConfidentialFields(), function
roundTripsMPTokenIssuanceLedgerEntryWithConfidentialFields(), and function
decodesTecBadProofInTransactionMetadata()) so the symbols describe and the nine
it calls keep the same behavior but satisfy func-names.

In `@packages/xrpl/test/models/ConfidentialMPTConvert.test.ts`:
- Around line 1-172: Add a new test case in the ConfidentialMPTConvert suite
that asserts a missing IssuerEncryptedAmount triggers the expected validation
error; use the existing assertInvalid helper with a tx object containing
TransactionType: 'ConfidentialMPTConvert', Account: ACCOUNT, MPTokenIssuanceID:
MPT_ISSUANCE_ID, MPTAmount: '100', HolderEncryptedAmount: CIPHERTEXT,
BlindingFactor: BLINDING (omit IssuerEncryptedAmount) and expect the message
'ConfidentialMPTConvert: missing field IssuerEncryptedAmount' to mirror the
other "missing field" tests for consistency with validateConfidentialMPTConvert.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cf049123-1b64-4467-9fdf-db79775c67d5

📥 Commits

Reviewing files that changed from the base of the PR and between f9d0a60 and 48d2dcf.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • packages/mpt-crypto/wasm/mpt_crypto.wasm is excluded by !**/*.wasm
📒 Files selected for processing (59)
  • lerna.json
  • packages/mpt-crypto/README.md
  • packages/mpt-crypto/eslint.config.js
  • packages/mpt-crypto/package.json
  • packages/mpt-crypto/src/constants.ts
  • packages/mpt-crypto/src/context.ts
  • packages/mpt-crypto/src/hex.ts
  • packages/mpt-crypto/src/index.ts
  • packages/mpt-crypto/src/internal.ts
  • packages/mpt-crypto/src/marshal.ts
  • packages/mpt-crypto/src/module.ts
  • packages/mpt-crypto/src/primitives.ts
  • packages/mpt-crypto/src/proofs.ts
  • packages/mpt-crypto/src/runtime.ts
  • packages/mpt-crypto/src/types.ts
  • packages/mpt-crypto/tsconfig.build.json
  • packages/mpt-crypto/tsconfig.eslint.json
  • packages/mpt-crypto/tsconfig.json
  • packages/mpt-crypto/wasm/mpt_crypto.js
  • packages/ripple-binary-codec/HISTORY.md
  • packages/ripple-binary-codec/src/enums/definitions.json
  • packages/ripple-binary-codec/test/confidential-mpt.test.ts
  • packages/xrpl/HISTORY.md
  • packages/xrpl/package.json
  • packages/xrpl/src/confidential/convert.ts
  • packages/xrpl/src/confidential/index.ts
  • packages/xrpl/src/confidential/ledger.ts
  • packages/xrpl/src/confidential/loader.ts
  • packages/xrpl/src/confidential/transfer.ts
  • packages/xrpl/src/confidential/types.ts
  • packages/xrpl/src/models/ledger/MPToken.ts
  • packages/xrpl/src/models/ledger/MPTokenIssuance.ts
  • packages/xrpl/src/models/transactions/ConfidentialMPTClawback.ts
  • packages/xrpl/src/models/transactions/ConfidentialMPTConvert.ts
  • packages/xrpl/src/models/transactions/ConfidentialMPTConvertBack.ts
  • packages/xrpl/src/models/transactions/ConfidentialMPTMergeInbox.ts
  • packages/xrpl/src/models/transactions/ConfidentialMPTSend.ts
  • packages/xrpl/src/models/transactions/MPTokenIssuanceCreate.ts
  • packages/xrpl/src/models/transactions/MPTokenIssuanceSet.ts
  • packages/xrpl/src/models/transactions/common.ts
  • packages/xrpl/src/models/transactions/index.ts
  • packages/xrpl/src/models/transactions/transaction.ts
  • packages/xrpl/src/sugar/autofill.ts
  • packages/xrpl/test/client/autofill.test.ts
  • packages/xrpl/test/integration/confidentialMPTUtils.ts
  • packages/xrpl/test/integration/transactions/confidentialMPTClawback.test.ts
  • packages/xrpl/test/integration/transactions/confidentialMPTConvert.test.ts
  • packages/xrpl/test/integration/transactions/confidentialMPTConvertBack.test.ts
  • packages/xrpl/test/integration/transactions/confidentialMPTLifecycle.test.ts
  • packages/xrpl/test/integration/transactions/confidentialMPTMergeInbox.test.ts
  • packages/xrpl/test/integration/transactions/confidentialMPTSend.test.ts
  • packages/xrpl/test/models/ConfidentialMPTClawback.test.ts
  • packages/xrpl/test/models/ConfidentialMPTConvert.test.ts
  • packages/xrpl/test/models/ConfidentialMPTConvertBack.test.ts
  • packages/xrpl/test/models/ConfidentialMPTMergeInbox.test.ts
  • packages/xrpl/test/models/ConfidentialMPTSend.test.ts
  • packages/xrpl/test/models/MPTokenIssuanceCreate.test.ts
  • packages/xrpl/test/models/MPTokenIssuanceSet.test.ts
  • packages/xrpl/tsconfig.build.json

Comment on lines +6 to +10
"files": [
"dist/*",
"wasm/*",
"src/*"
],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check for the mpt_crypto.wasm binary in the package

fd -t f 'mpt_crypto.wasm' packages/mpt-crypto/

Repository: XRPLF/xrpl.js

Length of output: 98


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== WASM files =="
ls -la packages/mpt-crypto/wasm || true
echo

echo "== References to mpt_crypto.wasm =="
rg -n "mpt_crypto\.wasm" -S packages/mpt-crypto || true
echo

echo "== WASM provenance/build docs (best-effort search) =="
rg -n "(build|builds|download|provenance|source|compile|generated|toolchain|wasm)" -S packages/mpt-crypto || true

Repository: XRPLF/xrpl.js

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== packages/mpt-crypto/src/module.ts around locateFile / wasm comment =="
sed -n '80,140p' packages/mpt-crypto/src/module.ts

echo
echo "== Search for any explicit build/download provenance keywords =="
rg -n "(vendored|download|build|compile|emcc|emscripten|source|provenance|generated)" packages/mpt-crypto || true

echo
echo "== Files array in packages/mpt-crypto/package.json =="
cat packages/mpt-crypto/package.json

echo
echo "== Any README/docs mentioning wasm =="
fd -t f -e md -e txt packages/mpt-crypto | xargs -r rg -n "wasm|mpt_crypto\.wasm" || true

Repository: XRPLF/xrpl.js

Length of output: 50769


Document the provenance of mpt_crypto.wasm (binary is present and packaged)

  • packages/mpt-crypto/wasm/mpt_crypto.wasm exists and is covered by packages/mpt-crypto/package.json’s "files": ["wasm/*", ...].
  • packages/mpt-crypto/src/module.ts notes the Emscripten glue locates mpt_crypto.wasm next to mpt_crypto.js, but it doesn’t explain how the WASM was built/downloaded.
  • Add a short README/comment describing the WASM’s provenance (build command/tooling and source it comes from, or download process/checksums).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mpt-crypto/package.json` around lines 6 - 10, Add provenance
documentation for the packaged WASM by creating or updating a short README or
inline comment that explains how packages/mpt-crypto/wasm/mpt_crypto.wasm was
produced or obtained: state the upstream source/repo and commit/tag, the exact
build command/tooling (e.g., Emscripten build command and environment), or the
download URL and version, and include integrity details such as a SHA256
checksum and any verification steps; ensure this note is referenced from
packages/mpt-crypto/src/module.ts (where the Emscripten glue expects
mpt_crypto.wasm) and that packages/mpt-crypto/package.json's "files" list
remains accurate.

Comment thread packages/mpt-crypto/src/marshal.ts
const factory: ModuleFactory = require('../wasm/mpt_crypto')
const instance = await factory()
// Force one-time initialization of the shared secp256k1 context.
instance._mpt_secp256k1_context()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate the secp256k1 context initialization return code.

The _mpt_secp256k1_context() function returns a number (per the WasmModule interface), but the return value is not checked. Following the pattern used elsewhere (e.g., context.ts lines 30-33), WASM functions return 0 on success and non-zero on failure. If initialization fails, subsequent cryptographic operations may produce incorrect results or crash.

🛡️ Proposed fix to check initialization status
     const factory: ModuleFactory = require('../wasm/mpt_crypto')
     const instance = await factory()
     // Force one-time initialization of the shared secp256k1 context.
-    instance._mpt_secp256k1_context()
+    const status = instance._mpt_secp256k1_context()
+    if (status !== 0) {
+      throw new Error('secp256k1 context initialization failed')
+    }
     return instance
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
instance._mpt_secp256k1_context()
const factory: ModuleFactory = require('../wasm/mpt_crypto')
const instance = await factory()
// Force one-time initialization of the shared secp256k1 context.
const status = instance._mpt_secp256k1_context()
if (status !== 0) {
throw new Error('secp256k1 context initialization failed')
}
return instance
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/mpt-crypto/src/module.ts` at line 124, The call to
instance._mpt_secp256k1_context() returns a numeric status that must be
validated; update the code where instance._mpt_secp256k1_context() is invoked to
capture its return value, check for non-zero (failure) and throw or return an
error (or call process exit) when initialization fails, following the same
pattern used in context.ts (check return === 0 for success). Ensure the check
references the exact function name _mpt_secp256k1_context and provides a clear
error message denoting secp256k1 context initialization failure.

Comment thread packages/ripple-binary-codec/src/enums/definitions.json
@kuan121 kuan121 changed the title Confidential mpts Support Confidential Transfers for Multi-Purpose Tokens (XLS-0096) Jun 5, 2026
@kuan121 kuan121 changed the title Support Confidential Transfers for Multi-Purpose Tokens (XLS-0096) Support Confidential Transfers for MPTs (XLS-0096) Jun 5, 2026
@kuan121

kuan121 commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator Author

/ai-review

@xrplf-ai-reviewer xrplf-ai-reviewer Bot 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.

No issues.

Review by Claude Sonnet 4.6 · Prompt: V15

Comment thread packages/xrpl/src/confidential/convert.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/xrpl/src/models/transactions/MPTokenIssuanceSet.ts (1)

174-178: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject AuditorEncryptionKey unless IssuerEncryptionKey is also set.
This only checks the auditor key’s format; add the cross-field guard so client validation rejects malformed transactions that rippled will reject.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/xrpl/src/models/transactions/MPTokenIssuanceSet.ts` around lines 174
- 178, Update the validation flow containing validateOptionalField for
AuditorEncryptionKey to require IssuerEncryptionKey whenever the auditor key is
provided. Preserve the existing AuditorEncryptionKey format validation, and add
the cross-field guard so transactions with an auditor key but no issuer key are
rejected.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/xrpl/src/models/transactions/MPTokenIssuanceSet.ts`:
- Around line 222-233: Update the mutation classification in the MPToken
issuance validation flow: include IssuerEncryptionKey and AuditorEncryptionKey
in isMutate, then reuse isMutate for the Holder exclusion so confidential key
updates cannot be combined with Holder. Remove the separate
isSetConfidentialKeys condition while preserving the existing no-op checks.

---

Outside diff comments:
In `@packages/xrpl/src/models/transactions/MPTokenIssuanceSet.ts`:
- Around line 174-178: Update the validation flow containing
validateOptionalField for AuditorEncryptionKey to require IssuerEncryptionKey
whenever the auditor key is provided. Preserve the existing AuditorEncryptionKey
format validation, and add the cross-field guard so transactions with an auditor
key but no issuer key are rejected.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: eec64fba-05ae-48fd-b17a-3b94cd5b6a22

📥 Commits

Reviewing files that changed from the base of the PR and between 48d2dcf and acd36c3.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (11)
  • packages/ripple-binary-codec/HISTORY.md
  • packages/ripple-binary-codec/src/enums/definitions.json
  • packages/xrpl/HISTORY.md
  • packages/xrpl/package.json
  • packages/xrpl/src/models/ledger/MPTokenIssuance.ts
  • packages/xrpl/src/models/transactions/MPTokenIssuanceCreate.ts
  • packages/xrpl/src/models/transactions/MPTokenIssuanceSet.ts
  • packages/xrpl/src/models/transactions/common.ts
  • packages/xrpl/src/models/transactions/index.ts
  • packages/xrpl/test/models/MPTokenIssuanceCreate.test.ts
  • packages/xrpl/test/models/MPTokenIssuanceSet.test.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • packages/xrpl/test/models/MPTokenIssuanceCreate.test.ts
  • packages/xrpl/test/models/MPTokenIssuanceSet.test.ts
  • packages/xrpl/src/models/transactions/index.ts
  • packages/xrpl/HISTORY.md
  • packages/xrpl/src/models/ledger/MPTokenIssuance.ts
  • packages/xrpl/src/models/transactions/MPTokenIssuanceCreate.ts
  • packages/ripple-binary-codec/HISTORY.md
  • packages/xrpl/src/models/transactions/common.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/xrpl/src/models/transactions/MPTokenIssuanceSet.ts (1)

174-178: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject AuditorEncryptionKey unless IssuerEncryptionKey is also set.
This only checks the auditor key’s format; add the cross-field guard so client validation rejects malformed transactions that rippled will reject.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/xrpl/src/models/transactions/MPTokenIssuanceSet.ts` around lines 174
- 178, Update the validation flow containing validateOptionalField for
AuditorEncryptionKey to require IssuerEncryptionKey whenever the auditor key is
provided. Preserve the existing AuditorEncryptionKey format validation, and add
the cross-field guard so transactions with an auditor key but no issuer key are
rejected.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/xrpl/src/models/transactions/MPTokenIssuanceSet.ts`:
- Around line 222-233: Update the mutation classification in the MPToken
issuance validation flow: include IssuerEncryptionKey and AuditorEncryptionKey
in isMutate, then reuse isMutate for the Holder exclusion so confidential key
updates cannot be combined with Holder. Remove the separate
isSetConfidentialKeys condition while preserving the existing no-op checks.

---

Outside diff comments:
In `@packages/xrpl/src/models/transactions/MPTokenIssuanceSet.ts`:
- Around line 174-178: Update the validation flow containing
validateOptionalField for AuditorEncryptionKey to require IssuerEncryptionKey
whenever the auditor key is provided. Preserve the existing AuditorEncryptionKey
format validation, and add the cross-field guard so transactions with an auditor
key but no issuer key are rejected.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: eec64fba-05ae-48fd-b17a-3b94cd5b6a22

📥 Commits

Reviewing files that changed from the base of the PR and between 48d2dcf and acd36c3.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (11)
  • packages/ripple-binary-codec/HISTORY.md
  • packages/ripple-binary-codec/src/enums/definitions.json
  • packages/xrpl/HISTORY.md
  • packages/xrpl/package.json
  • packages/xrpl/src/models/ledger/MPTokenIssuance.ts
  • packages/xrpl/src/models/transactions/MPTokenIssuanceCreate.ts
  • packages/xrpl/src/models/transactions/MPTokenIssuanceSet.ts
  • packages/xrpl/src/models/transactions/common.ts
  • packages/xrpl/src/models/transactions/index.ts
  • packages/xrpl/test/models/MPTokenIssuanceCreate.test.ts
  • packages/xrpl/test/models/MPTokenIssuanceSet.test.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • packages/xrpl/test/models/MPTokenIssuanceCreate.test.ts
  • packages/xrpl/test/models/MPTokenIssuanceSet.test.ts
  • packages/xrpl/src/models/transactions/index.ts
  • packages/xrpl/HISTORY.md
  • packages/xrpl/src/models/ledger/MPTokenIssuance.ts
  • packages/xrpl/src/models/transactions/MPTokenIssuanceCreate.ts
  • packages/ripple-binary-codec/HISTORY.md
  • packages/xrpl/src/models/transactions/common.ts
🛑 Comments failed to post (1)
packages/xrpl/src/models/transactions/MPTokenIssuanceSet.ts (1)

222-233: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

sed -n '180,260p' packages/xrpl/src/models/transactions/MPTokenIssuanceSet.ts

Repository: XRPLF/xrpl.js

Length of output: 3043


🏁 Script executed:

rg -n "isSetConfidentialKeys|IssuerEncryptionKey|AuditorEncryptionKey|MutableFlags|Holder|confidential" packages/xrpl/src -S

Repository: XRPLF/xrpl.js

Length of output: 18723


🏁 Script executed:

sed -n '1,180p' packages/xrpl/src/models/transactions/MPTokenIssuanceSet.ts

Repository: XRPLF/xrpl.js

Length of output: 6898


🏁 Script executed:

rg -n "Holder field is not allowed when mutating MPTokenIssuance|Cannot set both DomainID and Holder fields|IssuerEncryptionKey|AuditorEncryptionKey|MPTokenIssuanceSet" packages/xrpl/src test tests -S

Repository: XRPLF/xrpl.js

Length of output: 11512


🏁 Script executed:

git ls-files | rg 'MPTokenIssuanceSet|confidential|XLS|mptoken'

Repository: XRPLF/xrpl.js

Length of output: 1709


🏁 Script executed:

rg -n "MPTokenIssuanceSet|IssuerEncryptionKey|AuditorEncryptionKey|Holder field is not allowed when mutating|Transaction does not change the state" . -S

Repository: XRPLF/xrpl.js

Length of output: 50370


🏁 Script executed:

wc -l packages/xrpl/test/models/MPTokenIssuanceSet.test.ts packages/xrpl/test/integration/transactions/mptokenIssuanceSet.test.ts

Repository: XRPLF/xrpl.js

Length of output: 299


🏁 Script executed:

ast-grep outline packages/xrpl/test/models/MPTokenIssuanceSet.test.ts --view expanded

Repository: XRPLF/xrpl.js

Length of output: 363


🏁 Script executed:

rg -n "Holder.*IssuerEncryptionKey|Holder.*AuditorEncryptionKey|IssuerEncryptionKey.*Holder|AuditorEncryptionKey.*Holder|Holder field is not allowed|mutating MPTokenIssuance|Transaction does not change the state" packages/xrpl/test/models/MPTokenIssuanceSet.test.ts packages/xrpl/test/integration/transactions/mptokenIssuanceSet.test.ts -S

Repository: XRPLF/xrpl.js

Length of output: 917


🏁 Script executed:

sed -n '280,390p' packages/xrpl/test/models/MPTokenIssuanceSet.test.ts

Repository: XRPLF/xrpl.js

Length of output: 3570


🏁 Script executed:

sed -n '1,260p' packages/xrpl/test/integration/transactions/mptokenIssuanceSet.test.ts

Repository: XRPLF/xrpl.js

Length of output: 8835


🏁 Script executed:

rg -n "IssuerEncryptionKey|AuditorEncryptionKey|Holder" packages/xrpl/test/integration/transactions/mptokenIssuanceSet.test.ts packages/xrpl/test/models/MPTokenIssuanceSet.test.ts -S

Repository: XRPLF/xrpl.js

Length of output: 1901


🏁 Script executed:

rg -n "Holder.*IssuerEncryptionKey|Holder.*AuditorEncryptionKey|IssuerEncryptionKey.*Holder|AuditorEncryptionKey.*Holder|confidential keys|registered so confidential amounts can be encrypted" packages/xrpl/docs packages/xrpl/src packages/xrpl/test -S

Repository: XRPLF/xrpl.js

Length of output: 587


🏁 Script executed:

sed -n '120,170p' packages/xrpl/test/models/MPTokenIssuanceSet.test.ts

Repository: XRPLF/xrpl.js

Length of output: 1722


Treat confidential key updates as mutations. IssuerEncryptionKey / AuditorEncryptionKey already count as state changes in the no-op check, but they’re excluded from isMutate, so Holder can still be combined with them. Fold both keys into isMutate and reuse it for the holder exclusion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/xrpl/src/models/transactions/MPTokenIssuanceSet.ts` around lines 222
- 233, Update the mutation classification in the MPToken issuance validation
flow: include IssuerEncryptionKey and AuditorEncryptionKey in isMutate, then
reuse isMutate for the Holder exclusion so confidential key updates cannot be
combined with Holder. Remove the separate isSetConfidentialKeys condition while
preserving the existing no-op checks.

Comment thread packages/mpt-crypto/src/module.ts
// value, so cost scales with the amount, not the window. These are the library's
// recommended defaults (secp256k1_mpt.h): [0, 1_000_000]. High must be < UINT64_MAX.
const DECRYPT_RANGE_LOW = 0n
const DECRYPT_RANGE_HIGH = 1_000_000n

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: LOW

The ElGamal decryption range is hardcoded to [0, 1_000_000] with no way for callers to override it. The decryptAmount function is called internally by prepareConfidentialConvertBack, prepareConfidentialSend, and prepareConfidentialClawback to recover the balance witness needed for proof generation. If a holder's confidential balance exceeds 1,000,000 (possible since issuances allow MaximumAmount up to max int64), decryption fails and those operations become impossible — effectively locking the holder's funds in the confidential balance with no client-side recovery path.
Helpful? Add 👍 / 👎

💡 Fix Suggestion

Suggestion: Make the decrypt range configurable by adding optional rangeLow and rangeHigh parameters to the decryptAmount function (around line 79), using the current constants as defaults. For example, change the signature to:

export async function decryptAmount(
  ciphertext: string,
  privateKey: string,
  rangeLow: bigint = DECRYPT_RANGE_LOW,
  rangeHigh: bigint = DECRYPT_RANGE_HIGH,
): Promise<bigint> {

Then pass rangeLow and rangeHigh (instead of the hardcoded constants) to mod._mpt_decrypt_amount at lines 94-95. You should also re-export these default constants so callers are aware of the defaults. Finally, update the callers in transfer.ts, convert.ts, and ledger.ts to optionally accept and forward range parameters if needed.

@kuan121 kuan121 changed the title Support Confidential Transfers for MPTs (XLS-0096) Support for Confidential MPT (XLS-0096) Jul 16, 2026
kuan121 added 2 commits July 19, 2026 13:16
- rename tfMPTCanConfidentialAmount -> tfMPTCanHoldConfidentialBalance
- add confidential MutableFlags (Set 0x40, Create 0x80) + lsf/lsmf ledger flags
- validate exact ZKProof sizes (Convert 64, ConvertBack 816, Send 946, Clawback 64)
- validate MPTAmount range/zero (Convert allows 0; ConvertBack/Clawback reject 0)
- enforce Convert HolderEncryptionKey<->ZKProof and Set Auditor->Issuer key pairing
- pair ZKProof with HolderEncryptionKey in the Convert builder (temMALFORMED)
- null-check WASM _malloc; document vendored WASM provenance
}

async function main() {
console.log(`Confidential MPT lifecycle on ${SERVER}\n`)
* @returns A confidential keypair: a 33-byte hex `publicKey` and the bare
* 32-byte hex `privateKey` scalar.
*/
export function deriveConfidentialKeypair(seed?: string): ConfidentialKeypair {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Severity: MEDIUM

The documentation explicitly encourages reusing the account's signing seed ("including an account's own signing seed"). For secp256k1 wallets, Wallet.fromSeed(seed) and deriveConfidentialKeypair(seed) both call deriveKeypair(seed, { algorithm: secp256k1 }), producing the same private key scalar. This signing key is then passed into the unverified WASM binary for ElGamal decryption and proof generation across multiple builder functions, expanding the signing key's attack surface beyond the signing library to include the opaque WASM module.
Helpful? Add 👍 / 👎

💡 Fix Suggestion

Suggestion: This is a cryptographic key separation concern. To properly address it:

  1. Update the JSDoc (lines 16-18) to remove the encouragement to reuse the account's signing seed. Instead, warn users that reusing the signing seed expands its attack surface to include the WASM module and recommend using a distinct seed for confidential keys.

  2. Ideally, add domain separation in the derivation: even when the same seed is provided, derive a distinct key for confidential use by applying an additional key derivation step (e.g., HMAC-based domain separation such as HMAC-SHA512('confidential-mpt', rawPrivateKey) before using the scalar). This ensures the signing key and the confidential key are cryptographically independent even when derived from the same seed, following the principle of key separation across protocols.

  3. At minimum, if domain separation is not feasible due to cross-SDK compatibility constraints, update the documentation to strongly discourage reusing the signing seed and document the risk:

 * ⚠️ For best security, use a **dedicated** seed for confidential keys rather
 * than reusing your account's signing seed. Reusing the signing seed extends
 * the private key's trust boundary to the WASM proof-generation module.

* Enables the lsfMPTCanHoldConfidentialBalance flag on the issuance. Allows
* holders to hold confidential (encrypted) balances of this token.
*/
tfMPTSetCanHoldConfidentialBalance = 0x00000100,

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.

@kuan121 should tfMPTSetCanHoldConfidentialBalance be part of the mutable flags instead?
https://xls.xrpl.org/xls/XLS-0096-confidential-mpt.html#12-transaction-mptokenissuanceset
section 12.2.1

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@florent-uzio As part of the latest Dynamic MPT update, we no longer have mutable flags. See this PR to update the XLS spec for Dynamic MPT. I included only tfMPTSetCanHoldConfidentialBalance here since it's relevant to Confidential MPT.

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.

3 participants