From f4a16f6aa5998ef8252a257de4c5271bb2ca9b48 Mon Sep 17 00:00:00 2001 From: Maria Shodunke Date: Fri, 9 Jan 2026 13:25:33 +0000 Subject: [PATCH] Add Tutorials for Confidential Transfers --- .../confidential-transfers/README.md | 3 + .../confidential-transfers/py/README.md | 117 ++++++ .../py/issue-mpt-confidential.py | 333 ++++++++++++++++++ .../py/requirements.txt | 2 + .../index.page.tsx | 1 - .../issue-mpt-with-confidential-transfers.md | 262 ++++++++++++++ sidebars.yaml | 4 + 7 files changed, 721 insertions(+), 1 deletion(-) create mode 100644 _code-samples/confidential-transfers/README.md create mode 100644 _code-samples/confidential-transfers/py/README.md create mode 100644 _code-samples/confidential-transfers/py/issue-mpt-confidential.py create mode 100644 _code-samples/confidential-transfers/py/requirements.txt create mode 100644 docs/xls-96-confidential-transfers/tutorials/issue-mpt-with-confidential-transfers.md diff --git a/_code-samples/confidential-transfers/README.md b/_code-samples/confidential-transfers/README.md new file mode 100644 index 0000000..5bab6b7 --- /dev/null +++ b/_code-samples/confidential-transfers/README.md @@ -0,0 +1,3 @@ +# Confidential Transfers Code Samples + +This directory contains code samples demonstrating how to make confidential transfers on the XRP Ledger. diff --git a/_code-samples/confidential-transfers/py/README.md b/_code-samples/confidential-transfers/py/README.md new file mode 100644 index 0000000..8614019 --- /dev/null +++ b/_code-samples/confidential-transfers/py/README.md @@ -0,0 +1,117 @@ +# Confidential Transfers Examples (Python) + +This directory contains Python examples demonstrating how to issue Multi-Purpose Tokens (MPTs) with confidential transfer capabilities on the XRP Ledger using XLS-96. + +## Setup + +Install dependencies before running any examples: + +```bash +python -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` + +> **Note**: xrpl-py uses the [mpt-crypto](https://github.com/XRPLF/mpt-crypto) C extension for ElGamal encryption and zero-knowledge proofs. It should be automatically compiled when installing `xrpl-py` with confidential transfer support. + +## Issue MPT with Confidential Transfers + +```sh +python issue-mpt-confidential.py +``` + +The script demonstrates the complete flow of creating an MPT with confidential transfer capabilities. It should output: + +```sh +=== Generating Accounts === +Issuer: rD3WNSDgCvjR2BV99iJwuNbWaHw79FnbAe +Issuer Second Account: r37aF5y8LmnevoSazEPBxcuK9q16BxygR5 +Auditor: rJ8PzfXouMh9p7pu2qeYQQqNogLZw3E3ZZ + +=== Generating ElGamal Keypairs === +Issuer ElGamal Public Key: 0235DF4B6A3B01BFD8981BB00D7BF93C45B304756E6C0B1BB90DC2C2814C9A5079 +Issuer Second Account ElGamal Public Key: 0229FB94F953F527D256156F628BEBF0CC0CF142210B332A7E3EAAF0239E9CA6A3 +Auditor ElGamal Public Key: 03DCF9E4AE3425877A8E31FECB7E0D4039E8D6C0A6DC763F8A1FE89A5B5A8C8551 + +=== Creating MPT Issuance... === +Submitting MPTokenIssuanceCreate transaction... +MPT issuance created successfully! +MPT Issuance ID: 0035245E850CD28DE38957CCD3AA04AB7AEE25B2F218D0BE + +=== Registering ElGamal Keys On-Ledger... === +Submitting MPTokenIssuanceSet transaction... +{ + "account": "rD3WNSDgCvjR2BV99iJwuNbWaHw79FnbAe", + "transaction_type": "MPTokenIssuanceSet", + "signing_pub_key": "", + "mptoken_issuance_id": "0035245E850CD28DE38957CCD3AA04AB7AEE25B2F218D0BE", + "issuer_elgamal_public_key": "0235DF4B6A3B01BFD8981BB00D7BF93C45B304756E6C0B1BB90DC2C2814C9A5079", + "auditor_elgamal_public_key": "03DCF9E4AE3425877A8E31FECB7E0D4039E8D6C0A6DC763F8A1FE89A5B5A8C8551" +} +ElGamal keys registered successfully! + +=== Sending public tokens to issuer second account... === +Submitting MPTokenAuthorize transaction... +Issuer second account authorized CTST MPT successfully! + +Submitting Payment transaction... +Payment successful! +Issuer Sent 1000 CTST tokens to r37aF5y8LmnevoSazEPBxcuK9q16BxygR5 + +=== Converting Public Balance to Confidential... === +Generating zero-knowledge proof... +Encrypting amount for holder... +Encrypting amount for issuer... +Encrypting amount for auditor... + +Submitting ConfidentialMPTConvert transaction... +{ + "account": "r37aF5y8LmnevoSazEPBxcuK9q16BxygR5", + "transaction_type": "ConfidentialMPTConvert", + "signing_pub_key": "", + "mptoken_issuance_id": "0035245E850CD28DE38957CCD3AA04AB7AEE25B2F218D0BE", + "mpt_amount": 1000, + "holder_encrypted_amount": "02AA26FCE527014BA9A148721074E4E2D6760399C6C3512ADD51FE14526CF259C9021C9CF39776966B2D095011296BB4D52414B8123463A619E9F7347FD11D6F72F0", + "issuer_encrypted_amount": "02AA26FCE527014BA9A148721074E4E2D6760399C6C3512ADD51FE14526CF259C903E1FFED64B7F41543AF73B5F9B2B16FDD3BA8D9B477FD0993DAE84FEFCFA94032", + "blinding_factor": "3E9185BC123763C3F0FA564422768C6BC42B9D1B6B0D722EA81F46347DB6CAC7", + "holder_elgamal_public_key": "0229FB94F953F527D256156F628BEBF0CC0CF142210B332A7E3EAAF0239E9CA6A3", + "auditor_encrypted_amount": "02AA26FCE527014BA9A148721074E4E2D6760399C6C3512ADD51FE14526CF259C9029901F33957FD0DEC77DCF25E96E7EDDDC24AA7AE533398CA119297421B97343D", + "zk_proof": "02DD6A920AA90AA91500D04757D2E51F788F2F751FCABF3698E969606741D206F66C36C37DE4BAAF6EEF423F0C7D08AFE8DB382314C041755B999AB1223435E6AF" +} +Conversion successful! +Converted 1000 CTST tokens to confidential balance. + +=== Merging Inbox into Spending Balance... === +Submitting ConfidentialMPTMergeInbox transaction... +{ + "account": "r37aF5y8LmnevoSazEPBxcuK9q16BxygR5", + "transaction_type": "ConfidentialMPTMergeInbox", + "signing_pub_key": "", + "mptoken_issuance_id": "0035245E850CD28DE38957CCD3AA04AB7AEE25B2F218D0BE" +} +Merge successful! + +=== Verifying Confidential Balances === +Querying issuer second account's MPToken object... + +Issuer second account MPToken object: +{ + "Account": "r37aF5y8LmnevoSazEPBxcuK9q16BxygR5", + "AuditorEncryptedBalance": "02AA26FCE527014BA9A148721074E4E2D6760399C6C3512ADD51FE14526CF259C9029901F33957FD0DEC77DCF25E96E7EDDDC24AA7AE533398CA119297421B97343D", + "ConfidentialBalanceInbox": "03CE04EA1FC44C65E9DE022DCD08B9DDDFA881922909C2A080E7171CF8574D9A2003F481D7BBAB214E638BE548588B71F7E280EE64C8EFD0424D80FCF5640DBB1A68", + "ConfidentialBalanceSpending": "02595D92B73CD0A7DDB5D81B5BAF668C5367DDAF499AD8D0EF556D3526AA2FE5A90247149548FF9B26862AB9D980D2ED0AFE5E2B6F940623B1EA97C2868F2E05CAF4", + "ConfidentialBalanceVersion": 1, + "Flags": 0, + "HolderElGamalPublicKey": "0229FB94F953F527D256156F628BEBF0CC0CF142210B332A7E3EAAF0239E9CA6A3", + "IssuerEncryptedBalance": "02AA26FCE527014BA9A148721074E4E2D6760399C6C3512ADD51FE14526CF259C903E1FFED64B7F41543AF73B5F9B2B16FDD3BA8D9B477FD0993DAE84FEFCFA94032", + "LedgerEntryType": "MPToken", + "MPTokenIssuanceID": "0035245E850CD28DE38957CCD3AA04AB7AEE25B2F218D0BE", + "OwnerNode": "0", + "PreviousTxnID": "1826376084C31D89B90946306E0D70D0557C6B404DBE4356675269F6660B5432", + "PreviousTxnLgrSeq": 3482748, + "index": "5398F35AA4EA52AA0AC10C3A5A067C85FF70F8131921BE8678C9673D6EC5F1F9" +} + +=== Saving Keys for Development === +Keys and issuance data saved to confidential-setup.json +``` diff --git a/_code-samples/confidential-transfers/py/issue-mpt-confidential.py b/_code-samples/confidential-transfers/py/issue-mpt-confidential.py new file mode 100644 index 0000000..0a7823d --- /dev/null +++ b/_code-samples/confidential-transfers/py/issue-mpt-confidential.py @@ -0,0 +1,333 @@ +import json +from xrpl.clients import JsonRpcClient +from xrpl.wallet import generate_faucet_wallet +from xrpl.models.transactions import ( + MPTokenIssuanceCreate, + MPTokenIssuanceCreateFlag, + MPTokenIssuanceSet, + MPTokenAuthorize, + Payment +) +from xrpl.transaction import submit_and_wait +from xrpl.models.requests import AccountObjectType, AccountObjects +from xrpl.utils.mptoken_metadata import encode_mptoken_metadata +from xrpl.core.confidential import MPTCrypto +from xrpl.core.confidential.transaction_builders import ( + prepare_confidential_merge_inbox +) +from xrpl.core.confidential.context import compute_convert_context_hash +from xrpl.models.transactions import ConfidentialMPTConvert +from xrpl.models.requests import AccountInfo + +# Connect to the network +client = JsonRpcClient("https://confidential.devnet.rippletest.net:51234") +faucet_host = "https://confidential-faucet.devnet.rippletest.net" + +# Generate accounts ------------------------------- +print("=== Generating Accounts ===") +issuer = generate_faucet_wallet(client, faucet_host=faucet_host) +issuer_second_account = generate_faucet_wallet(client, faucet_host=faucet_host) +auditor = generate_faucet_wallet(client, faucet_host=faucet_host) +print(f"Issuer: {issuer.address}") +print(f"Issuer Second Account: {issuer_second_account.address}") +print(f"Auditor: {auditor.address}") + +# Generate ElGamal keypairs ---------------------- +print("\n=== Generating ElGamal Keypairs ===") +# Initialize MPTCrypto (wrapper for the mpt-crypto C library) +crypto = MPTCrypto() + +# Generate ElGamal keypair for the issuer. +# The issuer key allows the issuer to decrypt and track all confidential balances. +issuer_private_key, issuer_public_key = crypto.generate_keypair() + +# Generate ElGamal keypair for the issuer second account. +issuer_second_account_private_key, issuer_second_account_public_key = crypto.generate_keypair() + +# Generate ElGamal keypair for the auditor. +# The auditor key enables regulatory oversight and on-chain selective disclosure. +auditor_private_key, auditor_public_key = crypto.generate_keypair() + +print(f"Issuer ElGamal Public Key: {issuer_public_key}") +print(f"Issuer Second Account ElGamal Public Key: {issuer_second_account_public_key}") +print(f"Auditor ElGamal Public Key: {auditor_public_key}") + +# Create MPT Issuance ---------------------- +print("\n=== Creating MPT Issuance... ===") +# Create an MPT issuance with the TF_MPT_CAN_PRIVACY flag enabled. +# This flag is required for confidential transfers. +mpt_create_tx = MPTokenIssuanceCreate( + account=issuer.address, + flags=MPTokenIssuanceCreateFlag.TF_MPT_CAN_PRIVACY | + MPTokenIssuanceCreateFlag.TF_MPT_CAN_TRANSFER | # Enable transfer + MPTokenIssuanceCreateFlag.TF_MPT_CAN_TRADE | # Enable trade + MPTokenIssuanceCreateFlag.TF_MPT_CAN_CLAWBACK | # Enable clawback + MPTokenIssuanceCreateFlag.TF_MPT_CAN_LOCK, # Enable lock + asset_scale=2, + maximum_amount="1000000000", + transfer_fee=0, + mptoken_metadata=encode_mptoken_metadata({ + "ticker": "CTST", + "name": "Confidential Test Token", + "desc": "A test token demonstrating confidential transfer capabilities on the XRP Ledger.", + "icon": "https://xrpl.org/assets/img/xrp-ledger-logo.svg", + "asset_class": "rwa", + "asset_subclass": "treasury", + "issuer_name": "Example Financial Corp", + "uris": [ + { + "uri": "docs.com", + "category": "docs", + "title": "Documentation" + }, + { + "uri": "examplefinancial.co/bonds", + "category": "website", + "title": "Corporate Bond Information" + } + ], + "additional_info": { + "interest_rate": "4.25%", + "interest_type": "fixed", + } + }) +) + +# Submit, sign, and wait for validation ---------------------- +print("Submitting MPTokenIssuanceCreate transaction...") +response = submit_and_wait(mpt_create_tx, client, issuer, autofill=True) + +if response.result["meta"]["TransactionResult"] != "tesSUCCESS": + result_code = response.result["meta"]["TransactionResult"] + print(f"Error: Transaction failed: {result_code}") + exit(1) + +print("MPT issuance created successfully!") +mpt_issuance_id = response.result["meta"]["mpt_issuance_id"] +print(f"MPT Issuance ID: {mpt_issuance_id}") + +# Register ElGamal keys on the ledger ---------------------- +print("\n=== Registering ElGamal Keys On-Ledger... ===") +mpt_set_tx = MPTokenIssuanceSet( + account=issuer.address, + mptoken_issuance_id=mpt_issuance_id, + issuer_elgamal_public_key=issuer_public_key, + auditor_elgamal_public_key=auditor_public_key +) + +# Submit, sign, and wait for validation ---------------------- +print("Submitting MPTokenIssuanceSet transaction...") +print(json.dumps(mpt_set_tx.to_dict(), indent=2)) +mpt_set_response = submit_and_wait(mpt_set_tx, client, issuer) + +if mpt_set_response.result["meta"]["TransactionResult"] != "tesSUCCESS": + result_code = mpt_set_response.result["meta"]["TransactionResult"] + print(f"Error: Transaction failed: {result_code}") + exit(1) + +print("ElGamal keys registered successfully!") + +# Send public tokens to issuer second account ---------------------- +print("\n=== Sending public tokens to issuer second account... ===") +# Before receiving the MPT, the second account must authorize the issuance. +authorize_tx = MPTokenAuthorize( + account=issuer_second_account.address, + mptoken_issuance_id=mpt_issuance_id +) + +# Submit, sign, and wait for validation ---------------------- +print("Submitting MPTokenAuthorize transaction...") +authorize_response = submit_and_wait( + authorize_tx, + client, + issuer_second_account, + autofill=True +) + +if authorize_response.result["meta"]["TransactionResult"] != "tesSUCCESS": + result_code = authorize_response.result["meta"]["TransactionResult"] + print(f"Error: Authorization failed: {result_code}") + exit(1) + +print("Issuer second account authorized CTST MPT successfully!") + +payment_tx = Payment( + account=issuer.address, + destination=issuer_second_account.address, + amount={ + "mpt_issuance_id": mpt_issuance_id, + "value": "1000" + } +) + +# Submit, sign, and wait for validation ---------------------- +print("\nSubmitting Payment transaction...") +payment_response = submit_and_wait(payment_tx, client, issuer, autofill=True) + +if payment_response.result["meta"]["TransactionResult"] != "tesSUCCESS": + result_code = payment_response.result["meta"]["TransactionResult"] + print(f"Error: Payment failed: {result_code}") + exit(1) + +print("Payment successful!") +print(f"Issuer Sent 1000 CTST tokens to {issuer_second_account.address}") + +# Convert public balance to confidential ---------------------- +print("\n=== Converting Public Balance to Confidential... ===") +convert_amount = 1000 + +# Compute context hash +account_info = client.request(AccountInfo(account=issuer_second_account.address)) +sequence = account_info.result["account_data"]["Sequence"] +mpt_issuance_id_bytes = bytes.fromhex(mpt_issuance_id) +context_id = compute_convert_context_hash( + issuer_second_account.classic_address, + sequence, + mpt_issuance_id_bytes, + convert_amount +) + +# Generate Schnorr proof of knowledge +print("Generating zero-knowledge proof...") +schnorr_proof = crypto.generate_pok( + issuer_second_account_private_key, + issuer_second_account_public_key, + context_id +) + +# Encrypt amount for holder (issuer second account) +print("Encrypting amount for holder...") +holder_c1, holder_c2, blinding_factor = crypto.encrypt( + issuer_second_account_public_key, + convert_amount +) + +# Encrypt amount for issuer +print("Encrypting amount for issuer...") +issuer_c1, issuer_c2, _ = crypto.encrypt( + issuer_public_key, + convert_amount, + blinding_factor +) + +# Encrypt amount for auditor +print("Encrypting amount for auditor...") +auditor_c1, auditor_c2, _ = crypto.encrypt( + auditor_public_key, + convert_amount, + blinding_factor +) + +# Manually create the ConfidentialMPTConvert transaction with auditor support. +# We can't use prepare_confidential_convert helper function because it doesn't +# support auditor encryption yet. +convert_tx = ConfidentialMPTConvert( + account=issuer_second_account.address, + mptoken_issuance_id=mpt_issuance_id, + mpt_amount=convert_amount, + holder_elgamal_public_key=issuer_second_account_public_key, + holder_encrypted_amount=holder_c1 + holder_c2, + issuer_encrypted_amount=issuer_c1 + issuer_c2, + auditor_encrypted_amount=auditor_c1 + auditor_c2, + blinding_factor=blinding_factor, + zk_proof=schnorr_proof, +) + +# Submit, sign, and wait for validation ---------------------- +print("\nSubmitting ConfidentialMPTConvert transaction...") +print(json.dumps(convert_tx.to_dict(), indent=2)) +convert_response = submit_and_wait( + convert_tx, + client, + issuer_second_account, + autofill=True +) + +if convert_response.result["meta"]["TransactionResult"] != "tesSUCCESS": + result_code = convert_response.result["meta"]["TransactionResult"] + print(f"Error: Conversion failed: {result_code}") + exit(1) + +print("Conversion successful!") +print(f"Converted {convert_amount} CTST tokens to confidential balance.") + +# Merge inbox to spending balance ---------------------- +print("\n=== Merging Inbox into Spending Balance... ===") +# After converting tokens to confidential form, they arrive in the "inbox". +# The inbox must be merged into the spending balance before the tokens can be spent. +# This is a simple transaction that requires no proofs. +merge_tx = prepare_confidential_merge_inbox( + wallet=issuer_second_account, + mpt_issuance_id=mpt_issuance_id +) + +print("Submitting ConfidentialMPTMergeInbox transaction...") +print(json.dumps(merge_tx.to_dict(), indent=2)) +merge_result = submit_and_wait(merge_tx, client, issuer_second_account) + +if merge_result.result["meta"]["TransactionResult"] != "tesSUCCESS": + result_code = merge_result.result["meta"]["TransactionResult"] + print(f"Error: Merge failed: {result_code}") + exit(1) + +print("Merge successful!") + +# Verify confidential balances ---------------------- +print("\n=== Verifying Confidential Balances ===") + +# Query the issuer second account's MPToken object for this specific issuance. +# The MPToken object stores both public (MPTAmount) and encrypted confidential balances. +# Confidential balances are stored in two fields: +# - ConfidentialBalanceSpending: encrypted balance available to spend +# - ConfidentialBalanceInbox: encrypted balance waiting to be merged +print(f"Querying issuer second account's MPToken object...") +issuer_second_objects_response = client.request( + AccountObjects( + account=issuer_second_account.address, + type=AccountObjectType.MPTOKEN + ) +) +mpt_object = None +for obj in issuer_second_objects_response.result.get("account_objects", []): + if obj.get("MPTokenIssuanceID") == mpt_issuance_id: + mpt_object = obj + break + +if not mpt_object: + print("Error: MPToken object not found") + exit(1) + +print("\nIssuer second account MPToken object:") +print(json.dumps(mpt_object, indent=2)) + +# Save keys and issuance data for use in other tutorials +# WARNING: This is for DEVELOPMENT/TESTING ONLY! +# NEVER store private keys in plain text files in production! +print("\n=== Saving Keys for Development ===") +confidential_data = { + "description": "Setup data for development and testing. Contains account seeds, ElGamal keypairs, and MPT issuance ID.", + "mpt_issuance_id": mpt_issuance_id, + "issuer": { + "address": issuer.address, + "seed": issuer.seed, + "elgamal_private_key": issuer_private_key, + "elgamal_public_key": issuer_public_key + }, + "issuer_second_account": { + "address": issuer_second_account.address, + "seed": issuer_second_account.seed, + "elgamal_private_key": issuer_second_account_private_key, + "elgamal_public_key": issuer_second_account_public_key + }, + "auditor": { + "address": auditor.address, + "seed": auditor.seed, + "elgamal_private_key": auditor_private_key, + "elgamal_public_key": auditor_public_key + } +} + +with open("confidential-setup.json", "w") as f: + json.dump(confidential_data, f, indent=2) + +print("Keys and issuance data saved to confidential-setup.json") diff --git a/_code-samples/confidential-transfers/py/requirements.txt b/_code-samples/confidential-transfers/py/requirements.txt new file mode 100644 index 0000000..9b62ab4 --- /dev/null +++ b/_code-samples/confidential-transfers/py/requirements.txt @@ -0,0 +1,2 @@ +xrpl-py[confidential] @ git+https://github.com/XRPLF/xrpl-py.git@refs/pull/919/head +# xrpl-py[confidential]==4.6.0b0 diff --git a/docs/xls-96-confidential-transfers/index.page.tsx b/docs/xls-96-confidential-transfers/index.page.tsx index dbbd101..27830e1 100644 --- a/docs/xls-96-confidential-transfers/index.page.tsx +++ b/docs/xls-96-confidential-transfers/index.page.tsx @@ -87,7 +87,6 @@ export default function Page() { Read the Docs - diff --git a/docs/xls-96-confidential-transfers/tutorials/issue-mpt-with-confidential-transfers.md b/docs/xls-96-confidential-transfers/tutorials/issue-mpt-with-confidential-transfers.md new file mode 100644 index 0000000..7fe4518 --- /dev/null +++ b/docs/xls-96-confidential-transfers/tutorials/issue-mpt-with-confidential-transfers.md @@ -0,0 +1,262 @@ +--- +seo: + description: Issue a Multi-Purpose Token (MPT) with confidential transfers enabled on the XRP Ledger. +metadata: + indexPage: true +labels: + - Multi-Purpose Token + - MPT + - Token Issuance + - Confidential Transfers +--- +# Issue an MPT with Confidential Transfers + +A [Multi-Purpose Token (MPT)](https://xrpl.org/docs/concepts/tokens/fungible-tokens/multi-purpose-tokens) with [confidential transfers](../concepts/confidential-transfers.md) enabled allows token holders to keep their balances and transaction amounts private using encryption and Zero-Knowledge Proofs (ZKPs). + +This tutorial shows you how to issue an MPT with confidential transfers enabled, register encryption keys on-ledger, and mint confidential tokens using a dual-account setup. + +## Goals + +By the end of this tutorial, you will be able to: + +- Generate ElGamal encryption keypairs for confidential transfers. +- Issue an MPT with confidential transfers enabled and register the issuer's and auditor's ElGamal public keys. +- Mint confidential tokens by converting public tokens to confidential balances with auditor encryption for regulatory compliance. + +## Prerequisites + +To complete this tutorial, you should: + +- Have a basic understanding of the XRP Ledger. +- Understand the [confidential transfers concept](../concepts/confidential-transfers.md). +- Have an XRP Ledger client library set up in your development environment. This page provides examples for the following: + - **Python** with the [xrpl-py library](https://github.com/XRPLF/xrpl-py). See [Get Started Using Python](https://xrpl.org/docs/tutorials/get-started/get-started-python) for setup steps. + +## Source Code + +You can find the complete source code for this tutorial's example in the [code samples section of this website's repository](https://github.com/XRPLF/opensource.ripple.com/tree/main/_code-samples/confidential-transfers). + +## Steps + +The example in this tutorial demonstrates how to issue a confidential corporate bond MPT for confidential token distribution. + +### 1. Install dependencies + +{% tabs %} +{% tab label="Python" %} +From the code sample folder, install dependencies using pip: + +```bash +python -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` +{% /tab %} +{% /tabs %} + +### 2. Set up client and accounts + +To get started, import the necessary libraries and instantiate a client to connect to the XRPL. This example imports: + +{% tabs %} +{% tab label="Python" %} + +- `json`: Used for loading and formatting JSON data. +- `xrpl`: Used for XRPL client connection and transaction handling. + +{% code-snippet file="/_code-samples/confidential-transfers/py/issue-mpt-confidential.py" language="py" before="# Generate accounts" /%} +{% /tab %} +{% /tabs %} + +Next, fund the necessary accounts. You'll need three accounts: + +- **Issuer**: Creates the MPT issuance and registers encryption keys. +- **Issuer Second Account**: A regular holder account controlled by the issuer that holds confidential tokens. +- **Auditor**: Provides regulatory oversight with independent decryption capability. + +{% tabs %} +{% tab label="Python" %} +{% code-snippet file="/_code-samples/confidential-transfers/py/issue-mpt-confidential.py" language="py" from="# Generate accounts" before="# Generate ElGamal" /%} +{% /tab %} +{% /tabs %} + + + +### 3. Generate ElGamal encryption keypairs + +For confidential transfers, you need to generate ElGamal keypairs for encryption. These are separate from your XRPL account keys and must be persisted securely. + +- **Issuer keypair**: Required to track the total confidential supply (mirror balance). +- **Issuer Second Account keypair**: Required for the secondary account that will hold confidential balances. +- **Auditor keypair**: Enables regulatory oversight with independent decryption capability. This is generally optional, but is included in this example for completeness. + +{% tabs %} +{% tab label="Python" %} +{% code-snippet file="/_code-samples/confidential-transfers/py/issue-mpt-confidential.py" language="py" from="# Generate ElGamal" before="# Create MPT" /%} +{% /tab %} +{% /tabs %} + +{% admonition type="danger" name="Warning" %} +**Store the private keys securely!** If you lose the ElGamal private keys, you **cannot**: + +- Decrypt confidential balances. +- Generate ZKPs for transactions. +- Spend or manage confidential tokens. + +Once registered on-ledger, ElGamal public keys are **permanently stored** and cannot be changed. +{% /admonition %} + +### 4. Create the MPT issuance + +To issue an MPT for confidential transfers, prepare an [MPTokenIssuanceCreate transaction](https://xrpl.org/docs/references/protocol/transactions/types/mptokenissuancecreate) with the **Can Privacy** flag enabled. + +{% tabs %} +{% tab label="Python" %} +{% code-snippet file="/_code-samples/confidential-transfers/py/issue-mpt-confidential.py" language="py" from="# Create MPT Issuance" before="# Submit, sign" /%} +{% /tab %} +{% /tabs %} + +Submit the transaction and retrieve the MPT Issuance ID: + +{% tabs %} +{% tab label="Python" %} +{% code-snippet file="/_code-samples/confidential-transfers/py/issue-mpt-confidential.py" language="py" from="# Submit, sign" before="# Register ElGamal keys" /%} +{% /tab %} +{% /tabs %} + +{% admonition type="warning" name="Caution" %} +Once created, the MPT's privacy setting cannot be changed. If any holder has a non-zero confidential balance, you cannot disable the **Can Privacy** flag. Only enable confidential transfers if you're committed to supporting them long-term. +{% /admonition %} + +### 5. Register ElGamal keys on-ledger + +After creating the MPT issuance, register the issuer's and auditor's ElGamal public keys on-ledger using the [MPTokenIssuanceSet transaction](https://xrpl.org/docs/references/protocol/transactions/types/mptokenissuanceset). This allows: + +- **Issuer**: To decrypt and track all confidential balances (mirror balances). +- **Auditor**: To independently decrypt and verify balances for regulatory compliance. + +{% tabs %} +{% tab label="Python" %} +{% code-snippet file="/_code-samples/confidential-transfers/py/issue-mpt-confidential.py" language="py" from="# Register ElGamal keys" before="# Send public tokens " /%} +{% /tab %} +{% /tabs %} + +### 6. Send public tokens to the second account + +To prepare for confidential minting, the issuer sends public tokens to a second account via a [Payment transaction](https://xrpl.org/docs/references/protocol/transactions/types/payment). This dual-account setup separates the issuer's operational account from the account that will hold confidential balances. + +First, the second account must authorize the MPT issuance. + +{% tabs %} +{% tab label="Python" %} +{% code-snippet file="/_code-samples/confidential-transfers/py/issue-mpt-confidential.py" language="py" from="# Send public tokens" before="payment_tx"/%} +{% /tab %} +{% /tabs %} + +Then, the issuer sends public tokens to the **second** account. + +{% tabs %} +{% tab label="Python" %} +{% code-snippet file="/_code-samples/confidential-transfers/py/issue-mpt-confidential.py" language="py" from="payment_tx" before="# Convert public" /%} +{% /tab %} +{% /tabs %} + +### 7. Convert to confidential balance + +The second account must convert its public token balance to confidential using the [ConfidentialMPTConvert transaction][]. This transaction automatically registers the account's ElGamal public key on-ledger during the conversion. + +Before creating the transaction, you need to do the following: + +1. Compute the context hash for the Zero-Knowledge Proof. +2. Generate a Schnorr Proof of knowledge for the second account's ElGamal key. +3. Encrypt the amount for the second account (holder), issuer, and auditor. + +{% tabs %} +{% tab label="Python" %} +{% code-snippet file="/_code-samples/confidential-transfers/py/issue-mpt-confidential.py" language="py" from="# Convert public balance" before="# Manually create" /%} +{% /tab %} +{% /tabs %} + +{% admonition type="info" name="Note" %} +The context hash computation requires the account's sequence number before the transaction is submitted. This is why the code fetches the sequence number using an `AccountInfo` request before computing the ZKP. The sequence number is cryptographically bound to the proof to prevent replay attacks, so it cannot be autofilled after the proof is generated. +{% /admonition %} + +Then, create and submit the transaction: +{% tabs %} +{% tab label="Python" %} +{% code-snippet file="/_code-samples/confidential-transfers/py/issue-mpt-confidential.py" language="py" from="# Manually create" before="# Merge inbox to spending balance" /%} +{% /tab %} +{% /tabs %} + +{% admonition type="warning" name="Caution" %} +When an auditor key is registered on an MPT issuance, all confidential transactions must include the **auditor encrypted amount**. This ensures the auditor can independently decrypt and verify all confidential balances. Transactions that omit this field will fail with a `tecNO_PERMISSION` error. +{% /admonition %} + +### 8. Merge inbox into spending balance + +After conversion, tokens arrive in the holder's **inbox** balance. They must be merged into the spending balance via the [ConfidentialMPTMergeInbox transaction][] before they can be used (for example, sent to another account). This is a simple transaction that does not require proofs. + +{% tabs %} +{% tab label="Python" %} +The Python SDK has a `prepare_confidential_merge_inbox()` helper function to prepare the transaction. + +{% code-snippet file="/_code-samples/confidential-transfers/py/issue-mpt-confidential.py" language="py" from="# Merge inbox" before="# Verify confidential" /%} +{% /tab %} +{% /tabs %} + +### 9. Verify confidential balances + +Query the holder's `MPToken` object to verify the confidential balances. + +{% tabs %} +{% tab label="Python" %} +{% code-snippet file="/_code-samples/confidential-transfers/py/issue-mpt-confidential.py" language="py" from="# Verify confidential" before="# Save keys and issuance data for use in other tutorials" /%} +{% /tab %} +{% /tabs %} + +The `MPToken` object contains: + +- **HolderElGamalPublicKey**: The holder's registered ElGamal public key. +- **ConfidentialBalanceSpending**: Encrypted balance available to spend. +- **ConfidentialBalanceInbox**: Encrypted balance waiting to be merged. +- **IssuerEncryptedBalance**: Encrypted mirror balance. +- **AuditorEncryptedBalance**: Encrypted balance for regulatory oversight. + +Only the second account (holder), issuer, and auditor can decrypt their respective encrypted balances using their private keys. + +### 10. Save keys for development + +The code saves the ElGamal keys and MPT issuance ID to a `confidential-setup.json` file. This allows you to reuse them in other tutorials (such as sending confidential payments or auditing transactions). + +{% admonition type="danger" name="Warning" %} +**This is for DEVELOPMENT and TESTING purposes ONLY!** + +Never store private keys in plain text files in production environments. In production, use secure key management solutions such as Hardware Security Modules (HSMs), cloud key management services, or encrypted key stores with proper access controls. +{% /admonition %} + +{% tabs %} +{% tab label="Python" %} +{% code-snippet file="/_code-samples/confidential-transfers/py/issue-mpt-confidential.py" language="py" from="# Save keys and issuance data for use in other tutorials" /%} +{% /tab %} +{% /tabs %} + +## See Also + +- **Concepts**: + - [Confidential Transfers](../concepts/confidential-transfers.md) + - [Multi-Purpose Tokens (MPT)](https://xrpl.org/docs/concepts/tokens/fungible-tokens/multi-purpose-tokens) +- **Tutorials**: + - [Send Confidential Payments](send-confidential-payments.md) + - [Audit Confidential Balances](audit-confidential-balances.md) + - [Claw Back Confidential Balances](claw-back-confidential-balances.md) +- **References**: + - [MPTokenIssuance entry](../references/updated-ledger-entries.md#mptokenissuance) + - [MPToken entry](../references/updated-ledger-entries.md#mptoken) + - [MPTokenIssuanceCreate transaction](../references/transactions/updated-transactions.md#mptokenissuancecreate) + - [MPTokenIssuanceSet transaction](../references/transactions/updated-transactions.md#mptokenissuanceset) + - [ConfidentialMPTConvert transaction](../references/transactions/confidentialmptconvert.md) + - [ConfidentialMPTMergeInbox transaction](../references/transactions/confidentialmptmergeinbox.md) + +{% raw-partial file="/docs/_snippets/common-links.md" /%} diff --git a/sidebars.yaml b/sidebars.yaml index 9999658..a598a89 100644 --- a/sidebars.yaml +++ b/sidebars.yaml @@ -42,3 +42,7 @@ - page: docs/xls-96-confidential-transfers/references/transactions/confidentialmptsend.md - page: docs/xls-96-confidential-transfers/references/transactions/updated-transactions.md - page: docs/xls-96-confidential-transfers/references/updated-ledger-entries.md + - group: Tutorials + expanded: false + items: + - page: docs/xls-96-confidential-transfers/tutorials/issue-mpt-with-confidential-transfers.md