mirror of
https://github.com/XRPLF/rippled.git
synced 2026-03-02 02:32:30 +00:00
Compare commits
74 Commits
copilot/re
...
ripple/con
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3aa84194ac | ||
|
|
6b56fc2644 | ||
|
|
c2f8b91397 | ||
|
|
1ea9312946 | ||
|
|
6ad60d7141 | ||
|
|
94e911ed69 | ||
|
|
b2c434dd73 | ||
|
|
b6d1a8d62b | ||
|
|
9d0c854139 | ||
|
|
3a6ca681ff | ||
|
|
a216824c15 | ||
|
|
90cf86a920 | ||
|
|
e69d3c9bd7 | ||
|
|
fd390a4f1c | ||
|
|
3941283438 | ||
|
|
86af28d91d | ||
|
|
41f7102fb8 | ||
|
|
66ed0fa452 | ||
|
|
cad8fb328a | ||
|
|
31346425f0 | ||
|
|
40bfaa25d2 | ||
|
|
c4916f1251 | ||
|
|
fc8b7898c5 | ||
|
|
446f9fbe6d | ||
|
|
1297385b7e | ||
|
|
114adc0c57 | ||
|
|
1d349c32c5 | ||
|
|
a5f20c129d | ||
|
|
75d143a2a0 | ||
|
|
e3da98e310 | ||
|
|
ec6d7cb91d | ||
|
|
fa055c2bd5 | ||
|
|
6c38086f17 | ||
|
|
3e9dc276ed | ||
|
|
abf7a62b1f | ||
|
|
bd3a6e1631 | ||
|
|
7c0bd419a4 | ||
|
|
d3126959e7 | ||
|
|
67e8e89e0f | ||
|
|
4e4326a174 | ||
|
|
5397bd6d6e | ||
|
|
6dece25cc3 | ||
|
|
d9da8733be | ||
|
|
f6f51451e7 | ||
|
|
3c8ec2eb7e | ||
|
|
c754aa3bca | ||
|
|
b94c95b3e9 | ||
|
|
8365148b5c | ||
|
|
c03866bf0f | ||
|
|
389afc5f06 | ||
|
|
7b04eaae81 | ||
|
|
1343019509 | ||
|
|
cd75e630a2 | ||
|
|
ec57fbdc5f | ||
|
|
4fe67f5715 | ||
|
|
44d885e39b | ||
|
|
3af758145c | ||
|
|
f3d4d4341b | ||
|
|
ddb518ad09 | ||
|
|
3899e3f36c | ||
|
|
e4a8ba51f9 | ||
|
|
35e4fad557 | ||
|
|
8e9cb3c1da | ||
|
|
18d92058e3 | ||
|
|
f24d584f29 | ||
|
|
da3fbcd25b | ||
|
|
daa1303b5a | ||
|
|
a636fe5871 | ||
|
|
bbc3071fd1 | ||
|
|
8fdc639206 | ||
|
|
5a89641d98 | ||
|
|
beefa248a6 | ||
|
|
e919a25ecb | ||
|
|
c3fdbc0430 |
@@ -98,6 +98,7 @@ find_package(ed25519 REQUIRED)
|
||||
find_package(gRPC REQUIRED)
|
||||
find_package(LibArchive REQUIRED)
|
||||
find_package(lz4 REQUIRED)
|
||||
find_package(mpt-crypto REQUIRED)
|
||||
find_package(nudb REQUIRED)
|
||||
find_package(OpenSSL REQUIRED)
|
||||
find_package(secp256k1 REQUIRED)
|
||||
@@ -109,6 +110,7 @@ target_link_libraries(
|
||||
xrpl_libs
|
||||
INTERFACE ed25519::ed25519
|
||||
lz4::lz4
|
||||
mpt-crypto::mpt-crypto
|
||||
OpenSSL::Crypto
|
||||
OpenSSL::SSL
|
||||
secp256k1::secp256k1
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
"sqlite3/3.49.1#8631739a4c9b93bd3d6b753bac548a63%1765850149.926",
|
||||
"soci/4.0.3#a9f8d773cd33e356b5879a4b0564f287%1765850149.46",
|
||||
"snappy/1.1.10#968fef506ff261592ec30c574d4a7809%1765850147.878",
|
||||
"secp256k1/0.7.1#3a61e95e220062ef32c48d019e9c81f7%1770306721.686",
|
||||
"secp256k1/0.7.1#481881709eb0bdd0185a12b912bbe8ad%1770910500.329",
|
||||
"rocksdb/10.5.1#4a197eca381a3e5ae8adf8cffa5aacd0%1765850186.86",
|
||||
"re2/20230301#ca3b241baec15bd31ea9187150e0b333%1765850148.103",
|
||||
"protobuf/6.32.1#f481fd276fc23a33b85a3ed1e898b693%1765850161.038",
|
||||
"openssl/3.5.5#05a4ac5b7323f7a329b2db1391d9941f%1769599205.414",
|
||||
"openssl/3.5.5#05a4ac5b7323f7a329b2db1391d9941f%1770229825.601",
|
||||
"nudb/2.0.9#0432758a24204da08fee953ec9ea03cb%1769436073.32",
|
||||
"mpt-crypto/0.1.0-rc2#575de3d495f539e3e5eba957b324d260%1771955268.105",
|
||||
"lz4/1.10.0#59fc63cac7f10fbe8e05c7e62c2f3504%1765850143.914",
|
||||
"libiconv/1.17#1e65319e945f2d31941a9d28cc13c058%1765842973.492",
|
||||
"libbacktrace/cci.20210118#a7691bfccd8caaf66309df196790a5a1%1765842973.03",
|
||||
@@ -31,7 +32,7 @@
|
||||
"strawberryperl/5.32.1.1#707032463aa0620fa17ec0d887f5fe41%1765850165.196",
|
||||
"protobuf/6.32.1#f481fd276fc23a33b85a3ed1e898b693%1765850161.038",
|
||||
"nasm/2.16.01#31e26f2ee3c4346ecd347911bd126904%1765850144.707",
|
||||
"msys2/cci.latest#eea83308ad7e9023f7318c60d5a9e6cb%1770199879.083",
|
||||
"msys2/cci.latest#d22fe7b2808f5fd34d0a7923ace9c54f%1770657326.649",
|
||||
"m4/1.4.19#70dc8bbb33e981d119d2acc0175cf381%1763158052.846",
|
||||
"cmake/4.2.0#ae0a44f44a1ef9ab68fd4b3e9a1f8671%1765850153.937",
|
||||
"cmake/3.31.10#313d16a1aa16bbdb2ca0792467214b76%1765850153.479",
|
||||
|
||||
@@ -31,6 +31,7 @@ class Xrpl(ConanFile):
|
||||
"ed25519/2015.03",
|
||||
"grpc/1.72.0",
|
||||
"libarchive/3.8.1",
|
||||
"mpt-crypto/0.1.0-rc2",
|
||||
"nudb/2.0.9",
|
||||
"openssl/3.5.5",
|
||||
"secp256k1/0.7.1",
|
||||
@@ -209,6 +210,7 @@ class Xrpl(ConanFile):
|
||||
"grpc::grpc++",
|
||||
"libarchive::libarchive",
|
||||
"lz4::lz4",
|
||||
"mpt-crypto::mpt-crypto",
|
||||
"nudb::nudb",
|
||||
"openssl::crypto",
|
||||
"protobuf::libprotobuf",
|
||||
|
||||
@@ -54,6 +54,7 @@ words:
|
||||
- autobridging
|
||||
- bimap
|
||||
- bindir
|
||||
- blindings
|
||||
- bookdir
|
||||
- Bougalis
|
||||
- Britto
|
||||
@@ -86,6 +87,7 @@ words:
|
||||
- daria
|
||||
- dcmake
|
||||
- dearmor
|
||||
- decryptor
|
||||
- deleteme
|
||||
- demultiplexer
|
||||
- deserializaton
|
||||
@@ -95,6 +97,7 @@ words:
|
||||
- distro
|
||||
- doxyfile
|
||||
- dxrpl
|
||||
- elgamal
|
||||
- endmacro
|
||||
- exceptioned
|
||||
- Falco
|
||||
@@ -103,6 +106,7 @@ words:
|
||||
- fmtdur
|
||||
- fsanitize
|
||||
- funclets
|
||||
- Gamal
|
||||
- gcov
|
||||
- gcovr
|
||||
- ghead
|
||||
@@ -183,6 +187,7 @@ words:
|
||||
- partitioner
|
||||
- paychan
|
||||
- paychans
|
||||
- Pedersen
|
||||
- permdex
|
||||
- perminute
|
||||
- permissioned
|
||||
@@ -218,6 +223,7 @@ words:
|
||||
- sahyadri
|
||||
- Satoshi
|
||||
- scons
|
||||
- Schnorr
|
||||
- secp
|
||||
- sendq
|
||||
- seqit
|
||||
@@ -244,6 +250,7 @@ words:
|
||||
- stvar
|
||||
- stvector
|
||||
- stxchainattestations
|
||||
- summands
|
||||
- superpeer
|
||||
- superpeers
|
||||
- takergets
|
||||
|
||||
503
include/xrpl/protocol/ConfidentialTransfer.h
Normal file
503
include/xrpl/protocol/ConfidentialTransfer.h
Normal file
@@ -0,0 +1,503 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/MPTIssue.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/Rate.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/detail/secp256k1.h>
|
||||
|
||||
#include <secp256k1_mpt.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* @brief Bundles an ElGamal public key with its associated encrypted amount.
|
||||
*
|
||||
* Used to represent a recipient in confidential transfers, containing both
|
||||
* the recipient's ElGamal public key and the ciphertext encrypting the
|
||||
* transfer amount under that key.
|
||||
*/
|
||||
struct ConfidentialRecipient
|
||||
{
|
||||
Slice const publicKey; ///< The recipient's ElGamal public key (64 bytes).
|
||||
Slice const encryptedAmount; ///< The encrypted amount ciphertext (128 bytes).
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Increments the confidential balance version counter on an MPToken.
|
||||
*
|
||||
* The version counter is used to prevent replay attacks by binding proofs
|
||||
* to a specific state of the account's confidential balance. Wraps to 0
|
||||
* on overflow (defined behavior for unsigned integers).
|
||||
*
|
||||
* @param mptoken The MPToken ledger entry to update.
|
||||
*/
|
||||
inline void
|
||||
incrementConfidentialVersion(STObject& mptoken)
|
||||
{
|
||||
// Retrieve current version and increment.
|
||||
// Unsigned integer overflow is defined behavior in C++ (wraps to 0),
|
||||
// which is acceptable here.
|
||||
mptoken[sfConfidentialBalanceVersion] = mptoken[~sfConfidentialBalanceVersion].value_or(0u) + 1u;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Adds common fields to a serializer for ZKP context hash generation.
|
||||
*
|
||||
* Serializes the transaction type, account, sequence number, and issuance ID
|
||||
* into the provided serializer. These fields form the base of all context
|
||||
* hashes used in zero-knowledge proofs.
|
||||
*
|
||||
* @param s The serializer to append fields to.
|
||||
* @param txType The transaction type identifier.
|
||||
* @param account The account ID of the transaction sender.
|
||||
* @param sequence The transaction sequence number.
|
||||
* @param issuanceID The MPToken Issuance ID.
|
||||
*/
|
||||
void
|
||||
addCommonZKPFields(
|
||||
Serializer& s,
|
||||
std::uint16_t txType,
|
||||
AccountID const& account,
|
||||
std::uint32_t sequence,
|
||||
uint192 const& issuanceID);
|
||||
|
||||
/**
|
||||
* @brief Generates the context hash for ConfidentialMPTSend transactions.
|
||||
*
|
||||
* Creates a unique 256-bit hash that binds the zero-knowledge proofs to
|
||||
* this specific send transaction, preventing proof reuse across transactions.
|
||||
*
|
||||
* @param account The sender's account ID.
|
||||
* @param sequence The transaction sequence number.
|
||||
* @param issuanceID The MPToken Issuance ID.
|
||||
* @param destination The destination account ID.
|
||||
* @param version The sender's confidential balance version.
|
||||
* @return A 256-bit context hash unique to this transaction.
|
||||
*/
|
||||
uint256
|
||||
getSendContextHash(
|
||||
AccountID const& account,
|
||||
std::uint32_t sequence,
|
||||
uint192 const& issuanceID,
|
||||
AccountID const& destination,
|
||||
std::uint32_t version);
|
||||
|
||||
/**
|
||||
* @brief Generates the context hash for ConfidentialMPTClawback transactions.
|
||||
*
|
||||
* Creates a unique 256-bit hash that binds the equality proof to this
|
||||
* specific clawback transaction.
|
||||
*
|
||||
* @param account The issuer's account ID.
|
||||
* @param sequence The transaction sequence number.
|
||||
* @param issuanceID The MPToken Issuance ID.
|
||||
* @param amount The amount being clawed back.
|
||||
* @param holder The holder's account ID being clawed back from.
|
||||
* @return A 256-bit context hash unique to this transaction.
|
||||
*/
|
||||
uint256
|
||||
getClawbackContextHash(
|
||||
AccountID const& account,
|
||||
std::uint32_t sequence,
|
||||
uint192 const& issuanceID,
|
||||
std::uint64_t amount,
|
||||
AccountID const& holder);
|
||||
|
||||
/**
|
||||
* @brief Generates the context hash for ConfidentialMPTConvert transactions.
|
||||
*
|
||||
* Creates a unique 256-bit hash that binds the Schnorr proof (for key
|
||||
* registration) to this specific convert transaction.
|
||||
*
|
||||
* @param account The holder's account ID.
|
||||
* @param sequence The transaction sequence number.
|
||||
* @param issuanceID The MPToken Issuance ID.
|
||||
* @param amount The amount being converted to confidential.
|
||||
* @return A 256-bit context hash unique to this transaction.
|
||||
*/
|
||||
uint256
|
||||
getConvertContextHash(
|
||||
AccountID const& account,
|
||||
std::uint32_t sequence,
|
||||
uint192 const& issuanceID,
|
||||
std::uint64_t amount);
|
||||
|
||||
/**
|
||||
* @brief Generates the context hash for ConfidentialMPTConvertBack transactions.
|
||||
*
|
||||
* Creates a unique 256-bit hash that binds the zero-knowledge proofs to
|
||||
* this specific convert-back transaction.
|
||||
*
|
||||
* @param account The holder's account ID.
|
||||
* @param sequence The transaction sequence number.
|
||||
* @param issuanceID The MPToken Issuance ID.
|
||||
* @param amount The amount being converted back to public.
|
||||
* @param version The holder's confidential balance version.
|
||||
* @return A 256-bit context hash unique to this transaction.
|
||||
*/
|
||||
uint256
|
||||
getConvertBackContextHash(
|
||||
AccountID const& account,
|
||||
std::uint32_t sequence,
|
||||
uint192 const& issuanceID,
|
||||
std::uint64_t amount,
|
||||
std::uint32_t version);
|
||||
|
||||
/**
|
||||
* @brief Parses an ElGamal ciphertext into two secp256k1 public key components.
|
||||
*
|
||||
* Breaks a 66-byte encrypted amount (two 33-byte compressed EC points) into
|
||||
* two secp256k1_pubkey structures (C1, C2) for use in cryptographic operations.
|
||||
*
|
||||
* @param buffer The 66-byte buffer containing the compressed ciphertext.
|
||||
* @param out1 Output: The C1 component of the ElGamal ciphertext.
|
||||
* @param out2 Output: The C2 component of the ElGamal ciphertext.
|
||||
* @return true if parsing succeeds, false if the buffer is invalid.
|
||||
*/
|
||||
bool
|
||||
makeEcPair(Slice const& buffer, secp256k1_pubkey& out1, secp256k1_pubkey& out2);
|
||||
|
||||
/**
|
||||
* @brief Serializes two secp256k1 public key components into compressed form.
|
||||
*
|
||||
* Converts two secp256k1_pubkey structures (C1, C2) back into a 66-byte
|
||||
* buffer containing two 33-byte compressed EC points.
|
||||
*
|
||||
* @param in1 The C1 component to serialize.
|
||||
* @param in2 The C2 component to serialize.
|
||||
* @param buffer Output: The 66-byte buffer to write the compressed ciphertext.
|
||||
* @return true if serialization succeeds, false otherwise.
|
||||
*/
|
||||
bool
|
||||
serializeEcPair(secp256k1_pubkey const& in1, secp256k1_pubkey const& in2, Buffer& buffer);
|
||||
|
||||
/**
|
||||
* @brief Verifies that a buffer contains two valid, parsable EC public keys.
|
||||
*
|
||||
* @param buffer The input buffer containing two concatenated components.
|
||||
* @return true if both components can be parsed successfully, false otherwise.
|
||||
*/
|
||||
bool
|
||||
isValidCiphertext(Slice const& buffer);
|
||||
|
||||
/**
|
||||
* @brief Verifies that a buffer contains a valid, parsable compressed EC point.
|
||||
*
|
||||
* Can be used to validate both compressed public keys and Pedersen commitments.
|
||||
* Fails early if the prefix byte is not 0x02 or 0x03.
|
||||
*
|
||||
* @param buffer The input buffer containing a compressed EC point (33 bytes).
|
||||
* @return true if the point can be parsed successfully, false otherwise.
|
||||
*/
|
||||
bool
|
||||
isValidCompressedECPoint(Slice const& buffer);
|
||||
|
||||
/**
|
||||
* @brief Homomorphically adds two ElGamal ciphertexts.
|
||||
*
|
||||
* Uses the additive homomorphic property of ElGamal encryption to compute
|
||||
* Enc(a + b) from Enc(a) and Enc(b) without decryption.
|
||||
*
|
||||
* @param a The first ciphertext (66 bytes).
|
||||
* @param b The second ciphertext (66 bytes).
|
||||
* @param out Output: The resulting ciphertext Enc(a + b).
|
||||
* @return tesSUCCESS on success, or an error code if parsing fails.
|
||||
*/
|
||||
TER
|
||||
homomorphicAdd(Slice const& a, Slice const& b, Buffer& out);
|
||||
|
||||
/**
|
||||
* @brief Homomorphically subtracts two ElGamal ciphertexts.
|
||||
*
|
||||
* Uses the additive homomorphic property of ElGamal encryption to compute
|
||||
* Enc(a - b) from Enc(a) and Enc(b) without decryption.
|
||||
*
|
||||
* @param a The minuend ciphertext (66 bytes).
|
||||
* @param b The subtrahend ciphertext (66 bytes).
|
||||
* @param out Output: The resulting ciphertext Enc(a - b).
|
||||
* @return tesSUCCESS on success, or an error code if parsing fails.
|
||||
*/
|
||||
TER
|
||||
homomorphicSubtract(Slice const& a, Slice const& b, Buffer& out);
|
||||
|
||||
/**
|
||||
* @brief Encrypts an amount using ElGamal encryption.
|
||||
*
|
||||
* Produces a ciphertext C = (C1, C2) where C1 = r*G and C2 = m*G + r*Pk,
|
||||
* using the provided blinding factor r.
|
||||
*
|
||||
* @param amt The plaintext amount to encrypt.
|
||||
* @param pubKeySlice The recipient's ElGamal public key (64 bytes).
|
||||
* @param blindingFactor The 32-byte randomness used as blinding factor r.
|
||||
* @return The 66-byte ciphertext, or std::nullopt on failure.
|
||||
*/
|
||||
std::optional<Buffer>
|
||||
encryptAmount(uint64_t const amt, Slice const& pubKeySlice, Slice const& blindingFactor);
|
||||
|
||||
/**
|
||||
* @brief Generates the canonical zero encryption for a specific MPToken.
|
||||
*
|
||||
* Creates a deterministic encryption of zero that is unique to the account
|
||||
* and MPT issuance. Used to initialize confidential balance fields.
|
||||
*
|
||||
* @param pubKeySlice The holder's ElGamal public key (64 bytes).
|
||||
* @param account The account ID of the token holder.
|
||||
* @param mptId The MPToken Issuance ID.
|
||||
* @return The 66-byte canonical zero ciphertext, or std::nullopt on failure.
|
||||
*/
|
||||
std::optional<Buffer>
|
||||
encryptCanonicalZeroAmount(Slice const& pubKeySlice, AccountID const& account, MPTID const& mptId);
|
||||
|
||||
/**
|
||||
* @brief Verifies a Schnorr proof of knowledge of an ElGamal private key.
|
||||
*
|
||||
* Proves that the submitter knows the secret key corresponding to the
|
||||
* provided public key, without revealing the secret key itself.
|
||||
*
|
||||
* @param pubKeySlice The ElGamal public key (64 bytes).
|
||||
* @param proofSlice The Schnorr proof (65 bytes).
|
||||
* @param contextHash The 256-bit context hash binding the proof.
|
||||
* @return tesSUCCESS if valid, or an error code otherwise.
|
||||
*/
|
||||
TER
|
||||
verifySchnorrProof(Slice const& pubKeySlice, Slice const& proofSlice, uint256 const& contextHash);
|
||||
|
||||
/**
|
||||
* @brief Verifies that a ciphertext correctly encrypts a revealed amount.
|
||||
*
|
||||
* Given the plaintext amount and blinding factor, verifies that the
|
||||
* ciphertext was correctly constructed using ElGamal encryption.
|
||||
*
|
||||
* @param amount The revealed plaintext amount.
|
||||
* @param blindingFactor The 32-byte blinding factor used in encryption.
|
||||
* @param pubKeySlice The recipient's ElGamal public key (64 bytes).
|
||||
* @param ciphertext The ciphertext to verify (66 bytes).
|
||||
* @return tesSUCCESS if the encryption is valid, or an error code otherwise.
|
||||
*/
|
||||
TER
|
||||
verifyElGamalEncryption(
|
||||
std::uint64_t const amount,
|
||||
Slice const& blindingFactor,
|
||||
Slice const& pubKeySlice,
|
||||
Slice const& ciphertext);
|
||||
|
||||
/**
|
||||
* @brief Validates the format of encrypted amount fields in a transaction.
|
||||
*
|
||||
* Checks that all ciphertext fields in the transaction object have the
|
||||
* correct length and contain valid EC points. This function is only used
|
||||
* by ConfidentialMPTConvert and ConfidentialMPTConvertBack transactions.
|
||||
*
|
||||
* @param object The transaction object containing encrypted amount fields.
|
||||
* @return tesSUCCESS if all formats are valid, temMALFORMED if required fields
|
||||
* are missing, or temBAD_CIPHERTEXT if format validation fails.
|
||||
*/
|
||||
NotTEC
|
||||
checkEncryptedAmountFormat(STObject const& object);
|
||||
|
||||
/**
|
||||
* @brief Verifies revealed amount encryptions for all recipients.
|
||||
*
|
||||
* Validates that the same amount was correctly encrypted for the holder,
|
||||
* issuer, and optionally the auditor using their respective public keys.
|
||||
*
|
||||
* @param amount The revealed plaintext amount.
|
||||
* @param blindingFactor The 32-byte blinding factor used in all encryptions.
|
||||
* @param holder The holder's public key and encrypted amount.
|
||||
* @param issuer The issuer's public key and encrypted amount.
|
||||
* @param auditor Optional auditor's public key and encrypted amount.
|
||||
* @return tesSUCCESS if all encryptions are valid, or an error code otherwise.
|
||||
*/
|
||||
TER
|
||||
verifyRevealedAmount(
|
||||
std::uint64_t const amount,
|
||||
Slice const& blindingFactor,
|
||||
ConfidentialRecipient const& holder,
|
||||
ConfidentialRecipient const& issuer,
|
||||
std::optional<ConfidentialRecipient> const& auditor);
|
||||
|
||||
/**
|
||||
* @brief Returns the number of recipients in a confidential transfer.
|
||||
*
|
||||
* Returns 4 if an auditor is present (sender, destination, issuer, auditor),
|
||||
* or 3 if no auditor (sender, destination, issuer).
|
||||
*
|
||||
* @param hasAuditor Whether the issuance has an auditor configured.
|
||||
* @return The number of recipients (3 or 4).
|
||||
*/
|
||||
constexpr std::size_t
|
||||
getConfidentialRecipientCount(bool hasAuditor)
|
||||
{
|
||||
return hasAuditor ? 4 : 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the size of a multi-ciphertext equality proof.
|
||||
*
|
||||
* The proof size varies based on the number of recipients because each
|
||||
* additional recipient requires additional proof components.
|
||||
*
|
||||
* @param nRecipients The number of recipients in the transfer.
|
||||
* @return The size in bytes of the equality proof.
|
||||
*/
|
||||
std::size_t
|
||||
getMultiCiphertextEqualityProofSize(std::size_t nRecipients);
|
||||
|
||||
/**
|
||||
* @brief Verifies a multi-ciphertext equality proof.
|
||||
*
|
||||
* Proves that all ciphertexts in the recipients vector encrypt the same
|
||||
* plaintext amount, without revealing the amount itself.
|
||||
*
|
||||
* @param proof The zero-knowledge proof bytes.
|
||||
* @param recipients Vector of recipients with their public keys and ciphertexts.
|
||||
* @param nRecipients The number of recipients (must match recipients.size()).
|
||||
* @param contextHash The 256-bit context hash binding the proof.
|
||||
* @return tesSUCCESS if the proof is valid, or an error code otherwise.
|
||||
*/
|
||||
TER
|
||||
verifyMultiCiphertextEqualityProof(
|
||||
Slice const& proof,
|
||||
std::vector<ConfidentialRecipient> const& recipients,
|
||||
std::size_t const nRecipients,
|
||||
uint256 const& contextHash);
|
||||
|
||||
/**
|
||||
* @brief Verifies a clawback equality proof.
|
||||
*
|
||||
* Proves that the issuer knows the exact amount encrypted in the holder's
|
||||
* balance ciphertext. Used in ConfidentialMPTClawback to verify the issuer
|
||||
* can decrypt the balance using their private key.
|
||||
*
|
||||
* @param amount The revealed plaintext amount.
|
||||
* @param proof The zero-knowledge proof bytes.
|
||||
* @param pubKeySlice The issuer's ElGamal public key (64 bytes).
|
||||
* @param ciphertext The issuer's encrypted balance on the holder's account (66 bytes).
|
||||
* @param contextHash The 256-bit context hash binding the proof.
|
||||
* @return tesSUCCESS if the proof is valid, or an error code otherwise.
|
||||
*/
|
||||
TER
|
||||
verifyClawbackEqualityProof(
|
||||
uint64_t const amount,
|
||||
Slice const& proof,
|
||||
Slice const& pubKeySlice,
|
||||
Slice const& ciphertext,
|
||||
uint256 const& contextHash);
|
||||
|
||||
/**
|
||||
* @brief Generates a cryptographically secure 32-byte blinding factor.
|
||||
*
|
||||
* Produces random bytes suitable for use as an ElGamal blinding factor
|
||||
* or Pedersen commitment randomness.
|
||||
*
|
||||
* @return A 32-byte buffer containing the random blinding factor.
|
||||
*/
|
||||
Buffer
|
||||
generateBlindingFactor();
|
||||
|
||||
/**
|
||||
* @brief Verifies the cryptographic link between an ElGamal Ciphertext and a
|
||||
* Pedersen Commitment for a transaction Amount.
|
||||
*
|
||||
* It proves that the ElGamal ciphertext `encAmt` encrypts the same value `m`
|
||||
* as the Pedersen Commitment `pcmSlice`, using the randomness `r`.
|
||||
* Proves Enc(m) <-> Pcm(m)
|
||||
*
|
||||
* @param proof The Zero Knowledge Proof bytes.
|
||||
* @param encAmt The ElGamal ciphertext of the amount (C1, C2).
|
||||
* @param pubKeySlice The sender's public key.
|
||||
* @param pcmSlice The Pedersen Commitment to the amount.
|
||||
* @param contextHash The unique context hash for this transaction.
|
||||
* @return tesSUCCESS if the proof is valid, or an error code otherwise.
|
||||
*/
|
||||
TER
|
||||
verifyAmountPcmLinkage(
|
||||
Slice const& proof,
|
||||
Slice const& encAmt,
|
||||
Slice const& pubKeySlice,
|
||||
Slice const& pcmSlice,
|
||||
uint256 const& contextHash);
|
||||
|
||||
/**
|
||||
* @brief Verifies the cryptographic link between an ElGamal Ciphertext and a
|
||||
* Pedersen Commitment for an account Balance.
|
||||
*
|
||||
* It proves that the ElGamal ciphertext `encAmt` encrypts the same value `b`
|
||||
* as the Pedersen Commitment `pcmSlice`, using the secret key `s`.
|
||||
* Proves Enc(b) <-> Pcm(b)
|
||||
*
|
||||
* Note: Swaps arguments (Pk <-> C1) to accommodate the different algebraic
|
||||
* structure.
|
||||
*
|
||||
* @param proof The Zero Knowledge Proof bytes.
|
||||
* @param encAmt The ElGamal ciphertext of the balance (C1, C2).
|
||||
* @param pubKeySlice The sender's public key.
|
||||
* @param pcmSlice The Pedersen Commitment to the balance.
|
||||
* @param contextHash The unique context hash for this transaction.
|
||||
* @return tesSUCCESS if the proof is valid, or an error code otherwise.
|
||||
*/
|
||||
TER
|
||||
verifyBalancePcmLinkage(
|
||||
Slice const& proof,
|
||||
Slice const& encAmt,
|
||||
Slice const& pubKeySlice,
|
||||
Slice const& pcmSlice,
|
||||
uint256 const& contextHash);
|
||||
|
||||
/**
|
||||
* @brief Verifies an aggregated Bulletproof range proof.
|
||||
*
|
||||
* This function verifies that all commitments in commitment_C_vec commit
|
||||
* to values within the valid 64-bit range [0, 2^64 - 1].
|
||||
*
|
||||
* @param proof The serialized Bulletproof proof.
|
||||
* @param compressedCommitments Vector of compressed Pedersen commitments (each 33 bytes).
|
||||
* @param contextHash The unique context hash for this transaction.
|
||||
* @return tesSUCCESS if the proof is valid, tecBAD_PROOF if verification
|
||||
* fails, or tecINTERNAL for internal errors.
|
||||
*/
|
||||
TER
|
||||
verifyAggregatedBulletproof(
|
||||
Slice const& proof,
|
||||
std::vector<Slice> const& compressedCommitments,
|
||||
uint256 const& contextHash);
|
||||
|
||||
/**
|
||||
* @brief Computes the remainder commitment for ConfidentialMPTSend.
|
||||
*
|
||||
* Given a balance commitment PC_bal = m_bal*G + rho_bal*H and an amount
|
||||
* commitment PC_amt = m_amt*G + rho_amt*H, this function computes:
|
||||
* PC_rem = PC_bal - PC_amt = (m_bal - m_amt)*G + (rho_bal - rho_amt)*H
|
||||
*
|
||||
* This derived commitment is used in an aggregated range proof to ensure
|
||||
* the sender maintains a non-negative balance (m_bal - m_amt >= 0).
|
||||
*
|
||||
* @param balanceCommitment The compressed Pedersen commitment to the balance (33 bytes).
|
||||
* @param amountCommitment The compressed Pedersen commitment to the amount (33 bytes).
|
||||
* @param out Output buffer for the resulting remainder commitment (33 bytes).
|
||||
* @return tesSUCCESS on success, tecINTERNAL on failure.
|
||||
*/
|
||||
TER
|
||||
computeSendRemainder(Slice const& balanceCommitment, Slice const& amountCommitment, Buffer& out);
|
||||
|
||||
/**
|
||||
* @brief Computes the remainder commitment for ConvertBack.
|
||||
*
|
||||
* Given a Pedersen commitment PC = m*G + rho*H, this function computes
|
||||
* PC_rem = PC - amount*G = (m - amount)*G + rho*H
|
||||
*
|
||||
* @param commitment The compressed Pedersen commitment (33 bytes).
|
||||
* @param amount The amount to subtract (must be non-zero).
|
||||
* @param out Output buffer for the resulting commitment (33 bytes).
|
||||
* @return tesSUCCESS on success, tecINTERNAL on failure or if amount is 0.
|
||||
*/
|
||||
TER
|
||||
computeConvertBackRemainder(Slice const& commitment, std::uint64_t amount, Buffer& out);
|
||||
} // namespace xrpl
|
||||
@@ -167,7 +167,10 @@ enum LedgerSpecificFlags {
|
||||
lsfMPTCanTrade = 0x00000010,
|
||||
lsfMPTCanTransfer = 0x00000020,
|
||||
lsfMPTCanClawback = 0x00000040,
|
||||
lsfMPTCanPrivacy = 0x00000080,
|
||||
|
||||
// Mutable flags (lsmf prefix) control whether the issuer can change
|
||||
// corresponding feature flags after issuance via MPTokenIssuanceSet.
|
||||
lsmfMPTCanMutateCanLock = 0x00000002,
|
||||
lsmfMPTCanMutateRequireAuth = 0x00000004,
|
||||
lsmfMPTCanMutateCanEscrow = 0x00000008,
|
||||
@@ -176,6 +179,12 @@ enum LedgerSpecificFlags {
|
||||
lsmfMPTCanMutateCanClawback = 0x00000040,
|
||||
lsmfMPTCanMutateMetadata = 0x00010000,
|
||||
lsmfMPTCanMutateTransferFee = 0x00020000,
|
||||
// Controls mutability of lsfMPTCanPrivacy. Note the inverted naming:
|
||||
// - Other mutable flags: "CanMutate" means issuer CAN change the setting
|
||||
// - This flag: "CannotMutate" means issuer CANNOT change the setting
|
||||
// By default (flag not set), issuer can toggle lsfMPTCanPrivacy on/off.
|
||||
// If set, lsfMPTCanPrivacy is permanently locked to its creation value.
|
||||
lsmfMPTCannotMutatePrivacy = 0x00040000,
|
||||
|
||||
// ltMPTOKEN
|
||||
lsfMPTAuthorized = 0x00000002,
|
||||
|
||||
@@ -296,4 +296,43 @@ std::size_t constexpr permissionMaxSize = 10;
|
||||
/** The maximum number of transactions that can be in a batch. */
|
||||
std::size_t constexpr maxBatchTxCount = 8;
|
||||
|
||||
/** EC ElGamal ciphertext length 33-byte */
|
||||
std::size_t constexpr ecGamalEncryptedLength = 33;
|
||||
|
||||
/** EC ElGamal ciphertext length: two 33-byte components concatenated */
|
||||
std::size_t constexpr ecGamalEncryptedTotalLength = 66;
|
||||
|
||||
/** Length of equality ZKProof in bytes */
|
||||
std::size_t constexpr ecEqualityProofLength = 98;
|
||||
|
||||
/** Length of EC point (compressed) */
|
||||
std::size_t constexpr compressedECPointLength = 33;
|
||||
|
||||
/** Length of EC public key (compressed) */
|
||||
std::size_t constexpr ecPubKeyLength = compressedECPointLength;
|
||||
|
||||
/** Length of EC private key in bytes */
|
||||
std::size_t constexpr ecPrivKeyLength = 32;
|
||||
|
||||
/** Length of the EC blinding factor in bytes */
|
||||
std::size_t constexpr ecBlindingFactorLength = 32;
|
||||
|
||||
/** Length of Schnorr ZKProof for public key registration in bytes */
|
||||
std::size_t constexpr ecSchnorrProofLength = 65;
|
||||
|
||||
/** Length of ElGamal ciphertext equality proof in bytes */
|
||||
std::size_t constexpr ecCiphertextEqualityProofLength = 261;
|
||||
|
||||
/** Length of ElGamal Pedersen linkage proof in bytes */
|
||||
std::size_t constexpr ecPedersenProofLength = 195;
|
||||
|
||||
/** Length of Pedersen Commitment (compressed) */
|
||||
std::size_t constexpr ecPedersenCommitmentLength = compressedECPointLength;
|
||||
|
||||
/** Length of single bulletproof (range proof for 1 commitment) in bytes */
|
||||
std::size_t constexpr ecSingleBulletproofLength = 688;
|
||||
|
||||
/** Length of double bulletproof (range proof for 2 commitments) in bytes */
|
||||
std::size_t constexpr ecDoubleBulletproofLength = 754;
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -121,6 +121,7 @@ enum TEMcodes : TERUnderlyingType {
|
||||
temARRAY_TOO_LARGE,
|
||||
temBAD_TRANSFER_FEE,
|
||||
temINVALID_INNER_BATCH,
|
||||
temBAD_CIPHERTEXT,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -346,6 +347,7 @@ enum TECcodes : TERUnderlyingType {
|
||||
// backward compatibility with historical data on non-prod networks, can be
|
||||
// reclaimed after those networks reset.
|
||||
tecNO_DELEGATE_PERMISSION = 198,
|
||||
tecBAD_PROOF = 199
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -131,8 +131,9 @@ constexpr std::uint32_t const tfMPTCanEscrow = lsfMPTCanEscrow;
|
||||
constexpr std::uint32_t const tfMPTCanTrade = lsfMPTCanTrade;
|
||||
constexpr std::uint32_t const tfMPTCanTransfer = lsfMPTCanTransfer;
|
||||
constexpr std::uint32_t const tfMPTCanClawback = lsfMPTCanClawback;
|
||||
constexpr std::uint32_t const tfMPTCanPrivacy = lsfMPTCanPrivacy;
|
||||
constexpr std::uint32_t const tfMPTokenIssuanceCreateMask =
|
||||
~(tfUniversal | tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback);
|
||||
~(tfUniversal | tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback | tfMPTCanPrivacy);
|
||||
|
||||
// MPTokenIssuanceCreate MutableFlags:
|
||||
// Indicating specific fields or flags may be changed after issuance.
|
||||
@@ -144,9 +145,13 @@ constexpr std::uint32_t const tmfMPTCanMutateCanTransfer = lsmfMPTCanMutateCanTr
|
||||
constexpr std::uint32_t const tmfMPTCanMutateCanClawback = lsmfMPTCanMutateCanClawback;
|
||||
constexpr std::uint32_t const tmfMPTCanMutateMetadata = lsmfMPTCanMutateMetadata;
|
||||
constexpr std::uint32_t const tmfMPTCanMutateTransferFee = lsmfMPTCanMutateTransferFee;
|
||||
|
||||
// Issuer can mutate lsfMPTPrivacy by default unless lsmfMPTCannotMutatePrivacy is set.
|
||||
constexpr std::uint32_t const tmfMPTCannotMutatePrivacy = lsmfMPTCannotMutatePrivacy;
|
||||
constexpr std::uint32_t const tmfMPTokenIssuanceCreateMutableMask =
|
||||
~(tmfMPTCanMutateCanLock | tmfMPTCanMutateRequireAuth | tmfMPTCanMutateCanEscrow | tmfMPTCanMutateCanTrade
|
||||
| tmfMPTCanMutateCanTransfer | tmfMPTCanMutateCanClawback | tmfMPTCanMutateMetadata | tmfMPTCanMutateTransferFee);
|
||||
| tmfMPTCanMutateCanTransfer | tmfMPTCanMutateCanClawback | tmfMPTCanMutateMetadata | tmfMPTCanMutateTransferFee
|
||||
| tmfMPTCannotMutatePrivacy);
|
||||
|
||||
// MPTokenAuthorize flags:
|
||||
constexpr std::uint32_t const tfMPTUnauthorize = 0x00000001;
|
||||
@@ -172,10 +177,12 @@ constexpr std::uint32_t const tmfMPTSetCanTransfer = 0x00000100;
|
||||
constexpr std::uint32_t const tmfMPTClearCanTransfer = 0x00000200;
|
||||
constexpr std::uint32_t const tmfMPTSetCanClawback = 0x00000400;
|
||||
constexpr std::uint32_t const tmfMPTClearCanClawback = 0x00000800;
|
||||
constexpr std::uint32_t const tmfMPTSetPrivacy = 0x00001000;
|
||||
constexpr std::uint32_t const tmfMPTClearPrivacy = 0x00002000;
|
||||
constexpr std::uint32_t const tmfMPTokenIssuanceSetMutableMask = ~(tmfMPTSetCanLock | tmfMPTClearCanLock |
|
||||
tmfMPTSetRequireAuth | tmfMPTClearRequireAuth | tmfMPTSetCanEscrow | tmfMPTClearCanEscrow |
|
||||
tmfMPTSetCanTrade | tmfMPTClearCanTrade | tmfMPTSetCanTransfer | tmfMPTClearCanTransfer |
|
||||
tmfMPTSetCanClawback | tmfMPTClearCanClawback);
|
||||
tmfMPTSetCanClawback | tmfMPTClearCanClawback | tmfMPTSetPrivacy | tmfMPTClearPrivacy);
|
||||
|
||||
// MPTokenIssuanceDestroy flags:
|
||||
constexpr std::uint32_t const tfMPTokenIssuanceDestroyMask = ~tfUniversal;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
// Add new amendments to the top of this list.
|
||||
// Keep it sorted in reverse chronological order.
|
||||
XRPL_FEATURE(ConfidentialTransfer, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (PermissionedDomainInvariant, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (ExpiredNFTokenOfferRemoval, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (BatchInnerSigs, Supported::yes, VoteBehavior::DefaultNo)
|
||||
|
||||
@@ -398,6 +398,9 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
||||
{sfDomainID, soeOPTIONAL},
|
||||
{sfMutableFlags, soeDEFAULT},
|
||||
{sfIssuerElGamalPublicKey, soeOPTIONAL},
|
||||
{sfAuditorElGamalPublicKey, soeOPTIONAL},
|
||||
{sfConfidentialOutstandingAmount, soeDEFAULT},
|
||||
}))
|
||||
|
||||
/** A ledger object which tracks MPToken
|
||||
@@ -411,6 +414,12 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
|
||||
{sfOwnerNode, soeREQUIRED},
|
||||
{sfPreviousTxnID, soeREQUIRED},
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
||||
{sfConfidentialBalanceInbox, soeOPTIONAL},
|
||||
{sfConfidentialBalanceSpending, soeOPTIONAL},
|
||||
{sfConfidentialBalanceVersion, soeDEFAULT},
|
||||
{sfIssuerEncryptedBalance, soeOPTIONAL},
|
||||
{sfAuditorEncryptedBalance, soeOPTIONAL},
|
||||
{sfHolderElGamalPublicKey, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** A ledger object which tracks Oracle
|
||||
|
||||
@@ -114,6 +114,7 @@ TYPED_SFIELD(sfInterestRate, UINT32, 65) // 1/10 basis points (bi
|
||||
TYPED_SFIELD(sfLateInterestRate, UINT32, 66) // 1/10 basis points (bips)
|
||||
TYPED_SFIELD(sfCloseInterestRate, UINT32, 67) // 1/10 basis points (bips)
|
||||
TYPED_SFIELD(sfOverpaymentInterestRate, UINT32, 68) // 1/10 basis points (bips)
|
||||
TYPED_SFIELD(sfConfidentialBalanceVersion, UINT32, 69)
|
||||
|
||||
// 64-bit integers (common)
|
||||
TYPED_SFIELD(sfIndexNext, UINT64, 1)
|
||||
@@ -147,6 +148,7 @@ TYPED_SFIELD(sfSubjectNode, UINT64, 28)
|
||||
TYPED_SFIELD(sfLockedAmount, UINT64, 29, SField::sMD_BaseTen|SField::sMD_Default)
|
||||
TYPED_SFIELD(sfVaultNode, UINT64, 30)
|
||||
TYPED_SFIELD(sfLoanBrokerNode, UINT64, 31)
|
||||
TYPED_SFIELD(sfConfidentialOutstandingAmount, UINT64, 32, SField::sMD_BaseTen|SField::sMD_Default)
|
||||
|
||||
// 128-bit
|
||||
TYPED_SFIELD(sfEmailHash, UINT128, 1)
|
||||
@@ -297,6 +299,22 @@ TYPED_SFIELD(sfAssetClass, VL, 28)
|
||||
TYPED_SFIELD(sfProvider, VL, 29)
|
||||
TYPED_SFIELD(sfMPTokenMetadata, VL, 30)
|
||||
TYPED_SFIELD(sfCredentialType, VL, 31)
|
||||
TYPED_SFIELD(sfConfidentialBalanceInbox, VL, 32)
|
||||
TYPED_SFIELD(sfConfidentialBalanceSpending, VL, 33)
|
||||
TYPED_SFIELD(sfIssuerEncryptedBalance, VL, 34)
|
||||
TYPED_SFIELD(sfIssuerElGamalPublicKey, VL, 35)
|
||||
TYPED_SFIELD(sfHolderElGamalPublicKey, VL, 36)
|
||||
TYPED_SFIELD(sfZKProof, VL, 37)
|
||||
TYPED_SFIELD(sfHolderEncryptedAmount, VL, 38)
|
||||
TYPED_SFIELD(sfIssuerEncryptedAmount, VL, 39)
|
||||
TYPED_SFIELD(sfSenderEncryptedAmount, VL, 40)
|
||||
TYPED_SFIELD(sfDestinationEncryptedAmount, VL, 41)
|
||||
TYPED_SFIELD(sfAuditorEncryptedBalance, VL, 42)
|
||||
TYPED_SFIELD(sfAuditorEncryptedAmount, VL, 43)
|
||||
TYPED_SFIELD(sfAuditorElGamalPublicKey, VL, 44)
|
||||
TYPED_SFIELD(sfBlindingFactor, VL, 45)
|
||||
TYPED_SFIELD(sfAmountCommitment, VL, 46)
|
||||
TYPED_SFIELD(sfBalanceCommitment, VL, 47)
|
||||
|
||||
// account (common)
|
||||
TYPED_SFIELD(sfAccount, ACCOUNT, 1)
|
||||
|
||||
@@ -722,6 +722,8 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet,
|
||||
{sfMPTokenMetadata, soeOPTIONAL},
|
||||
{sfTransferFee, soeOPTIONAL},
|
||||
{sfMutableFlags, soeOPTIONAL},
|
||||
{sfIssuerElGamalPublicKey, soeOPTIONAL},
|
||||
{sfAuditorElGamalPublicKey, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** This transaction type authorizes a MPToken instance */
|
||||
@@ -1058,6 +1060,90 @@ TRANSACTION(ttLOAN_PAY, 84, LoanPay,
|
||||
{sfAmount, soeREQUIRED, soeMPTSupported},
|
||||
}))
|
||||
|
||||
/** This transaction type converts into confidential MPT balance. */
|
||||
#if TRANSACTION_INCLUDE
|
||||
#include <xrpld/app/tx/detail/ConfidentialMPTConvert.h>
|
||||
#endif
|
||||
TRANSACTION(ttCONFIDENTIAL_MPT_CONVERT, 85, ConfidentialMPTConvert,
|
||||
Delegation::delegable,
|
||||
featureConfidentialTransfer,
|
||||
noPriv,
|
||||
({
|
||||
{sfMPTokenIssuanceID, soeREQUIRED},
|
||||
{sfMPTAmount, soeREQUIRED},
|
||||
{sfHolderElGamalPublicKey, soeOPTIONAL},
|
||||
{sfHolderEncryptedAmount, soeREQUIRED},
|
||||
{sfIssuerEncryptedAmount, soeREQUIRED},
|
||||
{sfAuditorEncryptedAmount, soeOPTIONAL},
|
||||
{sfBlindingFactor, soeREQUIRED},
|
||||
{sfZKProof, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** This transaction type merges MPT inbox. */
|
||||
#if TRANSACTION_INCLUDE
|
||||
#include <xrpld/app/tx/detail/ConfidentialMPTMergeInbox.h>
|
||||
#endif
|
||||
TRANSACTION(ttCONFIDENTIAL_MPT_MERGE_INBOX, 86, ConfidentialMPTMergeInbox,
|
||||
Delegation::delegable,
|
||||
featureConfidentialTransfer,
|
||||
noPriv,
|
||||
({
|
||||
{sfMPTokenIssuanceID, soeREQUIRED},
|
||||
}))
|
||||
|
||||
/** This transaction type converts back into public MPT balance. */
|
||||
#if TRANSACTION_INCLUDE
|
||||
#include <xrpld/app/tx/detail/ConfidentialMPTConvertBack.h>
|
||||
#endif
|
||||
TRANSACTION(ttCONFIDENTIAL_MPT_CONVERT_BACK, 87, ConfidentialMPTConvertBack,
|
||||
Delegation::delegable,
|
||||
featureConfidentialTransfer,
|
||||
noPriv,
|
||||
({
|
||||
{sfMPTokenIssuanceID, soeREQUIRED},
|
||||
{sfMPTAmount, soeREQUIRED},
|
||||
{sfHolderEncryptedAmount, soeREQUIRED},
|
||||
{sfIssuerEncryptedAmount, soeREQUIRED},
|
||||
{sfAuditorEncryptedAmount, soeOPTIONAL},
|
||||
{sfBlindingFactor, soeREQUIRED},
|
||||
{sfZKProof, soeREQUIRED},
|
||||
{sfBalanceCommitment, soeREQUIRED},
|
||||
}))
|
||||
|
||||
#if TRANSACTION_INCLUDE
|
||||
#include <xrpld/app/tx/detail/ConfidentialMPTSend.h>
|
||||
#endif
|
||||
TRANSACTION(ttCONFIDENTIAL_MPT_SEND, 88, ConfidentialMPTSend,
|
||||
Delegation::delegable,
|
||||
featureConfidentialTransfer,
|
||||
noPriv,
|
||||
({
|
||||
{sfMPTokenIssuanceID, soeREQUIRED},
|
||||
{sfDestination, soeREQUIRED},
|
||||
{sfSenderEncryptedAmount, soeREQUIRED},
|
||||
{sfDestinationEncryptedAmount, soeREQUIRED},
|
||||
{sfIssuerEncryptedAmount, soeREQUIRED},
|
||||
{sfAuditorEncryptedAmount, soeOPTIONAL},
|
||||
{sfZKProof, soeREQUIRED},
|
||||
{sfAmountCommitment, soeREQUIRED},
|
||||
{sfBalanceCommitment, soeREQUIRED},
|
||||
{sfCredentialIDs, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
#if TRANSACTION_INCLUDE
|
||||
#include <xrpld/app/tx/detail/ConfidentialMPTClawback.h>
|
||||
#endif
|
||||
TRANSACTION(ttCONFIDENTIAL_MPT_CLAWBACK, 89, ConfidentialMPTClawback,
|
||||
Delegation::delegable,
|
||||
featureConfidentialTransfer,
|
||||
noPriv,
|
||||
({
|
||||
{sfMPTokenIssuanceID, soeREQUIRED},
|
||||
{sfHolder, soeREQUIRED},
|
||||
{sfMPTAmount, soeREQUIRED},
|
||||
{sfZKProof, soeREQUIRED},
|
||||
}))
|
||||
|
||||
/** This system-generated transaction type is used to update the status of the various amendments.
|
||||
|
||||
For details, see: https://xrpl.org/amendments.html
|
||||
|
||||
@@ -468,7 +468,8 @@ accountHolds(
|
||||
|
||||
// Only if auth check is needed, as it needs to do an additional read
|
||||
// operation. Note featureSingleAssetVault will affect error codes.
|
||||
if (zeroIfUnauthorized == ahZERO_IF_UNAUTHORIZED && view.rules().enabled(featureSingleAssetVault))
|
||||
if (zeroIfUnauthorized == ahZERO_IF_UNAUTHORIZED &&
|
||||
(view.rules().enabled(featureSingleAssetVault) || view.rules().enabled(featureConfidentialTransfer)))
|
||||
{
|
||||
if (auto const err = requireAuth(view, mptIssue, account, AuthType::StrongAuth); !isTesSuccess(err))
|
||||
amount.clear(mptIssue);
|
||||
|
||||
659
src/libxrpl/protocol/ConfidentialTransfer.cpp
Normal file
659
src/libxrpl/protocol/ConfidentialTransfer.cpp
Normal file
@@ -0,0 +1,659 @@
|
||||
#include <xrpl/protocol/ConfidentialTransfer.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
|
||||
#include <boost/endian/conversion.hpp>
|
||||
|
||||
#include <openssl/rand.h>
|
||||
#include <openssl/sha.h>
|
||||
|
||||
namespace xrpl {
|
||||
void
|
||||
addCommonZKPFields(
|
||||
Serializer& s,
|
||||
std::uint16_t txType,
|
||||
AccountID const& account,
|
||||
std::uint32_t sequence,
|
||||
uint192 const& issuanceID)
|
||||
{
|
||||
s.add16(txType);
|
||||
s.addBitString(account);
|
||||
s.add32(sequence);
|
||||
s.addBitString(issuanceID);
|
||||
}
|
||||
|
||||
uint256
|
||||
getSendContextHash(
|
||||
AccountID const& account,
|
||||
std::uint32_t sequence,
|
||||
uint192 const& issuanceID,
|
||||
AccountID const& destination,
|
||||
std::uint32_t version)
|
||||
{
|
||||
Serializer s;
|
||||
addCommonZKPFields(s, ttCONFIDENTIAL_MPT_SEND, account, sequence, issuanceID);
|
||||
|
||||
s.addBitString(destination);
|
||||
s.addInteger(version);
|
||||
|
||||
return s.getSHA512Half();
|
||||
}
|
||||
|
||||
uint256
|
||||
getClawbackContextHash(
|
||||
AccountID const& account,
|
||||
std::uint32_t sequence,
|
||||
uint192 const& issuanceID,
|
||||
std::uint64_t amount,
|
||||
AccountID const& holder)
|
||||
{
|
||||
Serializer s;
|
||||
addCommonZKPFields(s, ttCONFIDENTIAL_MPT_CLAWBACK, account, sequence, issuanceID);
|
||||
|
||||
s.add64(amount);
|
||||
s.addBitString(holder);
|
||||
|
||||
return s.getSHA512Half();
|
||||
}
|
||||
|
||||
uint256
|
||||
getConvertContextHash(AccountID const& account, std::uint32_t sequence, uint192 const& issuanceID, std::uint64_t amount)
|
||||
{
|
||||
Serializer s;
|
||||
addCommonZKPFields(s, ttCONFIDENTIAL_MPT_CONVERT, account, sequence, issuanceID);
|
||||
|
||||
s.add64(amount);
|
||||
|
||||
return s.getSHA512Half();
|
||||
}
|
||||
|
||||
uint256
|
||||
getConvertBackContextHash(
|
||||
AccountID const& account,
|
||||
std::uint32_t sequence,
|
||||
uint192 const& issuanceID,
|
||||
std::uint64_t amount,
|
||||
std::uint32_t version)
|
||||
{
|
||||
Serializer s;
|
||||
addCommonZKPFields(s, ttCONFIDENTIAL_MPT_CONVERT_BACK, account, sequence, issuanceID);
|
||||
|
||||
s.add64(amount);
|
||||
s.addInteger(version);
|
||||
|
||||
return s.getSHA512Half();
|
||||
}
|
||||
|
||||
bool
|
||||
makeEcPair(Slice const& buffer, secp256k1_pubkey& out1, secp256k1_pubkey& out2)
|
||||
{
|
||||
auto parsePubKey = [](Slice const& slice, secp256k1_pubkey& out) {
|
||||
return secp256k1_ec_pubkey_parse(
|
||||
secp256k1Context(), &out, reinterpret_cast<unsigned char const*>(slice.data()), slice.length());
|
||||
};
|
||||
|
||||
Slice s1{buffer.data(), ecGamalEncryptedLength};
|
||||
Slice s2{buffer.data() + ecGamalEncryptedLength, ecGamalEncryptedLength};
|
||||
|
||||
int const ret1 = parsePubKey(s1, out1);
|
||||
int const ret2 = parsePubKey(s2, out2);
|
||||
|
||||
return ret1 == 1 && ret2 == 1;
|
||||
}
|
||||
|
||||
bool
|
||||
serializeEcPair(secp256k1_pubkey const& in1, secp256k1_pubkey const& in2, Buffer& buffer)
|
||||
{
|
||||
auto serializePubKey = [](secp256k1_pubkey const& pub, unsigned char* out) {
|
||||
size_t outLen = ecGamalEncryptedLength; // 33 bytes
|
||||
int const ret = secp256k1_ec_pubkey_serialize(secp256k1Context(), out, &outLen, &pub, SECP256K1_EC_COMPRESSED);
|
||||
return ret == 1 && outLen == ecGamalEncryptedLength;
|
||||
};
|
||||
|
||||
unsigned char* ptr = buffer.data();
|
||||
bool const res1 = serializePubKey(in1, ptr);
|
||||
bool const res2 = serializePubKey(in2, ptr + ecGamalEncryptedLength);
|
||||
|
||||
return res1 && res2;
|
||||
}
|
||||
|
||||
bool
|
||||
isValidCiphertext(Slice const& buffer)
|
||||
{
|
||||
secp256k1_pubkey key1;
|
||||
secp256k1_pubkey key2;
|
||||
return makeEcPair(buffer, key1, key2);
|
||||
}
|
||||
|
||||
bool
|
||||
isValidCompressedECPoint(Slice const& buffer)
|
||||
{
|
||||
if (buffer.size() != compressedECPointLength)
|
||||
return false;
|
||||
|
||||
// Compressed EC points must start with 0x02 or 0x03
|
||||
if (buffer[0] != 0x02 && buffer[0] != 0x03)
|
||||
return false;
|
||||
|
||||
secp256k1_pubkey point;
|
||||
return secp256k1_ec_pubkey_parse(secp256k1Context(), &point, buffer.data(), buffer.size()) == 1;
|
||||
}
|
||||
|
||||
TER
|
||||
homomorphicAdd(Slice const& a, Slice const& b, Buffer& out)
|
||||
{
|
||||
if (a.length() != ecGamalEncryptedTotalLength || b.length() != ecGamalEncryptedTotalLength)
|
||||
return tecINTERNAL;
|
||||
|
||||
secp256k1_pubkey aC1;
|
||||
secp256k1_pubkey aC2;
|
||||
secp256k1_pubkey bC1;
|
||||
secp256k1_pubkey bC2;
|
||||
|
||||
if (!makeEcPair(a, aC1, aC2) || !makeEcPair(b, bC1, bC2))
|
||||
return tecINTERNAL;
|
||||
|
||||
secp256k1_pubkey sumC1;
|
||||
secp256k1_pubkey sumC2;
|
||||
|
||||
if (secp256k1_elgamal_add(secp256k1Context(), &sumC1, &sumC2, &aC1, &aC2, &bC1, &bC2) != 1)
|
||||
return tecINTERNAL;
|
||||
|
||||
if (!serializeEcPair(sumC1, sumC2, out))
|
||||
return tecINTERNAL;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
homomorphicSubtract(Slice const& a, Slice const& b, Buffer& out)
|
||||
{
|
||||
if (a.length() != ecGamalEncryptedTotalLength || b.length() != ecGamalEncryptedTotalLength)
|
||||
return tecINTERNAL;
|
||||
|
||||
secp256k1_pubkey aC1;
|
||||
secp256k1_pubkey aC2;
|
||||
secp256k1_pubkey bC1;
|
||||
secp256k1_pubkey bC2;
|
||||
|
||||
if (!makeEcPair(a, aC1, aC2) || !makeEcPair(b, bC1, bC2))
|
||||
return tecINTERNAL;
|
||||
|
||||
secp256k1_pubkey diffC1;
|
||||
secp256k1_pubkey diffC2;
|
||||
|
||||
if (secp256k1_elgamal_subtract(secp256k1Context(), &diffC1, &diffC2, &aC1, &aC2, &bC1, &bC2) != 1)
|
||||
return tecINTERNAL;
|
||||
|
||||
if (!serializeEcPair(diffC1, diffC2, out))
|
||||
return tecINTERNAL;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
Buffer
|
||||
generateBlindingFactor()
|
||||
{
|
||||
unsigned char blindingFactor[ecBlindingFactorLength];
|
||||
|
||||
// todo: might need to be updated using another RNG
|
||||
if (RAND_bytes(blindingFactor, ecBlindingFactorLength) != 1)
|
||||
Throw<std::runtime_error>("Failed to generate random number");
|
||||
|
||||
return Buffer(blindingFactor, ecBlindingFactorLength);
|
||||
}
|
||||
|
||||
std::optional<Buffer>
|
||||
encryptAmount(uint64_t const amt, Slice const& pubKeySlice, Slice const& blindingFactor)
|
||||
{
|
||||
if (blindingFactor.size() != ecBlindingFactorLength)
|
||||
return std::nullopt;
|
||||
|
||||
if (pubKeySlice.size() != ecPubKeyLength)
|
||||
return std::nullopt;
|
||||
|
||||
secp256k1_pubkey c1, c2, pubKey;
|
||||
if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength) != 1)
|
||||
return std::nullopt;
|
||||
|
||||
if (!secp256k1_elgamal_encrypt(secp256k1Context(), &c1, &c2, &pubKey, amt, blindingFactor.data()))
|
||||
return std::nullopt;
|
||||
|
||||
Buffer buf(ecGamalEncryptedTotalLength);
|
||||
if (!serializeEcPair(c1, c2, buf))
|
||||
return std::nullopt;
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
std::optional<Buffer>
|
||||
encryptCanonicalZeroAmount(Slice const& pubKeySlice, AccountID const& account, MPTID const& mptId)
|
||||
{
|
||||
if (pubKeySlice.size() != ecPubKeyLength)
|
||||
return std::nullopt; // LCOV_EXCL_LINE
|
||||
|
||||
secp256k1_pubkey c1, c2, pubKey;
|
||||
if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength) != 1)
|
||||
return std::nullopt; // LCOV_EXCL_LINE
|
||||
|
||||
if (!generate_canonical_encrypted_zero(secp256k1Context(), &c1, &c2, &pubKey, account.data(), mptId.data()))
|
||||
return std::nullopt; // LCOV_EXCL_LINE
|
||||
|
||||
Buffer buf(ecGamalEncryptedTotalLength);
|
||||
if (!serializeEcPair(c1, c2, buf))
|
||||
return std::nullopt; // LCOV_EXCL_LINE
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
TER
|
||||
verifySchnorrProof(Slice const& pubKeySlice, Slice const& proofSlice, uint256 const& contextHash)
|
||||
{
|
||||
if (proofSlice.size() != ecSchnorrProofLength)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (pubKeySlice.size() != ecPubKeyLength)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
secp256k1_pubkey pubKey;
|
||||
if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength) != 1)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (secp256k1_mpt_pok_sk_verify(secp256k1Context(), proofSlice.data(), &pubKey, contextHash.data()) != 1)
|
||||
return tecBAD_PROOF;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
verifyElGamalEncryption(
|
||||
std::uint64_t const amount,
|
||||
Slice const& blindingFactor,
|
||||
Slice const& pubKeySlice,
|
||||
Slice const& ciphertext)
|
||||
{
|
||||
if (blindingFactor.size() != ecBlindingFactorLength)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (pubKeySlice.size() != ecPubKeyLength)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
secp256k1_pubkey pubKey;
|
||||
if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength) != 1)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
secp256k1_pubkey c1, c2;
|
||||
if (!makeEcPair(ciphertext, c1, c2))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (secp256k1_elgamal_verify_encryption(secp256k1Context(), &c1, &c2, &pubKey, amount, blindingFactor.data()) != 1)
|
||||
return tecBAD_PROOF;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
verifyRevealedAmount(
|
||||
std::uint64_t const amount,
|
||||
Slice const& blindingFactor,
|
||||
ConfidentialRecipient const& holder,
|
||||
ConfidentialRecipient const& issuer,
|
||||
std::optional<ConfidentialRecipient> const& auditor)
|
||||
{
|
||||
if (auto const res = verifyElGamalEncryption(amount, blindingFactor, holder.publicKey, holder.encryptedAmount);
|
||||
!isTesSuccess(res))
|
||||
{
|
||||
return res;
|
||||
}
|
||||
|
||||
if (auto const res = verifyElGamalEncryption(amount, blindingFactor, issuer.publicKey, issuer.encryptedAmount);
|
||||
!isTesSuccess(res))
|
||||
{
|
||||
return res;
|
||||
}
|
||||
|
||||
if (auditor)
|
||||
{
|
||||
if (auto const res =
|
||||
verifyElGamalEncryption(amount, blindingFactor, auditor->publicKey, auditor->encryptedAmount);
|
||||
!isTesSuccess(res))
|
||||
{
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
getMultiCiphertextEqualityProofSize(std::size_t nRecipients)
|
||||
{
|
||||
// Points (33 bytes): T_m (1) + T_rG (nRecipients) + T_rP (nRecipients) = 1
|
||||
// + 2nRecipients Scalars (32 bytes): s_m (1) + s_r (nRecipients) = 1 +
|
||||
// nRecipients
|
||||
return ((1 + (2 * nRecipients)) * 33) + ((1 + nRecipients) * 32);
|
||||
}
|
||||
|
||||
TER
|
||||
verifyMultiCiphertextEqualityProof(
|
||||
Slice const& proof,
|
||||
std::vector<ConfidentialRecipient> const& recipients,
|
||||
std::size_t const nRecipients,
|
||||
uint256 const& contextHash)
|
||||
{
|
||||
if (recipients.size() != nRecipients)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (proof.size() != getMultiCiphertextEqualityProofSize(nRecipients))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
std::vector<secp256k1_pubkey> r(nRecipients);
|
||||
std::vector<secp256k1_pubkey> s(nRecipients);
|
||||
std::vector<secp256k1_pubkey> pk(nRecipients);
|
||||
|
||||
for (size_t i = 0; i < nRecipients; ++i)
|
||||
{
|
||||
auto const& recipient = recipients[i];
|
||||
if (recipient.encryptedAmount.size() != ecGamalEncryptedTotalLength)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (!makeEcPair(recipient.encryptedAmount, r[i], s[i]))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (recipient.publicKey.size() != ecPubKeyLength)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pk[i], recipient.publicKey.data(), ecPubKeyLength) != 1)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
int const result = secp256k1_mpt_verify_same_plaintext_multi(
|
||||
secp256k1Context(), proof.data(), proof.size(), nRecipients, r.data(), s.data(), pk.data(), contextHash.data());
|
||||
|
||||
if (result != 1)
|
||||
return tecBAD_PROOF;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
verifyClawbackEqualityProof(
|
||||
uint64_t const amount,
|
||||
Slice const& proof,
|
||||
Slice const& pubKeySlice,
|
||||
Slice const& ciphertext,
|
||||
uint256 const& contextHash)
|
||||
{
|
||||
secp256k1_pubkey c1, c2;
|
||||
if (!makeEcPair(ciphertext, c1, c2))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (pubKeySlice.size() != ecPubKeyLength)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
secp256k1_pubkey pubKey;
|
||||
if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength) != 1)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// Note: c2, c1 order - the proof is generated with c2 first (the encrypted
|
||||
// message component) because the equality proof structure expects the
|
||||
// message-containing term before the blinding term.
|
||||
if (secp256k1_equality_plaintext_verify(
|
||||
secp256k1Context(), proof.data(), &pubKey, &c2, &c1, amount, contextHash.data()) != 1)
|
||||
{
|
||||
return tecBAD_PROOF;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
NotTEC
|
||||
checkEncryptedAmountFormat(STObject const& object)
|
||||
{
|
||||
// Current usage of this function is only for ConfidentialMPTConvert and
|
||||
// ConfidentialMPTConvertBack transactions, which already enforce that these fields
|
||||
// are present.
|
||||
if (!object.isFieldPresent(sfHolderEncryptedAmount) || !object.isFieldPresent(sfIssuerEncryptedAmount))
|
||||
return temMALFORMED; // LCOV_EXCL_LINE
|
||||
|
||||
if (object[sfHolderEncryptedAmount].length() != ecGamalEncryptedTotalLength ||
|
||||
object[sfIssuerEncryptedAmount].length() != ecGamalEncryptedTotalLength)
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
bool const hasAuditor = object.isFieldPresent(sfAuditorEncryptedAmount);
|
||||
if (hasAuditor && object[sfAuditorEncryptedAmount].length() != ecGamalEncryptedTotalLength)
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
if (!isValidCiphertext(object[sfHolderEncryptedAmount]) || !isValidCiphertext(object[sfIssuerEncryptedAmount]))
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
if (hasAuditor && !isValidCiphertext(object[sfAuditorEncryptedAmount]))
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
verifyAmountPcmLinkage(
|
||||
Slice const& proof,
|
||||
Slice const& encAmt,
|
||||
Slice const& pubKeySlice,
|
||||
Slice const& pcmSlice,
|
||||
uint256 const& contextHash)
|
||||
{
|
||||
if (proof.length() != ecPedersenProofLength)
|
||||
return tecINTERNAL;
|
||||
|
||||
secp256k1_pubkey c1, c2;
|
||||
if (!makeEcPair(encAmt, c1, c2))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (pubKeySlice.size() != ecPubKeyLength)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (pcmSlice.size() != ecPedersenCommitmentLength)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
secp256k1_pubkey pubKey;
|
||||
if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength) != 1)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
secp256k1_pubkey pcm;
|
||||
if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pcm, pcmSlice.data(), ecPedersenCommitmentLength) != 1)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (secp256k1_elgamal_pedersen_link_verify(
|
||||
secp256k1Context(), proof.data(), &c1, &c2, &pubKey, &pcm, contextHash.data()) != 1)
|
||||
{
|
||||
return tecBAD_PROOF;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
verifyBalancePcmLinkage(
|
||||
Slice const& proof,
|
||||
Slice const& encAmt,
|
||||
Slice const& pubKeySlice,
|
||||
Slice const& pcmSlice,
|
||||
uint256 const& contextHash)
|
||||
{
|
||||
if (proof.length() != ecPedersenProofLength)
|
||||
return tecINTERNAL;
|
||||
|
||||
secp256k1_pubkey c1;
|
||||
secp256k1_pubkey c2;
|
||||
|
||||
if (!makeEcPair(encAmt, c1, c2))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (pubKeySlice.size() != ecPubKeyLength)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (pcmSlice.size() != ecPedersenCommitmentLength)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
secp256k1_pubkey pubKey;
|
||||
if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength) != 1)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
secp256k1_pubkey pcm;
|
||||
if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pcm, pcmSlice.data(), ecPedersenCommitmentLength) != 1)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// Note: c2, c1 order - the linkage proof expects the message-containing
|
||||
// component (c2 = m*G + r*Pk) before the blinding component (c1 = r*G).
|
||||
if (secp256k1_elgamal_pedersen_link_verify(
|
||||
secp256k1Context(), proof.data(), &pubKey, &c2, &c1, &pcm, contextHash.data()) != 1)
|
||||
{
|
||||
return tecBAD_PROOF;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
verifyAggregatedBulletproof(
|
||||
Slice const& proof,
|
||||
std::vector<Slice> const& compressedCommitments,
|
||||
uint256 const& contextHash)
|
||||
{
|
||||
// 1. Validate Aggregation Factor (m), m to be a power of 2
|
||||
std::size_t const m = compressedCommitments.size();
|
||||
if (m == 0 || (m & (m - 1)) != 0)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// 2. Prepare Pedersen Commitments, parse from compressed format
|
||||
auto const ctx = secp256k1Context();
|
||||
std::vector<secp256k1_pubkey> commitments(m);
|
||||
for (size_t i = 0; i < m; ++i)
|
||||
{
|
||||
// Sanity check length
|
||||
if (compressedCommitments[i].size() != ecPedersenCommitmentLength)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (secp256k1_ec_pubkey_parse(
|
||||
ctx, &commitments[i], compressedCommitments[i].data(), ecPedersenCommitmentLength) != 1)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
// 3. Prepare Generator Vectors (G_vec, H_vec)
|
||||
// The range proof requires vectors of size 64 * m
|
||||
std::size_t const n = 64 * m;
|
||||
std::vector<secp256k1_pubkey> G_vec(n);
|
||||
std::vector<secp256k1_pubkey> H_vec(n);
|
||||
|
||||
// Retrieve deterministic generators "G" and "H"
|
||||
if (secp256k1_mpt_get_generator_vector(ctx, G_vec.data(), n, (unsigned char const*)"G", 1) != 1)
|
||||
{
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
if (secp256k1_mpt_get_generator_vector(ctx, H_vec.data(), n, (unsigned char const*)"H", 1) != 1)
|
||||
{
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
// 4. Prepare Base Generator (pk_base / H)
|
||||
secp256k1_pubkey pk_base;
|
||||
if (secp256k1_mpt_get_h_generator(ctx, &pk_base) != 1)
|
||||
{
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
// 5. Verify the Proof
|
||||
int const result = secp256k1_bulletproof_verify_agg(
|
||||
ctx,
|
||||
G_vec.data(),
|
||||
H_vec.data(),
|
||||
reinterpret_cast<unsigned char const*>(proof.data()),
|
||||
proof.size(),
|
||||
commitments.data(),
|
||||
m,
|
||||
&pk_base,
|
||||
contextHash.data());
|
||||
|
||||
if (result != 1)
|
||||
return tecBAD_PROOF;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
computeSendRemainder(Slice const& balanceCommitment, Slice const& amountCommitment, Buffer& out)
|
||||
{
|
||||
if (balanceCommitment.size() != ecPedersenCommitmentLength || amountCommitment.size() != ecPedersenCommitmentLength)
|
||||
return tecINTERNAL;
|
||||
|
||||
auto const ctx = secp256k1Context();
|
||||
|
||||
secp256k1_pubkey pcBalance;
|
||||
if (secp256k1_ec_pubkey_parse(ctx, &pcBalance, balanceCommitment.data(), ecPedersenCommitmentLength) != 1)
|
||||
return tecINTERNAL;
|
||||
|
||||
secp256k1_pubkey pcAmount;
|
||||
if (secp256k1_ec_pubkey_parse(ctx, &pcAmount, amountCommitment.data(), ecPedersenCommitmentLength) != 1)
|
||||
return tecINTERNAL;
|
||||
|
||||
// Negate PC_amount point to get -PC_amount
|
||||
if (!secp256k1_ec_pubkey_negate(ctx, &pcAmount))
|
||||
return tecINTERNAL;
|
||||
|
||||
// Compute pcRem = pcBalance + (-pcAmount)
|
||||
secp256k1_pubkey const* summands[2] = {&pcBalance, &pcAmount};
|
||||
secp256k1_pubkey pcRem;
|
||||
if (!secp256k1_ec_pubkey_combine(ctx, &pcRem, summands, 2))
|
||||
return tecINTERNAL;
|
||||
|
||||
// Serialize result to compressed format
|
||||
out.alloc(ecPedersenCommitmentLength);
|
||||
size_t outLen = ecPedersenCommitmentLength;
|
||||
if (secp256k1_ec_pubkey_serialize(ctx, out.data(), &outLen, &pcRem, SECP256K1_EC_COMPRESSED) != 1)
|
||||
return tecINTERNAL;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
computeConvertBackRemainder(Slice const& commitment, std::uint64_t amount, Buffer& out)
|
||||
{
|
||||
if (commitment.size() != ecPedersenCommitmentLength || amount == 0)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const ctx = secp256k1Context();
|
||||
|
||||
// Parse commitment from compressed format
|
||||
secp256k1_pubkey pcBalance;
|
||||
if (secp256k1_ec_pubkey_parse(ctx, &pcBalance, commitment.data(), ecPedersenCommitmentLength) != 1)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// Convert amount to 32-byte big-endian scalar
|
||||
unsigned char mScalar[32] = {0};
|
||||
std::uint64_t amountBigEndian = boost::endian::native_to_big(amount);
|
||||
std::memcpy(&mScalar[24], &amountBigEndian, sizeof(amountBigEndian));
|
||||
|
||||
// Compute mG = amount * G
|
||||
secp256k1_pubkey mG;
|
||||
if (!secp256k1_ec_pubkey_create(ctx, &mG, mScalar))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// Negate mG to get -mG
|
||||
if (!secp256k1_ec_pubkey_negate(ctx, &mG))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// Compute pcRem = pcBalance + (-mG)
|
||||
secp256k1_pubkey const* summands[2] = {&pcBalance, &mG};
|
||||
secp256k1_pubkey pcRem;
|
||||
if (!secp256k1_ec_pubkey_combine(ctx, &pcRem, summands, 2))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// Serialize result to compressed format
|
||||
out.alloc(ecPedersenCommitmentLength);
|
||||
size_t outLen = ecPedersenCommitmentLength;
|
||||
if (secp256k1_ec_pubkey_serialize(ctx, out.data(), &outLen, &pcRem, SECP256K1_EC_COMPRESSED) != 1 ||
|
||||
outLen != ecPedersenCommitmentLength)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
} // namespace xrpl
|
||||
@@ -106,6 +106,7 @@ transResults()
|
||||
MAKE_ERROR(tecLIMIT_EXCEEDED, "Limit exceeded."),
|
||||
MAKE_ERROR(tecPSEUDO_ACCOUNT, "This operation is not allowed against a pseudo-account."),
|
||||
MAKE_ERROR(tecPRECISION_LOSS, "The amounts used by the transaction cannot interact."),
|
||||
MAKE_ERROR(tecBAD_PROOF, "Proof cannot be verified"),
|
||||
|
||||
MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."),
|
||||
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),
|
||||
@@ -198,6 +199,7 @@ transResults()
|
||||
MAKE_ERROR(temARRAY_TOO_LARGE, "Malformed: Array is too large."),
|
||||
MAKE_ERROR(temBAD_TRANSFER_FEE, "Malformed: Transfer fee is outside valid range."),
|
||||
MAKE_ERROR(temINVALID_INNER_BATCH, "Malformed: Invalid inner batch transaction."),
|
||||
MAKE_ERROR(temBAD_CIPHERTEXT, "Malformed: Invalid ciphertext."),
|
||||
|
||||
MAKE_ERROR(terRETRY, "Retry transaction."),
|
||||
MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."),
|
||||
|
||||
3837
src/test/app/ConfidentialTransfer_test.cpp
Normal file
3837
src/test/app/ConfidentialTransfer_test.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3707,6 +3707,189 @@ class Invariants_test : public beast::unit_test::suite
|
||||
precloseMpt);
|
||||
}
|
||||
|
||||
void
|
||||
testConfidentialMPTTransfer()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
testcase << "ValidConfidentialMPToken";
|
||||
|
||||
MPTID mptID;
|
||||
|
||||
// Generate an MPT with privacy, issue 100 tokens to A2.
|
||||
// Perform a confidential conversion to populate encrypted state.
|
||||
auto const precloseConfidential = [&mptID](Account const& A1, Account const& A2, Env& env) -> bool {
|
||||
MPTTester mpt(env, A1, {.holders = {A2}, .fund = false});
|
||||
mpt.create({.flags = tfMPTCanTransfer | tfMPTCanPrivacy});
|
||||
mptID = mpt.issuanceID();
|
||||
|
||||
mpt.authorize({.account = A2});
|
||||
mpt.pay(A1, A2, 100);
|
||||
|
||||
mpt.generateKeyPair(A1);
|
||||
mpt.set({.account = A1, .issuerPubKey = mpt.getPubKey(A1)});
|
||||
|
||||
mpt.generateKeyPair(A2);
|
||||
mpt.convert({
|
||||
.account = A2,
|
||||
.amt = 100,
|
||||
.holderPubKey = mpt.getPubKey(A2),
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
// badDelete
|
||||
doInvariantCheck(
|
||||
{"MPToken deleted with encrypted fields while COA > 0"},
|
||||
[&mptID](Account const& A1, Account const& A2, ApplyContext& ac) {
|
||||
auto sleToken = ac.view().peek(keylet::mptoken(mptID, A2.id()));
|
||||
if (!sleToken)
|
||||
return false;
|
||||
// Force an erase of the object while the COA remains 100
|
||||
ac.view().erase(sleToken);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
precloseConfidential);
|
||||
|
||||
// badConsistency
|
||||
doInvariantCheck(
|
||||
{"MPToken encrypted field existence inconsistency"},
|
||||
[&mptID](Account const& A1, Account const& A2, ApplyContext& ac) {
|
||||
auto sleToken = ac.view().peek(keylet::mptoken(mptID, A2.id()));
|
||||
if (!sleToken)
|
||||
return false;
|
||||
// Remove one of the required encrypted fields to create a mismatch
|
||||
sleToken->makeFieldAbsent(sfIssuerEncryptedBalance);
|
||||
ac.view().update(sleToken);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
|
||||
precloseConfidential);
|
||||
|
||||
// requiresPrivacyFlag
|
||||
auto const precloseNoPrivacy = [&mptID](Account const& A1, Account const& A2, Env& env) -> bool {
|
||||
MPTTester mpt(env, A1, {.holders = {A2}, .fund = false});
|
||||
// completely omitted the tfMPTCanPrivacy flag here.
|
||||
mpt.create({.flags = tfMPTCanTransfer});
|
||||
mptID = mpt.issuanceID();
|
||||
mpt.authorize({.account = A2});
|
||||
mpt.pay(A1, A2, 100);
|
||||
return true;
|
||||
};
|
||||
|
||||
doInvariantCheck(
|
||||
{"MPToken has encrypted fields but Issuance does not have lsfMPTCanPrivacy set"},
|
||||
[&mptID](Account const& A1, Account const& A2, ApplyContext& ac) {
|
||||
auto sleToken = ac.view().peek(keylet::mptoken(mptID, A2.id()));
|
||||
if (!sleToken)
|
||||
return false;
|
||||
// Inject fields correctly, but the Issuance was built without the privacy flag.
|
||||
sleToken->setFieldVL(sfConfidentialBalanceInbox, Blob{0x00});
|
||||
sleToken->setFieldVL(sfIssuerEncryptedBalance, Blob{0x00});
|
||||
ac.view().update(sleToken);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
|
||||
precloseNoPrivacy);
|
||||
|
||||
// badCOA
|
||||
doInvariantCheck(
|
||||
{"Confidential outstanding amount exceeds total outstanding amount"},
|
||||
[&mptID](Account const& A1, Account const& A2, ApplyContext& ac) {
|
||||
auto sleIssuance = ac.view().peek(keylet::mptIssuance(mptID));
|
||||
if (!sleIssuance)
|
||||
return false;
|
||||
// Total outstanding is natively 100; bloat the COA over 100
|
||||
sleIssuance->setFieldU64(sfConfidentialOutstandingAmount, 200);
|
||||
ac.view().update(sleIssuance);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttMPTOKEN_ISSUANCE_SET, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
|
||||
precloseConfidential);
|
||||
|
||||
// Conservation Violation
|
||||
doInvariantCheck(
|
||||
{"Token conservation violation for MPT"},
|
||||
[&mptID](Account const& A1, Account const& A2, ApplyContext& ac) {
|
||||
auto sleIssuance = ac.view().peek(keylet::mptIssuance(mptID));
|
||||
if (!sleIssuance)
|
||||
return false;
|
||||
|
||||
sleIssuance->setFieldU64(
|
||||
sfConfidentialOutstandingAmount, sleIssuance->getFieldU64(sfConfidentialOutstandingAmount) - 10);
|
||||
ac.view().update(sleIssuance);
|
||||
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
|
||||
precloseConfidential);
|
||||
|
||||
// badVersion
|
||||
doInvariantCheck(
|
||||
{"MPToken sfConfidentialBalanceVersion not updated when sfConfidentialBalanceSpending changed"},
|
||||
[&mptID](Account const& A1, Account const& A2, ApplyContext& ac) {
|
||||
auto sleToken = ac.view().peek(keylet::mptoken(mptID, A2.id()));
|
||||
if (!sleToken)
|
||||
return false;
|
||||
sleToken->setFieldVL(sfConfidentialBalanceSpending, Blob{0xBA, 0xDD});
|
||||
|
||||
// DO NOT update sfConfidentialBalanceVersion
|
||||
ac.view().update(sleToken);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tecINVARIANT_FAILED},
|
||||
precloseConfidential);
|
||||
|
||||
// Skipping Deleted MPTs (Issuance deleted)
|
||||
auto const precloseOrphan = [&mptID](Account const& A1, Account const& A2, Env& env) -> bool {
|
||||
MPTTester mpt(env, A1, {.holders = {A2}, .fund = false});
|
||||
mpt.create({.flags = tfMPTCanTransfer | tfMPTCanPrivacy});
|
||||
mptID = mpt.issuanceID();
|
||||
mpt.authorize({.account = A2});
|
||||
|
||||
// Generate privacy keys and convert 0 amount so Bob has the encrypted fields
|
||||
mpt.generateKeyPair(A1);
|
||||
mpt.set({.account = A1, .issuerPubKey = mpt.getPubKey(A1)});
|
||||
mpt.generateKeyPair(A2);
|
||||
mpt.convert({
|
||||
.account = A2,
|
||||
.amt = 0,
|
||||
.holderPubKey = mpt.getPubKey(A2),
|
||||
});
|
||||
|
||||
// Immediately destroy the issuance. A2's empty, encrypted token object lives on.
|
||||
mpt.destroy();
|
||||
return true;
|
||||
};
|
||||
|
||||
doInvariantCheck(
|
||||
{},
|
||||
[&mptID](Account const& A1, Account const& A2, ApplyContext& ac) {
|
||||
auto sleToken = ac.view().peek(keylet::mptoken(mptID, A2.id()));
|
||||
if (!sleToken)
|
||||
return false;
|
||||
// Safely able to erase the deleted token.
|
||||
ac.view().erase(sleToken);
|
||||
return true;
|
||||
},
|
||||
XRPAmount{},
|
||||
STTx{ttMPTOKEN_AUTHORIZE, [](STObject&) {}},
|
||||
{tesSUCCESS, tesSUCCESS},
|
||||
precloseOrphan);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -3732,6 +3915,7 @@ public:
|
||||
testValidPseudoAccounts();
|
||||
testValidLoanBroker();
|
||||
testVault();
|
||||
testConfidentialMPTTransfer();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -507,7 +507,8 @@ class MPToken_test : public beast::unit_test::suite
|
||||
// (2)
|
||||
mptAlice.set({.account = alice, .flags = 0x00000008, .err = temINVALID_FLAG});
|
||||
|
||||
if (!features[featureSingleAssetVault] && !features[featureDynamicMPT])
|
||||
if (!features[featureSingleAssetVault] && !features[featureDynamicMPT] &&
|
||||
!features[featureConfidentialTransfer])
|
||||
{
|
||||
// test invalid flags - nothing is being changed
|
||||
mptAlice.set({.account = alice, .flags = 0x00000000, .err = tecNO_PERMISSION});
|
||||
@@ -2550,6 +2551,7 @@ class MPToken_test : public beast::unit_test::suite
|
||||
tmfMPTSetCanTrade | tmfMPTClearCanTrade,
|
||||
tmfMPTSetCanTransfer | tmfMPTClearCanTransfer,
|
||||
tmfMPTSetCanClawback | tmfMPTClearCanClawback,
|
||||
tmfMPTSetPrivacy | tmfMPTClearPrivacy,
|
||||
tmfMPTSetCanLock | tmfMPTClearCanLock | tmfMPTClearCanTrade,
|
||||
tmfMPTSetCanTransfer | tmfMPTClearCanTransfer | tmfMPTSetCanEscrow | tmfMPTClearCanClawback};
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,8 +5,11 @@
|
||||
#include <test/jtx/ter.h>
|
||||
#include <test/jtx/txflags.h>
|
||||
|
||||
#include <xrpl/protocol/ConfidentialTransfer.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace xrpl {
|
||||
namespace test {
|
||||
namespace jtx {
|
||||
@@ -15,6 +18,22 @@ class MPTTester;
|
||||
|
||||
auto const MPTDEXFlags = tfMPTCanTrade | tfMPTCanTransfer;
|
||||
|
||||
/*Helper lambda to create a zero-initialized buffer.
|
||||
WHY THIS IS NEEDED: In C++, xrpl::Buffer(size) allocates uninitialized heap memory.
|
||||
Because CI runs unit tests sequentially in the same process, uninitialized memory
|
||||
often recycles "ghost data" (like valid SECP256k1 keys or Pedersen commitments)
|
||||
left over from previously executed tests.
|
||||
When testing malformed cryptography paths, passing uninitialized memory might
|
||||
accidentally supply a valid curve point, causing the ledger's preflight checks
|
||||
to falsely succeed and return tecBAD_PROOF instead of the expected temMALFORMED.
|
||||
Explicitly zeroing the buffer guarantees it fails structural validation. */
|
||||
static auto makeZeroBuffer = [](size_t size) {
|
||||
Buffer b(size);
|
||||
if (size > 0)
|
||||
std::memset(b.data(), 0, size);
|
||||
return b;
|
||||
};
|
||||
|
||||
// Check flags settings on MPT create
|
||||
class mptflags
|
||||
{
|
||||
@@ -93,6 +112,7 @@ struct MPTCreate
|
||||
struct MPTInit
|
||||
{
|
||||
Holders holders = {};
|
||||
std::optional<Account> auditor = std::nullopt;
|
||||
PrettyAmount const xrp = XRP(10'000);
|
||||
PrettyAmount const xrpHolders = XRP(10'000);
|
||||
bool fund = true;
|
||||
@@ -107,6 +127,7 @@ struct MPTInitDef
|
||||
Env& env;
|
||||
Account issuer;
|
||||
Holders holders = {};
|
||||
std::optional<Account> auditor = std::nullopt;
|
||||
std::uint16_t transferFee = 0;
|
||||
std::optional<std::uint64_t> pay = std::nullopt;
|
||||
std::uint32_t flags = MPTDEXFlags;
|
||||
@@ -151,18 +172,131 @@ struct MPTSet
|
||||
std::optional<std::string> metadata = std::nullopt;
|
||||
std::optional<Account> delegate = std::nullopt;
|
||||
std::optional<uint256> domainID = std::nullopt;
|
||||
std::optional<Buffer> issuerPubKey = std::nullopt;
|
||||
std::optional<Buffer> auditorPubKey = std::nullopt;
|
||||
std::optional<TER> err = std::nullopt;
|
||||
};
|
||||
|
||||
struct MPTConvert
|
||||
{
|
||||
std::optional<Account> account = std::nullopt;
|
||||
std::optional<MPTID> id = std::nullopt;
|
||||
std::optional<std::uint64_t> amt = std::nullopt;
|
||||
std::optional<std::string> proof = std::nullopt;
|
||||
std::optional<bool> fillAuditorEncryptedAmt = true;
|
||||
// indicates whether to autofill schnorr proof.
|
||||
// default : auto generate proof if holderPubKey is present.
|
||||
// true: force proof generation.
|
||||
// false: force proof omission.
|
||||
std::optional<bool> fillSchnorrProof = std::nullopt;
|
||||
std::optional<Buffer> holderPubKey = std::nullopt;
|
||||
std::optional<Buffer> holderEncryptedAmt = std::nullopt;
|
||||
std::optional<Buffer> issuerEncryptedAmt = std::nullopt;
|
||||
std::optional<Buffer> auditorEncryptedAmt = std::nullopt;
|
||||
|
||||
std::optional<Buffer> blindingFactor = std::nullopt;
|
||||
std::optional<std::uint32_t> ownerCount = std::nullopt;
|
||||
std::optional<std::uint32_t> holderCount = std::nullopt;
|
||||
std::optional<std::uint32_t> flags = std::nullopt;
|
||||
std::optional<TER> err = std::nullopt;
|
||||
};
|
||||
|
||||
struct MPTMergeInbox
|
||||
{
|
||||
std::optional<Account> account = std::nullopt;
|
||||
std::optional<MPTID> id = std::nullopt;
|
||||
std::optional<std::uint32_t> ownerCount = std::nullopt;
|
||||
std::optional<std::uint32_t> holderCount = std::nullopt;
|
||||
std::optional<std::uint32_t> flags = std::nullopt;
|
||||
std::optional<TER> err = std::nullopt;
|
||||
};
|
||||
|
||||
struct MPTConfidentialSend
|
||||
{
|
||||
std::optional<Account> account = std::nullopt;
|
||||
std::optional<Account> dest = std::nullopt;
|
||||
std::optional<MPTID> id = std::nullopt;
|
||||
// amt is to generate encrypted amounts for testing purposes
|
||||
std::optional<std::uint64_t> amt = std::nullopt;
|
||||
std::optional<std::string> proof = std::nullopt;
|
||||
std::optional<Buffer> senderEncryptedAmt = std::nullopt;
|
||||
std::optional<Buffer> destEncryptedAmt = std::nullopt;
|
||||
std::optional<Buffer> issuerEncryptedAmt = std::nullopt;
|
||||
std::optional<Buffer> auditorEncryptedAmt = std::nullopt;
|
||||
std::optional<std::vector<std::string>> credentials = std::nullopt;
|
||||
// not an txn param, only used for autofilling
|
||||
std::optional<Buffer> blindingFactor = std::nullopt;
|
||||
std::optional<Buffer> amountCommitment = std::nullopt;
|
||||
std::optional<Buffer> balanceCommitment = std::nullopt;
|
||||
std::optional<std::uint32_t> ownerCount = std::nullopt;
|
||||
std::optional<std::uint32_t> holderCount = std::nullopt;
|
||||
std::optional<std::uint32_t> flags = std::nullopt;
|
||||
std::optional<TER> err = std::nullopt;
|
||||
};
|
||||
|
||||
struct MPTConvertBack
|
||||
{
|
||||
std::optional<Account> account = std::nullopt;
|
||||
std::optional<MPTID> id = std::nullopt;
|
||||
std::optional<std::uint64_t> amt = std::nullopt;
|
||||
std::optional<Buffer> proof = std::nullopt;
|
||||
std::optional<Buffer> holderEncryptedAmt = std::nullopt;
|
||||
std::optional<Buffer> issuerEncryptedAmt = std::nullopt;
|
||||
std::optional<Buffer> auditorEncryptedAmt = std::nullopt;
|
||||
std::optional<bool> fillAuditorEncryptedAmt = true;
|
||||
// not an txn param, only used for autofilling
|
||||
std::optional<Buffer> blindingFactor = std::nullopt;
|
||||
std::optional<Buffer> pedersenCommitment = std::nullopt;
|
||||
std::optional<std::uint32_t> ownerCount = std::nullopt;
|
||||
std::optional<std::uint32_t> holderCount = std::nullopt;
|
||||
std::optional<std::uint32_t> flags = std::nullopt;
|
||||
std::optional<TER> err = std::nullopt;
|
||||
};
|
||||
|
||||
struct MPTConfidentialClawback
|
||||
{
|
||||
std::optional<Account> account = std::nullopt;
|
||||
std::optional<Account> holder = std::nullopt;
|
||||
std::optional<MPTID> id = std::nullopt;
|
||||
std::optional<std::uint64_t> amt = std::nullopt;
|
||||
std::optional<std::string> proof = std::nullopt;
|
||||
std::optional<std::uint32_t> ownerCount = std::nullopt;
|
||||
std::optional<std::uint32_t> holderCount = std::nullopt;
|
||||
std::optional<std::uint32_t> flags = std::nullopt;
|
||||
std::optional<TER> err = std::nullopt;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Stores the parameters that are exclusively used to generate a
|
||||
* pedersen linkage proof
|
||||
*/
|
||||
struct PedersenProofParams
|
||||
{
|
||||
Buffer const pedersenCommitment;
|
||||
uint64_t const amt; // either spending balance or value to be transferred
|
||||
Buffer const encryptedAmt;
|
||||
Buffer const blindingFactor;
|
||||
};
|
||||
|
||||
class MPTTester
|
||||
{
|
||||
Env& env_;
|
||||
Account const issuer_;
|
||||
std::unordered_map<std::string, Account> const holders_;
|
||||
std::optional<Account> const auditor_;
|
||||
std::optional<MPTID> id_;
|
||||
bool close_;
|
||||
std::unordered_map<AccountID, Buffer> pubKeys;
|
||||
std::unordered_map<AccountID, Buffer> privKeys;
|
||||
|
||||
public:
|
||||
enum EncryptedBalanceType {
|
||||
ISSUER_ENCRYPTED_BALANCE,
|
||||
HOLDER_ENCRYPTED_INBOX,
|
||||
HOLDER_ENCRYPTED_SPENDING,
|
||||
AUDITOR_ENCRYPTED_BALANCE,
|
||||
};
|
||||
|
||||
MPTTester(Env& env, Account const& issuer, MPTInit const& constr = {});
|
||||
MPTTester(MPTInitDef const& constr);
|
||||
MPTTester(
|
||||
@@ -200,6 +334,21 @@ public:
|
||||
static Json::Value
|
||||
setJV(MPTSet const& set = {});
|
||||
|
||||
void
|
||||
convert(MPTConvert const& arg = MPTConvert{});
|
||||
|
||||
void
|
||||
mergeInbox(MPTMergeInbox const& arg = MPTMergeInbox{});
|
||||
|
||||
void
|
||||
send(MPTConfidentialSend const& arg = MPTConfidentialSend{});
|
||||
|
||||
void
|
||||
convertBack(MPTConvertBack const& arg = MPTConvertBack{});
|
||||
|
||||
void
|
||||
confidentialClaw(MPTConfidentialClawback const& arg = MPTConfidentialClawback{});
|
||||
|
||||
[[nodiscard]] bool
|
||||
checkDomainID(std::optional<uint256> expected) const;
|
||||
|
||||
@@ -209,6 +358,9 @@ public:
|
||||
[[nodiscard]] bool
|
||||
checkMPTokenOutstandingAmount(std::int64_t expectedAmount) const;
|
||||
|
||||
[[nodiscard]] bool
|
||||
checkIssuanceConfidentialBalance(std::int64_t expectedAmount) const;
|
||||
|
||||
[[nodiscard]] bool
|
||||
checkFlags(uint32_t const expectedFlags, std::optional<Account> const& holder = std::nullopt) const;
|
||||
|
||||
@@ -229,6 +381,7 @@ public:
|
||||
{
|
||||
return issuer_;
|
||||
}
|
||||
|
||||
Account const&
|
||||
holder(std::string const& h) const;
|
||||
|
||||
@@ -256,6 +409,12 @@ public:
|
||||
std::int64_t
|
||||
getBalance(Account const& account) const;
|
||||
|
||||
std::int64_t
|
||||
getIssuanceConfidentialBalance() const;
|
||||
|
||||
std::optional<Buffer>
|
||||
getEncryptedBalance(Account const& account, EncryptedBalanceType option = HOLDER_ENCRYPTED_INBOX) const;
|
||||
|
||||
MPT
|
||||
operator[](std::string const& name) const;
|
||||
|
||||
@@ -264,6 +423,81 @@ public:
|
||||
|
||||
operator Asset() const;
|
||||
|
||||
bool
|
||||
printMPT(Account const& holder_) const;
|
||||
|
||||
void
|
||||
generateKeyPair(Account const& account);
|
||||
|
||||
std::optional<Buffer>
|
||||
getPubKey(Account const& account) const;
|
||||
|
||||
std::optional<Buffer>
|
||||
getPrivKey(Account const& account) const;
|
||||
|
||||
Buffer
|
||||
encryptAmount(Account const& account, uint64_t const amt, Buffer const& blindingFactor) const;
|
||||
|
||||
std::optional<uint64_t>
|
||||
decryptAmount(Account const& account, Buffer const& amt) const;
|
||||
|
||||
std::optional<uint64_t>
|
||||
getDecryptedBalance(Account const& account, EncryptedBalanceType balanceType) const;
|
||||
|
||||
std::int64_t
|
||||
getIssuanceOutstandingBalance() const;
|
||||
|
||||
std::optional<Buffer>
|
||||
getClawbackProof(Account const& holder, std::uint64_t amount, Buffer const& privateKey, uint256 const& txHash)
|
||||
const;
|
||||
|
||||
std::optional<Buffer>
|
||||
getSchnorrProof(Account const& account, uint256 const& ctxHash) const;
|
||||
|
||||
std::optional<Buffer>
|
||||
getConfidentialSendProof(
|
||||
Account const& sender,
|
||||
std::uint64_t const amount,
|
||||
std::vector<ConfidentialRecipient> const& recipients,
|
||||
Slice const& blindingFactor,
|
||||
std::size_t const nRecipients,
|
||||
uint256 const& contextHash,
|
||||
PedersenProofParams const& amountParams,
|
||||
PedersenProofParams const& balanceParams) const;
|
||||
|
||||
Buffer
|
||||
getConvertBackProof(
|
||||
Account const& holder,
|
||||
std::uint64_t const amount,
|
||||
uint256 const& contextHash,
|
||||
PedersenProofParams const& pcParams) const;
|
||||
|
||||
std::uint32_t
|
||||
getMPTokenVersion(Account const account) const;
|
||||
|
||||
Buffer
|
||||
getAmountLinkageProof(
|
||||
Buffer const& pubKey,
|
||||
Buffer const& blindingFactor,
|
||||
uint256 const& contextHash,
|
||||
PedersenProofParams const& params) const;
|
||||
|
||||
Buffer
|
||||
getBalanceLinkageProof(
|
||||
Account const& account,
|
||||
uint256 const& contextHash,
|
||||
Buffer const& pubKey,
|
||||
PedersenProofParams const& params) const;
|
||||
|
||||
Buffer
|
||||
getBulletproof(
|
||||
std::vector<std::uint64_t> const& values,
|
||||
std::vector<Buffer> const& blindingFactors,
|
||||
uint256 const& contextHash) const;
|
||||
|
||||
Buffer
|
||||
getPedersenCommitment(std::uint64_t const amount, Buffer const& pedersenBlindingFactor);
|
||||
|
||||
private:
|
||||
using SLEP = SLE::const_pointer;
|
||||
bool
|
||||
@@ -293,6 +527,16 @@ private:
|
||||
|
||||
std::uint32_t
|
||||
getFlags(std::optional<Account> const& holder) const;
|
||||
|
||||
template <typename T>
|
||||
void
|
||||
fillConversionCiphertexts(
|
||||
T const& arg,
|
||||
Json::Value& jv,
|
||||
Buffer& holderCiphertext,
|
||||
Buffer& issuerCiphertext,
|
||||
std::optional<Buffer>& auditorCiphertext,
|
||||
Buffer& blindingFactor) const;
|
||||
};
|
||||
|
||||
} // namespace jtx
|
||||
|
||||
@@ -51,6 +51,11 @@ public:
|
||||
ttLOAN_DELETE,
|
||||
ttLOAN_MANAGE,
|
||||
ttLOAN_PAY,
|
||||
ttCONFIDENTIAL_MPT_SEND,
|
||||
ttCONFIDENTIAL_MPT_CONVERT,
|
||||
ttCONFIDENTIAL_MPT_CONVERT_BACK,
|
||||
ttCONFIDENTIAL_MPT_MERGE_INBOX,
|
||||
ttCONFIDENTIAL_MPT_CLAWBACK,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
160
src/xrpld/app/tx/detail/ConfidentialMPTClawback.cpp
Normal file
160
src/xrpld/app/tx/detail/ConfidentialMPTClawback.cpp
Normal file
@@ -0,0 +1,160 @@
|
||||
#include <xrpld/app/tx/detail/ConfidentialMPTClawback.h>
|
||||
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/protocol/ConfidentialTransfer.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
NotTEC
|
||||
ConfidentialMPTClawback::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (!ctx.rules.enabled(featureConfidentialTransfer))
|
||||
return temDISABLED;
|
||||
|
||||
auto const account = ctx.tx[sfAccount];
|
||||
|
||||
// Only issuer can clawback
|
||||
if (account != MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer())
|
||||
return temMALFORMED;
|
||||
|
||||
// Cannot clawback from self
|
||||
if (account == ctx.tx[sfHolder])
|
||||
return temMALFORMED;
|
||||
|
||||
// Check invalid claw amount
|
||||
auto const clawAmount = ctx.tx[sfMPTAmount];
|
||||
if (clawAmount == 0 || clawAmount > maxMPTokenAmount)
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
// Verify proof length
|
||||
if (ctx.tx[sfZKProof].length() != ecEqualityProofLength)
|
||||
return temMALFORMED;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialMPTClawback::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
// Check if sender account exists
|
||||
auto const account = ctx.tx[sfAccount];
|
||||
if (!ctx.view.exists(keylet::account(account)))
|
||||
return terNO_ACCOUNT;
|
||||
|
||||
// Check if holder account exists
|
||||
auto const holder = ctx.tx[sfHolder];
|
||||
if (!ctx.view.exists(keylet::account(holder)))
|
||||
return tecNO_TARGET;
|
||||
|
||||
// Check if MPT issuance exists
|
||||
auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
|
||||
auto const sleIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID));
|
||||
if (!sleIssuance)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
// Sanity check: issuer must be the same as account
|
||||
if (sleIssuance->getAccountID(sfIssuer) != account)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// Check if issuance has issuer ElGamal public key
|
||||
if (!sleIssuance->isFieldPresent(sfIssuerElGamalPublicKey))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// Check if clawback is allowed
|
||||
if (!sleIssuance->isFlag(lsfMPTCanClawback))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// Check holder's MPToken
|
||||
auto const sleHolderMPToken = ctx.view.read(keylet::mptoken(mptIssuanceID, holder));
|
||||
if (!sleHolderMPToken)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
// Check if holder has confidential balances to claw back
|
||||
if (!sleHolderMPToken->isFieldPresent(sfIssuerEncryptedBalance))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// Sanity check: claw amount can not exceed confidential outstanding amount
|
||||
auto const amount = ctx.tx[sfMPTAmount];
|
||||
if (amount > (*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0))
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
auto const contextHash = getClawbackContextHash(account, ctx.tx[sfSequence], mptIssuanceID, amount, holder);
|
||||
|
||||
// Verify the revealed confidential amount by the issuer matches the exact
|
||||
// confidential balance of the holder.
|
||||
return verifyClawbackEqualityProof(
|
||||
amount,
|
||||
ctx.tx[sfZKProof],
|
||||
(*sleIssuance)[sfIssuerElGamalPublicKey],
|
||||
(*sleHolderMPToken)[sfIssuerEncryptedBalance],
|
||||
contextHash);
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialMPTClawback::doApply()
|
||||
{
|
||||
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
|
||||
auto const holder = ctx_.tx[sfHolder];
|
||||
|
||||
auto sleIssuance = view().peek(keylet::mptIssuance(mptIssuanceID));
|
||||
auto sleHolderMPToken = view().peek(keylet::mptoken(mptIssuanceID, holder));
|
||||
|
||||
if (!sleIssuance || !sleHolderMPToken)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const clawAmount = ctx_.tx[sfMPTAmount];
|
||||
|
||||
Slice const holderPubKey = (*sleHolderMPToken)[sfHolderElGamalPublicKey];
|
||||
Slice const issuerPubKey = (*sleIssuance)[sfIssuerElGamalPublicKey];
|
||||
|
||||
// After clawback, the balance should be encrypted zero.
|
||||
auto const encZeroForHolder = encryptCanonicalZeroAmount(holderPubKey, holder, mptIssuanceID);
|
||||
if (!encZeroForHolder)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const encZeroForIssuer = encryptCanonicalZeroAmount(issuerPubKey, holder, mptIssuanceID);
|
||||
if (!encZeroForIssuer)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// Set holder's confidential balances to encrypted zero
|
||||
(*sleHolderMPToken)[sfConfidentialBalanceInbox] = *encZeroForHolder;
|
||||
(*sleHolderMPToken)[sfConfidentialBalanceSpending] = *encZeroForHolder;
|
||||
(*sleHolderMPToken)[sfIssuerEncryptedBalance] = *encZeroForIssuer;
|
||||
(*sleHolderMPToken)[sfConfidentialBalanceVersion] = 0;
|
||||
|
||||
if (sleHolderMPToken->isFieldPresent(sfAuditorEncryptedBalance))
|
||||
{
|
||||
// Sanity check: the issuance must have an auditor public key if
|
||||
// auditing is enabled.
|
||||
if (!sleIssuance->isFieldPresent(sfAuditorElGamalPublicKey))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
Slice const auditorPubKey = (*sleIssuance)[sfAuditorElGamalPublicKey];
|
||||
|
||||
auto const encZeroForAuditor = encryptCanonicalZeroAmount(auditorPubKey, holder, mptIssuanceID);
|
||||
|
||||
if (!encZeroForAuditor)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
(*sleHolderMPToken)[sfAuditorEncryptedBalance] = *encZeroForAuditor;
|
||||
}
|
||||
|
||||
// Decrease Global Confidential Outstanding Amount
|
||||
auto const oldCOA = (*sleIssuance)[sfConfidentialOutstandingAmount];
|
||||
(*sleIssuance)[sfConfidentialOutstandingAmount] = oldCOA - clawAmount;
|
||||
|
||||
// Decrease Global Total Outstanding Amount
|
||||
auto const oldOA = (*sleIssuance)[sfOutstandingAmount];
|
||||
(*sleIssuance)[sfOutstandingAmount] = oldOA - clawAmount;
|
||||
|
||||
view().update(sleHolderMPToken);
|
||||
view().update(sleIssuance);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
} // namespace xrpl
|
||||
42
src/xrpld/app/tx/detail/ConfidentialMPTClawback.h
Normal file
42
src/xrpld/app/tx/detail/ConfidentialMPTClawback.h
Normal file
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpld/app/tx/detail/Transactor.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* @brief Allows an MPT issuer to clawback confidential balances from a holder.
|
||||
*
|
||||
* This transaction enables the issuer of an MPToken Issuance (with clawback
|
||||
* enabled) to reclaim confidential tokens from a holder's account. Unlike
|
||||
* regular clawback, the issuer cannot see the holder's balance directly.
|
||||
* Instead, the issuer must provide a zero-knowledge proof that demonstrates
|
||||
* they know the exact encrypted balance amount.
|
||||
*
|
||||
* @par Cryptographic Operations:
|
||||
* - **Equality Proof Verification**: Verifies that the issuer's revealed
|
||||
* amount matches the holder's encrypted balance using the issuer's
|
||||
* ElGamal private key.
|
||||
*
|
||||
* @see ConfidentialMPTSend, ConfidentialMPTConvert
|
||||
*/
|
||||
class ConfidentialMPTClawback : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
|
||||
|
||||
explicit ConfidentialMPTClawback(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
254
src/xrpld/app/tx/detail/ConfidentialMPTConvert.cpp
Normal file
254
src/xrpld/app/tx/detail/ConfidentialMPTConvert.cpp
Normal file
@@ -0,0 +1,254 @@
|
||||
#include <xrpld/app/tx/detail/ConfidentialMPTConvert.h>
|
||||
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/protocol/ConfidentialTransfer.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
NotTEC
|
||||
ConfidentialMPTConvert::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (!ctx.rules.enabled(featureConfidentialTransfer))
|
||||
return temDISABLED;
|
||||
|
||||
// issuer cannot convert
|
||||
if (MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer() == ctx.tx[sfAccount])
|
||||
return temMALFORMED;
|
||||
|
||||
if (ctx.tx[sfMPTAmount] > maxMPTokenAmount)
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
if (ctx.tx[sfBlindingFactor].size() != ecBlindingFactorLength)
|
||||
return temMALFORMED;
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfHolderElGamalPublicKey))
|
||||
{
|
||||
if (!isValidCompressedECPoint(ctx.tx[sfHolderElGamalPublicKey]))
|
||||
return temMALFORMED;
|
||||
|
||||
// proof of knowledge of the secret key corresponding to the provided
|
||||
// public key is needed when holder ec public key is being set.
|
||||
if (!ctx.tx.isFieldPresent(sfZKProof))
|
||||
return temMALFORMED;
|
||||
|
||||
// verify schnorr proof length when registerring holder ec public key
|
||||
if (ctx.tx[sfZKProof].size() != ecSchnorrProofLength)
|
||||
return temMALFORMED;
|
||||
}
|
||||
else
|
||||
{
|
||||
// zkp should not be present if public key was already set
|
||||
if (ctx.tx.isFieldPresent(sfZKProof))
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
// check encrypted amount format after the above basic checks
|
||||
// this check is more expensive so put it at the end
|
||||
if (auto const res = checkEncryptedAmountFormat(ctx.tx); !isTesSuccess(res))
|
||||
return res;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialMPTConvert::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
auto const account = ctx.tx[sfAccount];
|
||||
auto const issuanceID = ctx.tx[sfMPTokenIssuanceID];
|
||||
auto const amount = ctx.tx[sfMPTAmount];
|
||||
|
||||
// ensure that issuance exists
|
||||
auto const sleIssuance = ctx.view.read(keylet::mptIssuance(issuanceID));
|
||||
if (!sleIssuance)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
if (!sleIssuance->isFlag(lsfMPTCanPrivacy))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// already checked in preflight, but should also check that issuer on the
|
||||
// issuance isn't the account either
|
||||
if (sleIssuance->getAccountID(sfIssuer) == account)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// issuer has not uploaded their pub key yet
|
||||
if (!sleIssuance->isFieldPresent(sfIssuerElGamalPublicKey))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
|
||||
bool const requiresAuditor = sleIssuance->isFieldPresent(sfAuditorElGamalPublicKey);
|
||||
|
||||
// tx must include auditor ciphertext if the issuance has enabled
|
||||
// auditing, and must not include it if auditing is not enabled
|
||||
if (requiresAuditor != hasAuditor)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
auto const sleMptoken = ctx.view.read(keylet::mptoken(issuanceID, account));
|
||||
if (!sleMptoken)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
auto const mptIssue = MPTIssue{issuanceID};
|
||||
STAmount const mptAmount = STAmount(MPTAmount{static_cast<MPTAmount::value_type>(amount)}, mptIssue);
|
||||
if (accountHolds(
|
||||
ctx.view,
|
||||
account,
|
||||
mptIssue,
|
||||
FreezeHandling::fhZERO_IF_FROZEN,
|
||||
AuthHandling::ahZERO_IF_UNAUTHORIZED,
|
||||
ctx.j) < mptAmount)
|
||||
{
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
}
|
||||
|
||||
auto const hasHolderKeyOnLedger = sleMptoken->isFieldPresent(sfHolderElGamalPublicKey);
|
||||
auto const hasHolderKeyInTx = ctx.tx.isFieldPresent(sfHolderElGamalPublicKey);
|
||||
|
||||
// must have pk to convert
|
||||
if (!hasHolderKeyOnLedger && !hasHolderKeyInTx)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// can't update if there's already a pk
|
||||
if (hasHolderKeyOnLedger && hasHolderKeyInTx)
|
||||
return tecDUPLICATE;
|
||||
|
||||
Slice holderPubKey;
|
||||
if (hasHolderKeyInTx)
|
||||
{
|
||||
holderPubKey = ctx.tx[sfHolderElGamalPublicKey];
|
||||
|
||||
auto const contextHash = getConvertContextHash(account, ctx.tx[sfSequence], issuanceID, amount);
|
||||
|
||||
// when register new pk, verify through schnorr proof
|
||||
if (!isTesSuccess(verifySchnorrProof(holderPubKey, ctx.tx[sfZKProof], contextHash)))
|
||||
{
|
||||
return tecBAD_PROOF;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
holderPubKey = (*sleMptoken)[sfHolderElGamalPublicKey];
|
||||
}
|
||||
|
||||
std::optional<ConfidentialRecipient> auditor;
|
||||
if (hasAuditor)
|
||||
{
|
||||
auditor.emplace(
|
||||
ConfidentialRecipient{(*sleIssuance)[sfAuditorElGamalPublicKey], ctx.tx[sfAuditorEncryptedAmount]});
|
||||
}
|
||||
|
||||
return verifyRevealedAmount(
|
||||
amount,
|
||||
ctx.tx[sfBlindingFactor],
|
||||
{holderPubKey, ctx.tx[sfHolderEncryptedAmount]},
|
||||
{(*sleIssuance)[sfIssuerElGamalPublicKey], ctx.tx[sfIssuerEncryptedAmount]},
|
||||
auditor);
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialMPTConvert::doApply()
|
||||
{
|
||||
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
|
||||
|
||||
auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, account_));
|
||||
if (!sleMptoken)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto sleIssuance = view().peek(keylet::mptIssuance(mptIssuanceID));
|
||||
if (!sleIssuance)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const amtToConvert = ctx_.tx[sfMPTAmount];
|
||||
auto const amt = (*sleMptoken)[~sfMPTAmount].value_or(0);
|
||||
|
||||
if (ctx_.tx.isFieldPresent(sfHolderElGamalPublicKey))
|
||||
(*sleMptoken)[sfHolderElGamalPublicKey] = ctx_.tx[sfHolderElGamalPublicKey];
|
||||
|
||||
// Converting decreases regular balance and increases confidential outstanding.
|
||||
// The confidential outstanding tracks total tokens in confidential form globally.
|
||||
(*sleMptoken)[sfMPTAmount] = amt - amtToConvert;
|
||||
(*sleIssuance)[sfConfidentialOutstandingAmount] =
|
||||
(*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0) + amtToConvert;
|
||||
|
||||
Slice const holderEc = ctx_.tx[sfHolderEncryptedAmount];
|
||||
Slice const issuerEc = ctx_.tx[sfIssuerEncryptedAmount];
|
||||
|
||||
auto const auditorEc = ctx_.tx[~sfAuditorEncryptedAmount];
|
||||
|
||||
// Two cases for Convert:
|
||||
// 1. Holder already has confidential balances -> homomorphically add to inbox
|
||||
// 2. First-time convert -> initialize all confidential balance fields
|
||||
if (sleMptoken->isFieldPresent(sfIssuerEncryptedBalance) &&
|
||||
sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) &&
|
||||
sleMptoken->isFieldPresent(sfConfidentialBalanceSpending))
|
||||
{
|
||||
// Case 1: Add to existing inbox balance (holder will merge later)
|
||||
{
|
||||
Buffer sum(ecGamalEncryptedTotalLength);
|
||||
if (TER const ter = homomorphicAdd(holderEc, (*sleMptoken)[sfConfidentialBalanceInbox], sum);
|
||||
!isTesSuccess(ter))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
(*sleMptoken)[sfConfidentialBalanceInbox] = sum;
|
||||
}
|
||||
|
||||
// homomorphically add issuer's encrypted balance
|
||||
{
|
||||
Buffer sum(ecGamalEncryptedTotalLength);
|
||||
if (TER const ter = homomorphicAdd(issuerEc, (*sleMptoken)[sfIssuerEncryptedBalance], sum);
|
||||
!isTesSuccess(ter))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
(*sleMptoken)[sfIssuerEncryptedBalance] = sum;
|
||||
}
|
||||
|
||||
// homomorphically add auditor's encrypted balance
|
||||
if (auditorEc)
|
||||
{
|
||||
Buffer sum(ecGamalEncryptedTotalLength);
|
||||
if (TER const ter = homomorphicAdd(*auditorEc, (*sleMptoken)[sfAuditorEncryptedBalance], sum);
|
||||
!isTesSuccess(ter))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
(*sleMptoken)[sfAuditorEncryptedBalance] = sum;
|
||||
}
|
||||
}
|
||||
else if (
|
||||
!sleMptoken->isFieldPresent(sfIssuerEncryptedBalance) &&
|
||||
!sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) &&
|
||||
!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending))
|
||||
{
|
||||
// Case 2: First-time convert - initialize all confidential fields
|
||||
(*sleMptoken)[sfConfidentialBalanceInbox] = holderEc;
|
||||
(*sleMptoken)[sfIssuerEncryptedBalance] = issuerEc;
|
||||
(*sleMptoken)[sfConfidentialBalanceVersion] = 0;
|
||||
|
||||
if (auditorEc)
|
||||
(*sleMptoken)[sfAuditorEncryptedBalance] = *auditorEc;
|
||||
|
||||
// Spending balance starts at zero. Must use canonical zero encryption
|
||||
// (deterministic ciphertext) so the ledger state is reproducible.
|
||||
auto const zeroBalance =
|
||||
encryptCanonicalZeroAmount((*sleMptoken)[sfHolderElGamalPublicKey], account_, mptIssuanceID);
|
||||
|
||||
if (!zeroBalance)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
(*sleMptoken)[sfConfidentialBalanceSpending] = *zeroBalance;
|
||||
}
|
||||
else
|
||||
{
|
||||
// both sfIssuerEncryptedBalance and sfConfidentialBalanceInbox should
|
||||
// exist together
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
view().update(sleIssuance);
|
||||
view().update(sleMptoken);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
44
src/xrpld/app/tx/detail/ConfidentialMPTConvert.h
Normal file
44
src/xrpld/app/tx/detail/ConfidentialMPTConvert.h
Normal file
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpld/app/tx/detail/Transactor.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* @brief Converts public (plaintext) MPT balance to confidential (encrypted)
|
||||
* balance.
|
||||
*
|
||||
* This transaction allows a token holder to convert their publicly visible
|
||||
* MPToken balance into an encrypted confidential balance. Once converted,
|
||||
* the balance can only be spent using ConfidentialMPTSend transactions and
|
||||
* remains hidden from public view on the ledger.
|
||||
*
|
||||
* @par Cryptographic Operations:
|
||||
* - **Schnorr Proof Verification**: When registering a new ElGamal public key,
|
||||
* verifies proof of knowledge of the corresponding private key.
|
||||
* - **Revealed Amount Verification**: Verifies that the provided encrypted
|
||||
* amounts (for holder, issuer, and optionally auditor) all encrypt the
|
||||
* same plaintext amount using the provided blinding factor.
|
||||
*
|
||||
* @see ConfidentialMPTConvertBack, ConfidentialMPTSend
|
||||
*/
|
||||
class ConfidentialMPTConvert : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
|
||||
|
||||
explicit ConfidentialMPTConvert(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
288
src/xrpld/app/tx/detail/ConfidentialMPTConvertBack.cpp
Normal file
288
src/xrpld/app/tx/detail/ConfidentialMPTConvertBack.cpp
Normal file
@@ -0,0 +1,288 @@
|
||||
#include <xrpld/app/tx/detail/ConfidentialMPTConvertBack.h>
|
||||
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/protocol/ConfidentialTransfer.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
NotTEC
|
||||
ConfidentialMPTConvertBack::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (!ctx.rules.enabled(featureConfidentialTransfer))
|
||||
return temDISABLED;
|
||||
|
||||
// issuer cannot convert back
|
||||
if (MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer() == ctx.tx[sfAccount])
|
||||
return temMALFORMED;
|
||||
|
||||
if (ctx.tx[sfMPTAmount] == 0 || ctx.tx[sfMPTAmount] > maxMPTokenAmount)
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
if (ctx.tx[sfBlindingFactor].size() != ecBlindingFactorLength)
|
||||
return temMALFORMED;
|
||||
|
||||
if (!isValidCompressedECPoint(ctx.tx[sfBalanceCommitment]))
|
||||
return temMALFORMED;
|
||||
|
||||
// check encrypted amount format after the above basic checks
|
||||
// this check is more expensive so put it at the end
|
||||
if (auto const res = checkEncryptedAmountFormat(ctx.tx); !isTesSuccess(res))
|
||||
return res;
|
||||
|
||||
// ConvertBack proof = pedersen linkage proof + single bulletproof
|
||||
if (ctx.tx[sfZKProof].size() != ecPedersenProofLength + ecSingleBulletproofLength)
|
||||
return temMALFORMED;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the cryptographic proofs for a ConvertBack transaction.
|
||||
*
|
||||
* This function verifies three proofs:
|
||||
* 1. Revealed amount proof: verifies the encrypted amounts (holder, issuer,
|
||||
* auditor) all encrypt the same revealed amount using the blinding factor.
|
||||
* 2. Pedersen linkage proof: verifies the balance commitment is derived from
|
||||
* the holder's encrypted spending balance.
|
||||
* 3. Bulletproof (range proof): verifies the remaining balance (balance - amount)
|
||||
* is non-negative, preventing overdrafts.
|
||||
*
|
||||
* All proofs are verified before returning any error to prevent timing attacks.
|
||||
*/
|
||||
static TER
|
||||
verifyProofs(STTx const& tx, std::shared_ptr<SLE const> const& issuance, std::shared_ptr<SLE const> const& mptoken)
|
||||
{
|
||||
if (!mptoken->isFieldPresent(sfHolderElGamalPublicKey))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const mptIssuanceID = tx[sfMPTokenIssuanceID];
|
||||
auto const account = tx[sfAccount];
|
||||
auto const amount = tx[sfMPTAmount];
|
||||
auto const blindingFactor = tx[sfBlindingFactor];
|
||||
auto const holderPubKey = (*mptoken)[sfHolderElGamalPublicKey];
|
||||
|
||||
auto const contextHash = getConvertBackContextHash(
|
||||
account, tx[sfSequence], mptIssuanceID, amount, (*mptoken)[~sfConfidentialBalanceVersion].value_or(0));
|
||||
|
||||
// Prepare Auditor Info
|
||||
std::optional<ConfidentialRecipient> auditor;
|
||||
bool const hasAuditor = issuance->isFieldPresent(sfAuditorElGamalPublicKey);
|
||||
if (hasAuditor)
|
||||
{
|
||||
auditor.emplace(ConfidentialRecipient{(*issuance)[sfAuditorElGamalPublicKey], tx[sfAuditorEncryptedAmount]});
|
||||
}
|
||||
|
||||
// Run all verifications before returning any error to prevent timing attacks
|
||||
// that could reveal which proof failed.
|
||||
bool valid = true;
|
||||
|
||||
// verify revealed amount
|
||||
if (auto const ter = verifyRevealedAmount(
|
||||
amount,
|
||||
blindingFactor,
|
||||
{holderPubKey, tx[sfHolderEncryptedAmount]},
|
||||
{(*issuance)[sfIssuerElGamalPublicKey], tx[sfIssuerEncryptedAmount]},
|
||||
auditor);
|
||||
!isTesSuccess(ter))
|
||||
{
|
||||
valid = false;
|
||||
}
|
||||
|
||||
// Parse proof components using offset
|
||||
auto const proof = tx[sfZKProof];
|
||||
size_t remainingLength = proof.size();
|
||||
size_t currentOffset = 0;
|
||||
|
||||
// Extract Pedersen linkage proof
|
||||
if (remainingLength < ecPedersenProofLength)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const pedersenProof = proof.substr(currentOffset, ecPedersenProofLength);
|
||||
currentOffset += ecPedersenProofLength;
|
||||
remainingLength -= ecPedersenProofLength;
|
||||
|
||||
// Extract bulletproof
|
||||
if (remainingLength < ecSingleBulletproofLength)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const bulletproof = proof.substr(currentOffset, ecSingleBulletproofLength);
|
||||
currentOffset += ecSingleBulletproofLength;
|
||||
remainingLength -= ecSingleBulletproofLength;
|
||||
|
||||
if (remainingLength != 0)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// verify el gamal pedersen linkage
|
||||
if (auto const ter = verifyBalancePcmLinkage(
|
||||
pedersenProof,
|
||||
(*mptoken)[sfConfidentialBalanceSpending],
|
||||
holderPubKey,
|
||||
tx[sfBalanceCommitment],
|
||||
contextHash);
|
||||
!isTesSuccess(ter))
|
||||
{
|
||||
valid = false;
|
||||
}
|
||||
|
||||
// verify bullet proof
|
||||
{
|
||||
// Compute PC_rem = PC_balance - mG (the commitment to the remaining balance)
|
||||
Buffer pcRem;
|
||||
if (auto const ter = computeConvertBackRemainder(tx[sfBalanceCommitment], amount, pcRem); !isTesSuccess(ter))
|
||||
{
|
||||
valid = false;
|
||||
}
|
||||
|
||||
// The bulletproof verifies that the remaining balance is non-negative
|
||||
std::vector<Slice> commitments{Slice(pcRem.data(), pcRem.size())};
|
||||
|
||||
if (auto const ter = verifyAggregatedBulletproof(bulletproof, commitments, contextHash); !isTesSuccess(ter))
|
||||
{
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!valid)
|
||||
return tecBAD_PROOF;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialMPTConvertBack::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
|
||||
auto const account = ctx.tx[sfAccount];
|
||||
auto const amount = ctx.tx[sfMPTAmount];
|
||||
|
||||
// ensure that issuance exists
|
||||
auto const sleIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID));
|
||||
if (!sleIssuance)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
if (!sleIssuance->isFlag(lsfMPTCanPrivacy))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
|
||||
bool const requiresAuditor = sleIssuance->isFieldPresent(sfAuditorElGamalPublicKey);
|
||||
|
||||
// tx must include auditor ciphertext if the issuance has enabled
|
||||
// auditing
|
||||
if (requiresAuditor && !hasAuditor)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// if auditing is not supported then user should not upload auditor
|
||||
// ciphertext
|
||||
if (!requiresAuditor && hasAuditor)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// already checked in preflight, but should also check that issuer on
|
||||
// the issuance isn't the account either
|
||||
if (sleIssuance->getAccountID(sfIssuer) == account)
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const sleMptoken = ctx.view.read(keylet::mptoken(mptIssuanceID, account));
|
||||
if (!sleMptoken)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
if (!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) ||
|
||||
!sleMptoken->isFieldPresent(sfHolderElGamalPublicKey))
|
||||
{
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
// if the total circulating confidential balance is smaller than what the
|
||||
// holder is trying to convert back, we know for sure this txn should
|
||||
// fail
|
||||
if ((*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0) < amount)
|
||||
{
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
}
|
||||
|
||||
// Check lock
|
||||
MPTIssue const mptIssue(mptIssuanceID);
|
||||
if (auto const ter = checkFrozen(ctx.view, account, mptIssue); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// Check auth
|
||||
if (auto const ter = requireAuth(ctx.view, mptIssue, account); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
if (TER const res = verifyProofs(ctx.tx, sleIssuance, sleMptoken); !isTesSuccess(res))
|
||||
return res;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialMPTConvertBack::doApply()
|
||||
{
|
||||
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
|
||||
|
||||
auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, account_));
|
||||
if (!sleMptoken)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto sleIssuance = view().peek(keylet::mptIssuance(mptIssuanceID));
|
||||
if (!sleIssuance)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const amtToConvertBack = ctx_.tx[sfMPTAmount];
|
||||
auto const amt = (*sleMptoken)[~sfMPTAmount].value_or(0);
|
||||
|
||||
// Converting back increases regular balance and decreases confidential
|
||||
// outstanding. This is the inverse of Convert.
|
||||
(*sleMptoken)[sfMPTAmount] = amt + amtToConvertBack;
|
||||
(*sleIssuance)[sfConfidentialOutstandingAmount] =
|
||||
(*sleIssuance)[sfConfidentialOutstandingAmount] - amtToConvertBack;
|
||||
|
||||
std::optional<Slice> const auditorEc = ctx_.tx[~sfAuditorEncryptedAmount];
|
||||
|
||||
// homomorphically subtract holder's encrypted balance
|
||||
{
|
||||
Buffer res(ecGamalEncryptedTotalLength);
|
||||
if (TER const ter = homomorphicSubtract(
|
||||
(*sleMptoken)[sfConfidentialBalanceSpending], ctx_.tx[sfHolderEncryptedAmount], res);
|
||||
!isTesSuccess(ter))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
(*sleMptoken)[sfConfidentialBalanceSpending] = res;
|
||||
}
|
||||
|
||||
// homomorphically subtract issuer's encrypted balance
|
||||
{
|
||||
Buffer res(ecGamalEncryptedTotalLength);
|
||||
if (TER const ter =
|
||||
homomorphicSubtract((*sleMptoken)[sfIssuerEncryptedBalance], ctx_.tx[sfIssuerEncryptedAmount], res);
|
||||
!isTesSuccess(ter))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
(*sleMptoken)[sfIssuerEncryptedBalance] = res;
|
||||
}
|
||||
|
||||
if (auditorEc)
|
||||
{
|
||||
Buffer res(ecGamalEncryptedTotalLength);
|
||||
if (TER const ter =
|
||||
homomorphicSubtract((*sleMptoken)[sfAuditorEncryptedBalance], ctx_.tx[sfAuditorEncryptedAmount], res);
|
||||
!isTesSuccess(ter))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
(*sleMptoken)[sfAuditorEncryptedBalance] = res;
|
||||
}
|
||||
|
||||
incrementConfidentialVersion(*sleMptoken);
|
||||
|
||||
view().update(sleIssuance);
|
||||
view().update(sleMptoken);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
45
src/xrpld/app/tx/detail/ConfidentialMPTConvertBack.h
Normal file
45
src/xrpld/app/tx/detail/ConfidentialMPTConvertBack.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpld/app/tx/detail/Transactor.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* @brief Converts confidential (encrypted) MPT balance back to public
|
||||
* (plaintext) balance.
|
||||
*
|
||||
* This transaction allows a token holder to convert their encrypted
|
||||
* confidential balance back into a publicly visible MPToken balance. The
|
||||
* holder must prove they have sufficient confidential balance without
|
||||
* revealing the actual balance amount.
|
||||
*
|
||||
* @par Cryptographic Operations:
|
||||
* - **Revealed Amount Verification**: Verifies that the provided encrypted
|
||||
* amounts correctly encrypt the conversion amount.
|
||||
* - **Pedersen Linkage Proof**: Verifies that the provided balance commitment
|
||||
* correctly links to the holder's encrypted spending balance.
|
||||
* - **Bulletproof Range Proof**: Verifies that the remaining balance (after
|
||||
* conversion) is non-negative, ensuring the holder has sufficient funds.
|
||||
*
|
||||
* @see ConfidentialMPTConvert, ConfidentialMPTSend
|
||||
*/
|
||||
class ConfidentialMPTConvertBack : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
|
||||
|
||||
explicit ConfidentialMPTConvertBack(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
95
src/xrpld/app/tx/detail/ConfidentialMPTMergeInbox.cpp
Normal file
95
src/xrpld/app/tx/detail/ConfidentialMPTMergeInbox.cpp
Normal file
@@ -0,0 +1,95 @@
|
||||
#include <xrpld/app/tx/detail/ConfidentialMPTMergeInbox.h>
|
||||
|
||||
#include <xrpl/protocol/ConfidentialTransfer.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
NotTEC
|
||||
ConfidentialMPTMergeInbox::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (!ctx.rules.enabled(featureConfidentialTransfer))
|
||||
return temDISABLED;
|
||||
|
||||
// issuer cannot merge
|
||||
if (MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer() == ctx.tx[sfAccount])
|
||||
return temMALFORMED;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialMPTMergeInbox::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
auto const sleIssuance = ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID]));
|
||||
if (!sleIssuance)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
if (!sleIssuance->isFlag(lsfMPTCanPrivacy))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// already checked in preflight, but should also check that issuer on the
|
||||
// issuance isn't the account either
|
||||
if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount])
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const sleMptoken = ctx.view.read(keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], ctx.tx[sfAccount]));
|
||||
if (!sleMptoken)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
if (!sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) ||
|
||||
!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) ||
|
||||
!sleMptoken->isFieldPresent(sfHolderElGamalPublicKey))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialMPTMergeInbox::doApply()
|
||||
{
|
||||
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
|
||||
auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, account_));
|
||||
if (!sleMptoken)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// sanity check
|
||||
if (!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending) ||
|
||||
!sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) ||
|
||||
!sleMptoken->isFieldPresent(sfHolderElGamalPublicKey))
|
||||
{
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
// Merge inbox into spending: spending = spending + inbox
|
||||
// This allows holder to use received funds. Without merging, incoming
|
||||
// transfers sit in inbox and cannot be spent or converted back.
|
||||
Buffer sum(ecGamalEncryptedTotalLength);
|
||||
if (TER const ter = homomorphicAdd(
|
||||
(*sleMptoken)[sfConfidentialBalanceSpending], (*sleMptoken)[sfConfidentialBalanceInbox], sum);
|
||||
!isTesSuccess(ter))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
(*sleMptoken)[sfConfidentialBalanceSpending] = sum;
|
||||
|
||||
// Reset inbox to encrypted zero. Must use canonical zero encryption
|
||||
// (deterministic ciphertext) so the ledger state is reproducible.
|
||||
auto const zeroEncryption =
|
||||
encryptCanonicalZeroAmount((*sleMptoken)[sfHolderElGamalPublicKey], account_, mptIssuanceID);
|
||||
|
||||
if (!zeroEncryption)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
(*sleMptoken)[sfConfidentialBalanceInbox] = *zeroEncryption;
|
||||
|
||||
incrementConfidentialVersion(*sleMptoken);
|
||||
|
||||
view().update(sleMptoken);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
46
src/xrpld/app/tx/detail/ConfidentialMPTMergeInbox.h
Normal file
46
src/xrpld/app/tx/detail/ConfidentialMPTMergeInbox.h
Normal file
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpld/app/tx/detail/Transactor.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* @brief Merges the confidential inbox balance into the spending balance.
|
||||
*
|
||||
* In the confidential transfer system, incoming funds are deposited into an
|
||||
* "inbox" balance that the recipient cannot immediately spend. This prevents
|
||||
* front-running attacks where an attacker could invalidate a pending
|
||||
* transaction by sending funds to the sender. This transaction merges the
|
||||
* inbox into the spending balance, making those funds available for spending.
|
||||
*
|
||||
* @par Cryptographic Operations:
|
||||
* - **Homomorphic Addition**: Adds the encrypted inbox balance to the
|
||||
* encrypted spending balance using ElGamal homomorphic properties.
|
||||
* - **Zero Encryption**: Resets the inbox to an encryption of zero.
|
||||
*
|
||||
* @note This transaction requires no zero-knowledge proofs because it only
|
||||
* combines encrypted values that the holder already owns. The
|
||||
* homomorphic properties of ElGamal encryption ensure correctness.
|
||||
*
|
||||
* @see ConfidentialMPTSend, ConfidentialMPTConvert
|
||||
*/
|
||||
class ConfidentialMPTMergeInbox : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
|
||||
|
||||
explicit ConfidentialMPTMergeInbox(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
397
src/xrpld/app/tx/detail/ConfidentialMPTSend.cpp
Normal file
397
src/xrpld/app/tx/detail/ConfidentialMPTSend.cpp
Normal file
@@ -0,0 +1,397 @@
|
||||
#include <xrpld/app/tx/detail/ConfidentialMPTSend.h>
|
||||
|
||||
#include <xrpl/ledger/CredentialHelpers.h>
|
||||
#include <xrpl/ledger/View.h>
|
||||
#include <xrpl/protocol/ConfidentialTransfer.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
NotTEC
|
||||
ConfidentialMPTSend::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (!ctx.rules.enabled(featureConfidentialTransfer))
|
||||
return temDISABLED;
|
||||
|
||||
auto const account = ctx.tx[sfAccount];
|
||||
auto const issuer = MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer();
|
||||
|
||||
// ConfidentialMPTSend only allows holder to holder, holder to second account,
|
||||
// and second account to holder transfers. So issuer cannot be the sender.
|
||||
if (account == issuer)
|
||||
return temMALFORMED;
|
||||
|
||||
// Can not send to self
|
||||
if (account == ctx.tx[sfDestination])
|
||||
return temMALFORMED;
|
||||
|
||||
// Check the length of the encrypted amounts
|
||||
if (ctx.tx[sfSenderEncryptedAmount].length() != ecGamalEncryptedTotalLength ||
|
||||
ctx.tx[sfDestinationEncryptedAmount].length() != ecGamalEncryptedTotalLength ||
|
||||
ctx.tx[sfIssuerEncryptedAmount].length() != ecGamalEncryptedTotalLength)
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
|
||||
if (hasAuditor && ctx.tx[sfAuditorEncryptedAmount].length() != ecGamalEncryptedTotalLength)
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
// Check the length of the ZKProof
|
||||
auto const recipientCount = getConfidentialRecipientCount(hasAuditor);
|
||||
auto const sizeEquality = getMultiCiphertextEqualityProofSize(recipientCount);
|
||||
auto const sizePedersenLinkage = 2 * ecPedersenProofLength;
|
||||
|
||||
if (ctx.tx[sfZKProof].length() != sizeEquality + sizePedersenLinkage + ecDoubleBulletproofLength)
|
||||
return temMALFORMED;
|
||||
|
||||
// Check the Pedersen commitments are valid
|
||||
if (!isValidCompressedECPoint(ctx.tx[sfBalanceCommitment]) || !isValidCompressedECPoint(ctx.tx[sfAmountCommitment]))
|
||||
return temMALFORMED;
|
||||
|
||||
// Check the encrypted amount formats, this is more expensive so put it at
|
||||
// the end
|
||||
if (!isValidCiphertext(ctx.tx[sfSenderEncryptedAmount]) ||
|
||||
!isValidCiphertext(ctx.tx[sfDestinationEncryptedAmount]) || !isValidCiphertext(ctx.tx[sfIssuerEncryptedAmount]))
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
if (hasAuditor && !isValidCiphertext(ctx.tx[sfAuditorEncryptedAmount]))
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
verifySendProofs(
|
||||
PreclaimContext const& ctx,
|
||||
std::shared_ptr<SLE const> const& sleSenderMPToken,
|
||||
std::shared_ptr<SLE const> const& sleDestinationMPToken,
|
||||
std::shared_ptr<SLE const> const& sleIssuance)
|
||||
{
|
||||
// Sanity check
|
||||
if (!sleSenderMPToken || !sleDestinationMPToken || !sleIssuance)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
|
||||
auto const recipientCount = getConfidentialRecipientCount(hasAuditor);
|
||||
auto const proof = ctx.tx[sfZKProof];
|
||||
size_t remainingLength = proof.size();
|
||||
size_t currentOffset = 0;
|
||||
|
||||
// Extract equality proof
|
||||
auto const sizeEquality = getMultiCiphertextEqualityProofSize(recipientCount);
|
||||
if (remainingLength < sizeEquality)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const equalityProof = proof.substr(currentOffset, sizeEquality);
|
||||
currentOffset += sizeEquality;
|
||||
remainingLength -= sizeEquality;
|
||||
|
||||
// Extract Pedersen linkage proof for amount commitment
|
||||
if (remainingLength < ecPedersenProofLength)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const amountLinkageProof = proof.substr(currentOffset, ecPedersenProofLength);
|
||||
currentOffset += ecPedersenProofLength;
|
||||
remainingLength -= ecPedersenProofLength;
|
||||
|
||||
// Extract Pedersen linkage proof for balance commitment
|
||||
if (remainingLength < ecPedersenProofLength)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
auto const balanceLinkageProof = proof.substr(currentOffset, ecPedersenProofLength);
|
||||
currentOffset += ecPedersenProofLength;
|
||||
remainingLength -= ecPedersenProofLength;
|
||||
|
||||
// Extract range proof
|
||||
if (remainingLength < ecDoubleBulletproofLength)
|
||||
return tecINTERNAL;
|
||||
|
||||
auto const rangeProof = proof.substr(currentOffset, ecDoubleBulletproofLength);
|
||||
currentOffset += ecDoubleBulletproofLength;
|
||||
remainingLength -= ecDoubleBulletproofLength;
|
||||
|
||||
if (remainingLength != 0)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// Prepare recipient list
|
||||
std::vector<ConfidentialRecipient> recipients;
|
||||
recipients.reserve(recipientCount);
|
||||
|
||||
recipients.push_back({(*sleSenderMPToken)[sfHolderElGamalPublicKey], ctx.tx[sfSenderEncryptedAmount]});
|
||||
recipients.push_back({(*sleDestinationMPToken)[sfHolderElGamalPublicKey], ctx.tx[sfDestinationEncryptedAmount]});
|
||||
recipients.push_back({(*sleIssuance)[sfIssuerElGamalPublicKey], ctx.tx[sfIssuerEncryptedAmount]});
|
||||
|
||||
if (hasAuditor)
|
||||
{
|
||||
recipients.push_back({(*sleIssuance)[sfAuditorElGamalPublicKey], ctx.tx[sfAuditorEncryptedAmount]});
|
||||
}
|
||||
|
||||
// Prepare the context hash
|
||||
auto const contextHash = getSendContextHash(
|
||||
ctx.tx[sfAccount],
|
||||
ctx.tx[sfSequence],
|
||||
ctx.tx[sfMPTokenIssuanceID],
|
||||
ctx.tx[sfDestination],
|
||||
(*sleSenderMPToken)[~sfConfidentialBalanceVersion].value_or(0));
|
||||
|
||||
// Use a boolean flag to track validity instead of returning early on failure to prevent leaking information about
|
||||
// which proof failed through timing differences
|
||||
bool valid = true;
|
||||
|
||||
// Verify the multi-ciphertext equality proof
|
||||
if (auto const ter = verifyMultiCiphertextEqualityProof(equalityProof, recipients, recipientCount, contextHash);
|
||||
!isTesSuccess(ter))
|
||||
{
|
||||
valid = false;
|
||||
}
|
||||
|
||||
// Verify amount linkage
|
||||
if (auto const ter = verifyAmountPcmLinkage(
|
||||
amountLinkageProof,
|
||||
ctx.tx[sfSenderEncryptedAmount],
|
||||
(*sleSenderMPToken)[sfHolderElGamalPublicKey],
|
||||
ctx.tx[sfAmountCommitment],
|
||||
contextHash);
|
||||
!isTesSuccess(ter))
|
||||
{
|
||||
valid = false;
|
||||
}
|
||||
|
||||
// Verify balance linkage
|
||||
if (auto const ter = verifyBalancePcmLinkage(
|
||||
balanceLinkageProof,
|
||||
(*sleSenderMPToken)[sfConfidentialBalanceSpending],
|
||||
(*sleSenderMPToken)[sfHolderElGamalPublicKey],
|
||||
ctx.tx[sfBalanceCommitment],
|
||||
contextHash);
|
||||
!isTesSuccess(ter))
|
||||
{
|
||||
valid = false;
|
||||
}
|
||||
|
||||
// Verify Range Proof
|
||||
{
|
||||
Buffer pcRem;
|
||||
|
||||
// Derive PC_rem = PC_balance - PC_amount
|
||||
if (auto const ter = computeSendRemainder(ctx.tx[sfBalanceCommitment], ctx.tx[sfAmountCommitment], pcRem);
|
||||
!isTesSuccess(ter))
|
||||
{
|
||||
valid = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Aggregated commitments: [PC_amount, PC_rem]
|
||||
// Prove that both the transfer amount and the remaining balance are in range
|
||||
std::vector<Slice> commitments;
|
||||
commitments.push_back(ctx.tx[sfAmountCommitment]);
|
||||
commitments.push_back(Slice{pcRem.data(), pcRem.size()});
|
||||
|
||||
if (auto const ter = verifyAggregatedBulletproof(rangeProof, commitments, contextHash); !isTesSuccess(ter))
|
||||
{
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!valid)
|
||||
{
|
||||
JLOG(ctx.j.trace()) << "ConfidentialMPTSend: One or more cryptographic proofs failed.";
|
||||
return tecBAD_PROOF;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialMPTSend::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
// Check if sender account exists
|
||||
auto const account = ctx.tx[sfAccount];
|
||||
if (!ctx.view.exists(keylet::account(account)))
|
||||
return terNO_ACCOUNT;
|
||||
|
||||
// Check if destination account exists
|
||||
auto const destination = ctx.tx[sfDestination];
|
||||
if (!ctx.view.exists(keylet::account(destination)))
|
||||
return tecNO_TARGET;
|
||||
|
||||
// Check if MPT issuance exists
|
||||
auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
|
||||
auto const sleIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID));
|
||||
if (!sleIssuance)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
// Check if the issuance allows transfer
|
||||
if (!sleIssuance->isFlag(lsfMPTCanTransfer))
|
||||
return tecNO_AUTH;
|
||||
|
||||
// Check if issuance allows confidential transfer
|
||||
if (!sleIssuance->isFlag(lsfMPTCanPrivacy))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// Check if issuance has issuer ElGamal public key
|
||||
if (!sleIssuance->isFieldPresent(sfIssuerElGamalPublicKey))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
|
||||
bool const requiresAuditor = sleIssuance->isFieldPresent(sfAuditorElGamalPublicKey);
|
||||
|
||||
// Tx must include auditor ciphertext if the issuance has enabled
|
||||
// auditing, and must not include it if auditing is not enabled
|
||||
if (requiresAuditor != hasAuditor)
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// Sanity check: issuer isn't the sender
|
||||
if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount])
|
||||
return tefINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// Check sender's MPToken existence
|
||||
auto const sleSenderMPToken = ctx.view.read(keylet::mptoken(mptIssuanceID, account));
|
||||
if (!sleSenderMPToken)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
// Check sender's MPToken has necessary fields for confidential send
|
||||
if (!sleSenderMPToken->isFieldPresent(sfHolderElGamalPublicKey) ||
|
||||
!sleSenderMPToken->isFieldPresent(sfConfidentialBalanceSpending) ||
|
||||
!sleSenderMPToken->isFieldPresent(sfIssuerEncryptedBalance))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// Sanity check: MPToken's auditor field must be present if auditing is
|
||||
// enabled
|
||||
if (requiresAuditor && !sleSenderMPToken->isFieldPresent(sfAuditorEncryptedBalance))
|
||||
return tefINTERNAL;
|
||||
|
||||
// Check destination's MPToken existence
|
||||
auto const sleDestinationMPToken = ctx.view.read(keylet::mptoken(mptIssuanceID, destination));
|
||||
if (!sleDestinationMPToken)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
// Check destination's MPToken has necessary fields for confidential send
|
||||
if (!sleDestinationMPToken->isFieldPresent(sfHolderElGamalPublicKey) ||
|
||||
!sleDestinationMPToken->isFieldPresent(sfConfidentialBalanceInbox) ||
|
||||
!sleDestinationMPToken->isFieldPresent(sfIssuerEncryptedBalance))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// Check lock
|
||||
MPTIssue const mptIssue(mptIssuanceID);
|
||||
if (auto const ter = checkFrozen(ctx.view, account, mptIssue); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
if (auto const ter = checkFrozen(ctx.view, destination, mptIssue); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
// Check auth
|
||||
if (auto const ter = requireAuth(ctx.view, mptIssue, account); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
if (auto const ter = requireAuth(ctx.view, mptIssue, destination); !isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
return verifySendProofs(ctx, sleSenderMPToken, sleDestinationMPToken, sleIssuance);
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialMPTSend::doApply()
|
||||
{
|
||||
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
|
||||
auto const destination = ctx_.tx[sfDestination];
|
||||
|
||||
auto sleSenderMPToken = view().peek(keylet::mptoken(mptIssuanceID, account_));
|
||||
auto sleDestinationMPToken = view().peek(keylet::mptoken(mptIssuanceID, destination));
|
||||
|
||||
auto sleDestAcct = view().peek(keylet::account(destination));
|
||||
|
||||
if (!sleSenderMPToken || !sleDestinationMPToken || !sleDestAcct)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
if (auto err = verifyDepositPreauth(ctx_.tx, ctx_.view(), account_, destination, sleDestAcct, ctx_.journal);
|
||||
!isTesSuccess(err))
|
||||
return err;
|
||||
|
||||
Slice const senderEc = ctx_.tx[sfSenderEncryptedAmount];
|
||||
Slice const destEc = ctx_.tx[sfDestinationEncryptedAmount];
|
||||
Slice const issuerEc = ctx_.tx[sfIssuerEncryptedAmount];
|
||||
|
||||
auto const auditorEc = ctx_.tx[~sfAuditorEncryptedAmount];
|
||||
|
||||
// Subtract from sender's spending balance
|
||||
{
|
||||
Slice const curSpending = (*sleSenderMPToken)[sfConfidentialBalanceSpending];
|
||||
Buffer newSpending(ecGamalEncryptedTotalLength);
|
||||
|
||||
if (TER const ter = homomorphicSubtract(curSpending, senderEc, newSpending); !isTesSuccess(ter))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
(*sleSenderMPToken)[sfConfidentialBalanceSpending] = newSpending;
|
||||
}
|
||||
|
||||
// Subtract from issuer's balance
|
||||
{
|
||||
Slice const curIssuerEnc = (*sleSenderMPToken)[sfIssuerEncryptedBalance];
|
||||
Buffer newIssuerEnc(ecGamalEncryptedTotalLength);
|
||||
|
||||
if (TER const ter = homomorphicSubtract(curIssuerEnc, issuerEc, newIssuerEnc); !isTesSuccess(ter))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
(*sleSenderMPToken)[sfIssuerEncryptedBalance] = newIssuerEnc;
|
||||
}
|
||||
|
||||
// Subtract from auditor's balance if present
|
||||
if (auditorEc)
|
||||
{
|
||||
Slice const curAuditorEnc = (*sleSenderMPToken)[sfAuditorEncryptedBalance];
|
||||
Buffer newAuditorEnc(ecGamalEncryptedTotalLength);
|
||||
|
||||
if (TER const ter = homomorphicSubtract(curAuditorEnc, *auditorEc, newAuditorEnc); !isTesSuccess(ter))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
(*sleSenderMPToken)[sfAuditorEncryptedBalance] = newAuditorEnc;
|
||||
}
|
||||
|
||||
// Add to destination's inbox balance
|
||||
{
|
||||
Slice const curInbox = (*sleDestinationMPToken)[sfConfidentialBalanceInbox];
|
||||
Buffer newInbox(ecGamalEncryptedTotalLength);
|
||||
|
||||
if (TER const ter = homomorphicAdd(curInbox, destEc, newInbox); !isTesSuccess(ter))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
(*sleDestinationMPToken)[sfConfidentialBalanceInbox] = newInbox;
|
||||
}
|
||||
|
||||
// Add to issuer's balance
|
||||
{
|
||||
Slice const curIssuerEnc = (*sleDestinationMPToken)[sfIssuerEncryptedBalance];
|
||||
Buffer newIssuerEnc(ecGamalEncryptedTotalLength);
|
||||
|
||||
if (TER const ter = homomorphicAdd(curIssuerEnc, issuerEc, newIssuerEnc); !isTesSuccess(ter))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
(*sleDestinationMPToken)[sfIssuerEncryptedBalance] = newIssuerEnc;
|
||||
}
|
||||
|
||||
// Add to auditor's balance if present
|
||||
if (auditorEc)
|
||||
{
|
||||
Slice const curAuditorEnc = (*sleDestinationMPToken)[sfAuditorEncryptedBalance];
|
||||
Buffer newAuditorEnc(ecGamalEncryptedTotalLength);
|
||||
|
||||
if (TER const ter = homomorphicAdd(curAuditorEnc, *auditorEc, newAuditorEnc); !isTesSuccess(ter))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
(*sleDestinationMPToken)[sfAuditorEncryptedBalance] = newAuditorEnc;
|
||||
}
|
||||
|
||||
// increment version
|
||||
incrementConfidentialVersion(*sleSenderMPToken);
|
||||
incrementConfidentialVersion(*sleDestinationMPToken);
|
||||
|
||||
view().update(sleSenderMPToken);
|
||||
view().update(sleDestinationMPToken);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
} // namespace xrpl
|
||||
52
src/xrpld/app/tx/detail/ConfidentialMPTSend.h
Normal file
52
src/xrpld/app/tx/detail/ConfidentialMPTSend.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpld/app/tx/detail/Transactor.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* @brief Transfers confidential MPT tokens between holders privately.
|
||||
*
|
||||
* This transaction enables private token transfers where the transfer amount
|
||||
* is hidden from public view. Both sender and recipient must have initialized
|
||||
* confidential balances. The transaction provides encrypted amounts for all
|
||||
* parties (sender, destination, issuer, and optionally auditor) along with
|
||||
* zero-knowledge proofs that verify correctness without revealing the amount.
|
||||
*
|
||||
* @par Cryptographic Operations:
|
||||
* - **Multi-Ciphertext Equality Proof**: Verifies that all encrypted amounts
|
||||
* (sender, destination, issuer, auditor) encrypt the same plaintext value.
|
||||
* - **Amount Pedersen Linkage Proof**: Verifies that the amount commitment
|
||||
* correctly links to the sender's encrypted amount.
|
||||
* - **Balance Pedersen Linkage Proof**: Verifies that the balance commitment
|
||||
* correctly links to the sender's encrypted spending balance.
|
||||
* - **Bulletproof Range Proof**: Verifies remaining balance and
|
||||
* transfer amount are non-negative.
|
||||
*
|
||||
* @note Funds are deposited into the destination's inbox, not spending
|
||||
* balance. The recipient must call ConfidentialMPTMergeInbox to make
|
||||
* received funds spendable.
|
||||
*
|
||||
* @see ConfidentialMPTMergeInbox, ConfidentialMPTConvert,
|
||||
* ConfidentialMPTConvertBack
|
||||
*/
|
||||
class ConfidentialMPTSend : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
|
||||
|
||||
explicit ConfidentialMPTSend(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
};
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -3310,4 +3310,204 @@ ValidVault::finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadVie
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
ValidConfidentialMPToken::visitEntry(
|
||||
bool isDelete,
|
||||
std::shared_ptr<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> const& after)
|
||||
{
|
||||
// Helper to get MPToken Issuance ID safely
|
||||
auto const getMptID = [](std::shared_ptr<SLE const> const& sle) -> uint192 {
|
||||
if (!sle)
|
||||
return beast::zero;
|
||||
if (sle->getType() == ltMPTOKEN)
|
||||
return sle->getFieldH192(sfMPTokenIssuanceID);
|
||||
if (sle->getType() == ltMPTOKEN_ISSUANCE)
|
||||
return makeMptID(sle->getFieldU32(sfSequence), sle->getAccountID(sfIssuer));
|
||||
return beast::zero;
|
||||
};
|
||||
|
||||
if (before && before->getType() == ltMPTOKEN)
|
||||
{
|
||||
uint192 const id = getMptID(before);
|
||||
changes_[id].mptAmountDelta -= before->getFieldU64(sfMPTAmount);
|
||||
|
||||
// Cannot delete MPToken with non-zero confidential state or non-zero public amount
|
||||
if (isDelete)
|
||||
{
|
||||
bool const hasPublicBalance = before->getFieldU64(sfMPTAmount) > 0;
|
||||
bool const hasEncryptedFields = before->isFieldPresent(sfConfidentialBalanceSpending) ||
|
||||
before->isFieldPresent(sfConfidentialBalanceInbox) || before->isFieldPresent(sfIssuerEncryptedBalance);
|
||||
|
||||
if (hasPublicBalance || hasEncryptedFields)
|
||||
changes_[id].deletedWithEncrypted = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (after && after->getType() == ltMPTOKEN)
|
||||
{
|
||||
uint192 const id = getMptID(after);
|
||||
changes_[id].mptAmountDelta += after->getFieldU64(sfMPTAmount);
|
||||
|
||||
// Encrypted field existence consistency
|
||||
bool const hasIssuerBalance = after->isFieldPresent(sfIssuerEncryptedBalance);
|
||||
bool const hasHolderInbox = after->isFieldPresent(sfConfidentialBalanceInbox);
|
||||
bool const hasHolderSpending = after->isFieldPresent(sfConfidentialBalanceSpending);
|
||||
|
||||
bool const hasAnyHolder = hasHolderInbox || hasHolderSpending;
|
||||
|
||||
if (hasAnyHolder != hasIssuerBalance)
|
||||
{
|
||||
changes_[id].badConsistency = true;
|
||||
}
|
||||
|
||||
// Privacy flag consistency
|
||||
bool const hasEncrypted = hasAnyHolder || hasIssuerBalance;
|
||||
if (hasEncrypted)
|
||||
changes_[id].requiresPrivacyFlag = true;
|
||||
}
|
||||
|
||||
if (before && before->getType() == ltMPTOKEN_ISSUANCE)
|
||||
{
|
||||
uint192 const id = getMptID(before);
|
||||
if (before->isFieldPresent(sfConfidentialOutstandingAmount))
|
||||
changes_[id].coaDelta -= before->getFieldU64(sfConfidentialOutstandingAmount);
|
||||
changes_[id].outstandingDelta -= before->getFieldU64(sfOutstandingAmount);
|
||||
}
|
||||
|
||||
if (after && after->getType() == ltMPTOKEN_ISSUANCE)
|
||||
{
|
||||
uint192 const id = getMptID(after);
|
||||
auto& change = changes_[id];
|
||||
|
||||
bool const hasCOA = after->isFieldPresent(sfConfidentialOutstandingAmount);
|
||||
std::uint64_t const coa = (*after)[~sfConfidentialOutstandingAmount].value_or(0);
|
||||
std::uint64_t const oa = after->getFieldU64(sfOutstandingAmount);
|
||||
|
||||
if (hasCOA)
|
||||
change.coaDelta += coa;
|
||||
|
||||
change.outstandingDelta += oa;
|
||||
change.issuance = after;
|
||||
|
||||
// COA <= OutstandingAmount
|
||||
if (coa > oa)
|
||||
change.badCOA = true;
|
||||
}
|
||||
|
||||
if (before && after && before->getType() == ltMPTOKEN && after->getType() == ltMPTOKEN)
|
||||
{
|
||||
uint192 const id = getMptID(after);
|
||||
|
||||
// sfConfidentialBalanceVersion must change when spending changes
|
||||
auto const spendingBefore = (*before)[~sfConfidentialBalanceSpending];
|
||||
auto const spendingAfter = (*after)[~sfConfidentialBalanceSpending];
|
||||
auto const versionBefore = (*before)[~sfConfidentialBalanceVersion];
|
||||
auto const versionAfter = (*after)[~sfConfidentialBalanceVersion];
|
||||
|
||||
if (spendingBefore.has_value() && spendingBefore != spendingAfter)
|
||||
{
|
||||
if (versionBefore == versionAfter)
|
||||
{
|
||||
changes_[id].badVersion = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
ValidConfidentialMPToken::finalize(
|
||||
STTx const& tx,
|
||||
TER const result,
|
||||
XRPAmount const,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
if (result != tesSUCCESS)
|
||||
return true;
|
||||
|
||||
for (auto const& [id, checks] : changes_)
|
||||
{
|
||||
// Find the MPTokenIssuance
|
||||
auto const issuance = [&]() -> std::shared_ptr<SLE const> {
|
||||
if (checks.issuance)
|
||||
return checks.issuance;
|
||||
return view.read(keylet::mptIssuance(id));
|
||||
}();
|
||||
|
||||
// Skip all invariance checks if issuance doesn't exist because that means the MPT has been deleted
|
||||
if (!issuance)
|
||||
continue;
|
||||
|
||||
// Cannot delete MPToken with non-zero confidential state
|
||||
if (checks.deletedWithEncrypted)
|
||||
{
|
||||
if ((*issuance)[~sfConfidentialOutstandingAmount].value_or(0) > 0)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: MPToken deleted with encrypted fields while COA > 0";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypted field existence consistency
|
||||
if (checks.badConsistency)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: MPToken encrypted field "
|
||||
"existence inconsistency";
|
||||
return false;
|
||||
}
|
||||
|
||||
// COA <= OutstandingAmount
|
||||
if (checks.badCOA)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: Confidential outstanding amount "
|
||||
"exceeds total outstanding amount";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Privacy flag consistency
|
||||
if (checks.requiresPrivacyFlag)
|
||||
{
|
||||
if (!issuance->isFlag(lsfMPTCanPrivacy))
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: MPToken has encrypted "
|
||||
"fields but Issuance does not have "
|
||||
"lsfMPTCanPrivacy set";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// We only enforce this when Confidential Outstanding Amount changes (Convert, ConvertBack,
|
||||
// ConfidentialClawback). This avoids falsely failing on Escrow or AMM operations that lock public tokens
|
||||
// outside of ltMPTOKEN.
|
||||
// Convert / ConvertBack:
|
||||
// - COA and MPTAmount must have opposite deltas, which cancel each other out to zero.
|
||||
// - OA remains unchanged.
|
||||
// - Therefore, the net delta on both sides of the equation is zero.
|
||||
//
|
||||
// Clawback:
|
||||
// - MPTAmount remains unchanged.
|
||||
// - COA and OA must have identical deltas (mirrored on each side).
|
||||
// - The equation remains balanced as both sides have equal offsets.
|
||||
if (checks.coaDelta != 0)
|
||||
{
|
||||
if (checks.mptAmountDelta + checks.coaDelta != checks.outstandingDelta)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: Token conservation "
|
||||
"violation for MPT "
|
||||
<< to_string(id);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (checks.badVersion)
|
||||
{
|
||||
JLOG(j.fatal()) << "Invariant failed: MPToken sfConfidentialBalanceVersion not updated when "
|
||||
"sfConfidentialBalanceSpending changed";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -678,6 +678,48 @@ public:
|
||||
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Invariants: Confidential MPToken consistency
|
||||
*
|
||||
* - Convert/ConvertBack symmetry:
|
||||
* Regular MPToken balance change (±X) == COA (Confidential Outstanding Amount) change (∓X)
|
||||
* - Cannot delete MPToken with non-zero confidential state:
|
||||
* Cannot delete if sfIssuerEncryptedBalance exists
|
||||
* Cannot delete if sfConfidentialBalanceInbox and sfConfidentialBalanceSpending exist
|
||||
* - Privacy flag consistency:
|
||||
* MPToken can only have encrypted fields if lsfMPTCanPrivacy is set on
|
||||
* issuance.
|
||||
* - Encrypted field existence consistency:
|
||||
* If sfConfidentialBalanceSpending/sfConfidentialBalanceInbox exists, then
|
||||
* sfIssuerEncryptedBalance must also exist (and vice versa).
|
||||
* - COA <= OutstandingAmount:
|
||||
* Confidential outstanding balance cannot exceed total outstanding.
|
||||
* - Verifies sfConfidentialBalanceVersion is changed whenever sfConfidentialBalanceSpending is modified on an MPToken.
|
||||
*/
|
||||
class ValidConfidentialMPToken
|
||||
{
|
||||
struct Changes
|
||||
{
|
||||
std::int64_t mptAmountDelta = 0;
|
||||
std::int64_t coaDelta = 0;
|
||||
std::int64_t outstandingDelta = 0;
|
||||
SLE::const_pointer issuance;
|
||||
bool deletedWithEncrypted = false;
|
||||
bool badConsistency = false;
|
||||
bool badCOA = false;
|
||||
bool requiresPrivacyFlag = false;
|
||||
bool badVersion = false;
|
||||
};
|
||||
std::map<uint192, Changes> changes_;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
|
||||
|
||||
bool
|
||||
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
|
||||
};
|
||||
|
||||
// additional invariant checks can be declared above and then added to this
|
||||
// tuple
|
||||
using InvariantChecks = std::tuple<
|
||||
@@ -704,7 +746,8 @@ using InvariantChecks = std::tuple<
|
||||
ValidPseudoAccounts,
|
||||
ValidLoanBroker,
|
||||
ValidLoan,
|
||||
ValidVault>;
|
||||
ValidVault,
|
||||
ValidConfidentialMPToken>;
|
||||
|
||||
/**
|
||||
* @brief get a tuple of all invariant checks
|
||||
|
||||
@@ -70,6 +70,23 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
|
||||
if (ctx.view.rules().enabled(featureSingleAssetVault) && sleMpt->isFlag(lsfMPTLocked))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
if (ctx.view.rules().enabled(featureConfidentialTransfer))
|
||||
{
|
||||
auto const sleMptIssuance = ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID]));
|
||||
|
||||
// if there still existing encrypted balances of MPT in
|
||||
// circulation
|
||||
if (sleMptIssuance && (*sleMptIssuance)[~sfConfidentialOutstandingAmount].value_or(0) != 0)
|
||||
{
|
||||
// this MPT still has encrypted balance, since we don't know
|
||||
// if it's non-zero or not, we won't allow deletion of
|
||||
// MPToken
|
||||
if (sleMpt->isFieldPresent(sfConfidentialBalanceInbox) ||
|
||||
sleMpt->isFieldPresent(sfConfidentialBalanceSpending))
|
||||
return tecHAS_OBLIGATIONS;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,14 @@ MPTokenIssuanceCreate::checkExtraFeatures(PreflightContext const& ctx)
|
||||
if (ctx.tx.isFieldPresent(sfMutableFlags) && !ctx.rules.enabled(featureDynamicMPT))
|
||||
return false;
|
||||
|
||||
if (ctx.tx.isFlag(tfMPTCanPrivacy) && !ctx.rules.enabled(featureConfidentialTransfer))
|
||||
return false;
|
||||
|
||||
// can not set tmfMPTCannotMutatePrivacy without featureConfidentialTransfer
|
||||
auto const mutableFlags = ctx.tx[~sfMutableFlags];
|
||||
if (mutableFlags && (*mutableFlags & tmfMPTCannotMutatePrivacy) && !ctx.rules.enabled(featureConfidentialTransfer))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <xrpld/app/misc/DelegateUtils.h>
|
||||
#include <xrpld/app/tx/detail/MPTokenIssuanceSet.h>
|
||||
|
||||
#include <xrpl/protocol/ConfidentialTransfer.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/LedgerFormats.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
@@ -27,16 +28,19 @@ struct MPTMutabilityFlags
|
||||
{
|
||||
std::uint32_t setFlag;
|
||||
std::uint32_t clearFlag;
|
||||
std::uint32_t canMutateFlag;
|
||||
std::uint32_t mutabilityFlag;
|
||||
std::uint32_t targetFlag;
|
||||
bool isCannotMutate = false; // if true, cannot mutate by default.
|
||||
};
|
||||
|
||||
static constexpr std::array<MPTMutabilityFlags, 6> mptMutabilityFlags = {
|
||||
{{tmfMPTSetCanLock, tmfMPTClearCanLock, lsmfMPTCanMutateCanLock},
|
||||
{tmfMPTSetRequireAuth, tmfMPTClearRequireAuth, lsmfMPTCanMutateRequireAuth},
|
||||
{tmfMPTSetCanEscrow, tmfMPTClearCanEscrow, lsmfMPTCanMutateCanEscrow},
|
||||
{tmfMPTSetCanTrade, tmfMPTClearCanTrade, lsmfMPTCanMutateCanTrade},
|
||||
{tmfMPTSetCanTransfer, tmfMPTClearCanTransfer, lsmfMPTCanMutateCanTransfer},
|
||||
{tmfMPTSetCanClawback, tmfMPTClearCanClawback, lsmfMPTCanMutateCanClawback}}};
|
||||
static constexpr std::array<MPTMutabilityFlags, 7> mptMutabilityFlags = {
|
||||
{{tmfMPTSetCanLock, tmfMPTClearCanLock, lsmfMPTCanMutateCanLock, lsfMPTCanLock},
|
||||
{tmfMPTSetRequireAuth, tmfMPTClearRequireAuth, lsmfMPTCanMutateRequireAuth, lsfMPTRequireAuth},
|
||||
{tmfMPTSetCanEscrow, tmfMPTClearCanEscrow, lsmfMPTCanMutateCanEscrow, lsfMPTCanEscrow},
|
||||
{tmfMPTSetCanTrade, tmfMPTClearCanTrade, lsmfMPTCanMutateCanTrade, lsfMPTCanTrade},
|
||||
{tmfMPTSetCanTransfer, tmfMPTClearCanTransfer, lsmfMPTCanMutateCanTransfer, lsfMPTCanTransfer},
|
||||
{tmfMPTSetCanClawback, tmfMPTClearCanClawback, lsmfMPTCanMutateCanClawback, lsfMPTCanClawback},
|
||||
{tmfMPTSetPrivacy, tmfMPTClearPrivacy, lsmfMPTCannotMutatePrivacy, lsfMPTCanPrivacy, true}}};
|
||||
|
||||
NotTEC
|
||||
MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
|
||||
@@ -45,14 +49,27 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
|
||||
auto const metadata = ctx.tx[~sfMPTokenMetadata];
|
||||
auto const transferFee = ctx.tx[~sfTransferFee];
|
||||
auto const isMutate = mutableFlags || metadata || transferFee;
|
||||
auto const hasIssuerElGamalKey = ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey);
|
||||
auto const hasAuditorElGamalKey = ctx.tx.isFieldPresent(sfAuditorElGamalPublicKey);
|
||||
auto const txFlags = ctx.tx.getFlags();
|
||||
|
||||
auto const mutatePrivacy = mutableFlags && ((*mutableFlags & (tmfMPTSetPrivacy | tmfMPTClearPrivacy)));
|
||||
|
||||
auto const hasDomain = ctx.tx.isFieldPresent(sfDomainID);
|
||||
auto const hasHolder = ctx.tx.isFieldPresent(sfHolder);
|
||||
|
||||
if (isMutate && !ctx.rules.enabled(featureDynamicMPT))
|
||||
return temDISABLED;
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfDomainID) && ctx.tx.isFieldPresent(sfHolder))
|
||||
if ((hasIssuerElGamalKey || hasAuditorElGamalKey || mutatePrivacy) &&
|
||||
!ctx.rules.enabled(featureConfidentialTransfer))
|
||||
return temDISABLED;
|
||||
|
||||
if (hasDomain && hasHolder)
|
||||
return temMALFORMED;
|
||||
|
||||
auto const txFlags = ctx.tx.getFlags();
|
||||
if (mutatePrivacy && hasHolder)
|
||||
return temMALFORMED;
|
||||
|
||||
// fails if both flags are set
|
||||
if ((txFlags & tfMPTLock) && (txFlags & tfMPTUnlock))
|
||||
@@ -63,10 +80,11 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
|
||||
if (holderID && accountID == holderID)
|
||||
return temMALFORMED;
|
||||
|
||||
if (ctx.rules.enabled(featureSingleAssetVault) || ctx.rules.enabled(featureDynamicMPT))
|
||||
if (ctx.rules.enabled(featureSingleAssetVault) || ctx.rules.enabled(featureDynamicMPT) ||
|
||||
ctx.rules.enabled(featureConfidentialTransfer))
|
||||
{
|
||||
// Is this transaction actually changing anything ?
|
||||
if (txFlags == 0 && !ctx.tx.isFieldPresent(sfDomainID) && !isMutate)
|
||||
if (txFlags == 0 && !hasDomain && !hasIssuerElGamalKey && !hasAuditorElGamalKey && !isMutate)
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
@@ -104,6 +122,18 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
|
||||
}
|
||||
}
|
||||
|
||||
if (hasHolder && (hasIssuerElGamalKey || hasAuditorElGamalKey))
|
||||
return temMALFORMED;
|
||||
|
||||
if (hasAuditorElGamalKey && !hasIssuerElGamalKey)
|
||||
return temMALFORMED;
|
||||
|
||||
if (hasIssuerElGamalKey && !isValidCompressedECPoint(ctx.tx[sfIssuerElGamalPublicKey]))
|
||||
return temMALFORMED;
|
||||
|
||||
if (hasAuditorElGamalKey && !isValidCompressedECPoint(ctx.tx[sfAuditorElGamalPublicKey]))
|
||||
return temMALFORMED;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -193,13 +223,26 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
auto isMutableFlag = [&](std::uint32_t mutableFlag) -> bool { return currentMutableFlags & mutableFlag; };
|
||||
|
||||
if (auto const mutableFlags = ctx.tx[~sfMutableFlags])
|
||||
auto const mutableFlags = ctx.tx[~sfMutableFlags];
|
||||
if (mutableFlags)
|
||||
{
|
||||
if (std::any_of(
|
||||
mptMutabilityFlags.begin(), mptMutabilityFlags.end(), [mutableFlags, &isMutableFlag](auto const& f) {
|
||||
return !isMutableFlag(f.canMutateFlag) && ((*mutableFlags & (f.setFlag | f.clearFlag)));
|
||||
bool const canMutate =
|
||||
f.isCannotMutate ? isMutableFlag(f.mutabilityFlag) : !isMutableFlag(f.mutabilityFlag);
|
||||
return canMutate && (*mutableFlags & (f.setFlag | f.clearFlag));
|
||||
}))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
if ((*mutableFlags & tmfMPTSetPrivacy) || (*mutableFlags & tmfMPTClearPrivacy))
|
||||
{
|
||||
std::uint64_t const confidentialOA = (*sleMptIssuance)[~sfConfidentialOutstandingAmount].value_or(0);
|
||||
|
||||
// If there's any confidential outstanding amount, disallow toggling
|
||||
// the lsfMPTCanPrivacy flag
|
||||
if (confidentialOA > 0)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isMutableFlag(lsmfMPTCanMutateMetadata) && ctx.tx.isFieldPresent(sfMPTokenMetadata))
|
||||
@@ -218,6 +261,35 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
// cannot update issuer public key
|
||||
if (ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey) && sleMptIssuance->isFieldPresent(sfIssuerElGamalPublicKey))
|
||||
{
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
// cannot update auditor public key
|
||||
if (ctx.tx.isFieldPresent(sfAuditorElGamalPublicKey) && sleMptIssuance->isFieldPresent(sfAuditorElGamalPublicKey))
|
||||
{
|
||||
return tecNO_PERMISSION; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey) && !sleMptIssuance->isFlag(lsfMPTCanPrivacy))
|
||||
{
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfAuditorElGamalPublicKey) && !sleMptIssuance->isFlag(lsfMPTCanPrivacy))
|
||||
{
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
// cannot upload key if there's circulating supply of COA
|
||||
if ((ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey) || ctx.tx.isFieldPresent(sfAuditorElGamalPublicKey)) &&
|
||||
sleMptIssuance->isFieldPresent(sfConfidentialOutstandingAmount))
|
||||
{
|
||||
return tecNO_PERMISSION; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -251,9 +323,9 @@ MPTokenIssuanceSet::doApply()
|
||||
for (auto const& f : mptMutabilityFlags)
|
||||
{
|
||||
if (mutableFlags & f.setFlag)
|
||||
flagsOut |= f.canMutateFlag;
|
||||
flagsOut |= f.targetFlag;
|
||||
else if (mutableFlags & f.clearFlag)
|
||||
flagsOut &= ~f.canMutateFlag;
|
||||
flagsOut &= ~f.targetFlag;
|
||||
}
|
||||
|
||||
if (mutableFlags & tmfMPTClearCanTransfer)
|
||||
@@ -303,6 +375,22 @@ MPTokenIssuanceSet::doApply()
|
||||
}
|
||||
}
|
||||
|
||||
if (auto const pubKey = ctx_.tx[~sfIssuerElGamalPublicKey])
|
||||
{
|
||||
// This is enforced in preflight.
|
||||
XRPL_ASSERT(sle->getType() == ltMPTOKEN_ISSUANCE, "MPTokenIssuanceSet::doApply : modifying MPTokenIssuance");
|
||||
|
||||
sle->setFieldVL(sfIssuerElGamalPublicKey, *pubKey);
|
||||
}
|
||||
|
||||
if (auto const pubKey = ctx_.tx[~sfAuditorElGamalPublicKey])
|
||||
{
|
||||
// This is enforced in preflight.
|
||||
XRPL_ASSERT(sle->getType() == ltMPTOKEN_ISSUANCE, "MPTokenIssuanceSet::doApply : modifying MPTokenIssuance");
|
||||
|
||||
sle->setFieldVL(sfAuditorElGamalPublicKey, *pubKey);
|
||||
}
|
||||
|
||||
view().update(sle);
|
||||
|
||||
return tesSUCCESS;
|
||||
|
||||
Reference in New Issue
Block a user