mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-19 18:45:52 +00:00
Compare commits
28 Commits
pratik/ope
...
ripple/con
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
180
include/xrpl/protocol/ConfidentialTransfer.h
Normal file
180
include/xrpl/protocol/ConfidentialTransfer.h
Normal file
@@ -0,0 +1,180 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_PROTOCOL_CONFIDENTIALTRANSFER_H_INCLUDED
|
||||
#define RIPPLE_PROTOCOL_CONFIDENTIALTRANSFER_H_INCLUDED
|
||||
|
||||
#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/detail/secp256k1.h>
|
||||
|
||||
#include <secp256k1.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/**
|
||||
* @brief Generates a new secp256k1 key pair.
|
||||
*/
|
||||
SECP256K1_API int
|
||||
secp256k1_elgamal_generate_keypair(
|
||||
secp256k1_context const* ctx,
|
||||
unsigned char* privkey,
|
||||
secp256k1_pubkey* pubkey);
|
||||
|
||||
/**
|
||||
* @brief Encrypts a 64-bit amount using ElGamal.
|
||||
*/
|
||||
SECP256K1_API int
|
||||
secp256k1_elgamal_encrypt(
|
||||
secp256k1_context const* ctx,
|
||||
secp256k1_pubkey* c1,
|
||||
secp256k1_pubkey* c2,
|
||||
secp256k1_pubkey const* pubkey_Q,
|
||||
uint64_t amount,
|
||||
unsigned char const* blinding_factor);
|
||||
|
||||
/**
|
||||
* @brief Decrypts an ElGamal ciphertext to recover the amount.
|
||||
*/
|
||||
SECP256K1_API int
|
||||
secp256k1_elgamal_decrypt(
|
||||
secp256k1_context const* ctx,
|
||||
uint64_t* amount,
|
||||
secp256k1_pubkey const* c1,
|
||||
secp256k1_pubkey const* c2,
|
||||
unsigned char const* privkey);
|
||||
|
||||
/**
|
||||
* @brief Homomorphically adds two ElGamal ciphertexts.
|
||||
*/
|
||||
SECP256K1_API int
|
||||
secp256k1_elgamal_add(
|
||||
secp256k1_context const* ctx,
|
||||
secp256k1_pubkey* sum_c1,
|
||||
secp256k1_pubkey* sum_c2,
|
||||
secp256k1_pubkey const* a_c1,
|
||||
secp256k1_pubkey const* a_c2,
|
||||
secp256k1_pubkey const* b_c1,
|
||||
secp256k1_pubkey const* b_c2);
|
||||
|
||||
/**
|
||||
* @brief Homomorphically subtracts two ElGamal ciphertexts.
|
||||
*/
|
||||
SECP256K1_API int
|
||||
secp256k1_elgamal_subtract(
|
||||
secp256k1_context const* ctx,
|
||||
secp256k1_pubkey* diff_c1,
|
||||
secp256k1_pubkey* diff_c2,
|
||||
secp256k1_pubkey const* a_c1,
|
||||
secp256k1_pubkey const* a_c2,
|
||||
secp256k1_pubkey const* b_c1,
|
||||
secp256k1_pubkey const* b_c2);
|
||||
|
||||
/**
|
||||
* @brief Generates the canonical encrypted zero for a given MPT token instance.
|
||||
*
|
||||
* This ciphertext represents a zero balance for a specific account's holding
|
||||
* of a token defined by its MPTokenIssuanceID.
|
||||
*
|
||||
* @param[in] ctx A pointer to a valid secp256k1 context.
|
||||
* @param[out] enc_zero_c1 The C1 component of the canonical ciphertext.
|
||||
* @param[out] enc_zero_c2 The C2 component of the canonical ciphertext.
|
||||
* @param[in] pubkey The ElGamal public key of the account holder.
|
||||
* @param[in] account_id A pointer to the 20-byte AccountID.
|
||||
* @param[in] mpt_issuance_id A pointer to the 24-byte MPTokenIssuanceID.
|
||||
*
|
||||
* @return 1 on success, 0 on failure.
|
||||
*/
|
||||
SECP256K1_API int
|
||||
generate_canonical_encrypted_zero(
|
||||
secp256k1_context const* ctx,
|
||||
secp256k1_pubkey* enc_zero_c1,
|
||||
secp256k1_pubkey* enc_zero_c2,
|
||||
secp256k1_pubkey const* pubkey,
|
||||
unsigned char const* account_id, // 20 bytes
|
||||
unsigned char const* mpt_issuance_id // 24 bytes
|
||||
);
|
||||
|
||||
// breaks a 66-byte encrypted amount into two 33-byte components
|
||||
// then parses each 33-byte component into 64-byte secp256k1_pubkey format
|
||||
bool
|
||||
makeEcPair(Slice const& buffer, secp256k1_pubkey& out1, secp256k1_pubkey& out2);
|
||||
|
||||
// serialize two secp256k1_pubkey components back into compressed 66-byte form
|
||||
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);
|
||||
|
||||
TER
|
||||
homomorphicAdd(Slice const& a, Slice const& b, Buffer& out);
|
||||
|
||||
TER
|
||||
homomorphicSubtract(Slice const& a, Slice const& b, Buffer& out);
|
||||
|
||||
TER
|
||||
proveEquality(
|
||||
Slice const& proof,
|
||||
Slice const& encAmt, // encrypted amount
|
||||
Slice const& pubkey,
|
||||
uint64_t const amount,
|
||||
uint256 const& txHash, // Transaction context data
|
||||
std::uint32_t const spendVersion);
|
||||
|
||||
Buffer
|
||||
encryptAmount(uint64_t amt, Slice const& pubKeySlice);
|
||||
|
||||
Buffer
|
||||
encryptCanonicalZeroAmount(
|
||||
Slice const& pubKeySlice,
|
||||
AccountID const& account,
|
||||
MPTID const& mptId);
|
||||
|
||||
TER
|
||||
verifyConfidentialSendProof(
|
||||
Slice const& proof,
|
||||
Slice const& encSenderBalance,
|
||||
Slice const& encSenderAmt,
|
||||
Slice const& encDestAmt,
|
||||
Slice const& encIssuerAmt,
|
||||
Slice const& senderPubKey,
|
||||
Slice const& destPubKey,
|
||||
Slice const& issuerPubKey,
|
||||
std::uint32_t const version,
|
||||
uint256 const& txHash);
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -187,6 +187,7 @@ enum LedgerSpecificFlags {
|
||||
lsfMPTCanTrade = 0x00000010,
|
||||
lsfMPTCanTransfer = 0x00000020,
|
||||
lsfMPTCanClawback = 0x00000040,
|
||||
lsfMPTNoConfidentialTransfer = 0x00000080,
|
||||
|
||||
lsmfMPTCanMutateCanLock = 0x00000002,
|
||||
lsmfMPTCanMutateRequireAuth = 0x00000004,
|
||||
|
||||
@@ -181,6 +181,20 @@ 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 */
|
||||
std::size_t constexpr ecEqualityProofLength = 98;
|
||||
|
||||
/** Length of EC public key */
|
||||
std::size_t constexpr ecPubKeyLength = 64;
|
||||
|
||||
/** Length of EC private key */
|
||||
std::size_t constexpr ecPrivKeyLength = 32;
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
|
||||
@@ -141,6 +141,7 @@ enum TEMcodes : TERUnderlyingType {
|
||||
temARRAY_TOO_LARGE,
|
||||
temBAD_TRANSFER_FEE,
|
||||
temINVALID_INNER_BATCH,
|
||||
temBAD_CIPHERTEXT,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -366,6 +367,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
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -151,8 +151,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 tfMPTNoConfidentialTransfer = lsfMPTNoConfidentialTransfer;
|
||||
constexpr std::uint32_t const tfMPTokenIssuanceCreateMask =
|
||||
~(tfUniversal | tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback);
|
||||
~(tfUniversal | tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback | tfMPTNoConfidentialTransfer);
|
||||
|
||||
// MPTokenIssuanceCreate MutableFlags:
|
||||
// Indicating specific fields or flags may be changed after issuance.
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
// Add new amendments to the top of this list.
|
||||
// Keep it sorted in reverse chronological order.
|
||||
|
||||
XRPL_FEATURE(ConfidentialTransfer, Supported::no, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(PermissionDelegationV1_1, Supported::no, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (DirectoryLimit, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (IncludeKeyletFields, Supported::yes, VoteBehavior::DefaultNo)
|
||||
|
||||
@@ -416,6 +416,8 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
||||
{sfDomainID, soeOPTIONAL},
|
||||
{sfMutableFlags, soeDEFAULT},
|
||||
{sfIssuerElGamalPublicKey, soeOPTIONAL},
|
||||
{sfConfidentialOutstandingAmount, soeDEFAULT},
|
||||
}))
|
||||
|
||||
/** A ledger object which tracks MPToken
|
||||
@@ -429,6 +431,11 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
|
||||
{sfOwnerNode, soeREQUIRED},
|
||||
{sfPreviousTxnID, soeREQUIRED},
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
||||
{sfConfidentialBalanceInbox, soeOPTIONAL},
|
||||
{sfConfidentialBalanceSpending, soeOPTIONAL},
|
||||
{sfConfidentialBalanceVersion, soeDEFAULT},
|
||||
{sfIssuerEncryptedBalance, soeOPTIONAL},
|
||||
{sfHolderElGamalPublicKey, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** A ledger object which tracks Oracle
|
||||
|
||||
@@ -115,6 +115,7 @@ TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50)
|
||||
TYPED_SFIELD(sfOracleDocumentID, UINT32, 51)
|
||||
TYPED_SFIELD(sfPermissionValue, UINT32, 52)
|
||||
TYPED_SFIELD(sfMutableFlags, UINT32, 53)
|
||||
TYPED_SFIELD(sfConfidentialBalanceVersion, UINT32, 54)
|
||||
|
||||
// 64-bit integers (common)
|
||||
TYPED_SFIELD(sfIndexNext, UINT64, 1)
|
||||
@@ -146,6 +147,7 @@ TYPED_SFIELD(sfMPTAmount, UINT64, 26, SField::sMD_BaseTen|SFie
|
||||
TYPED_SFIELD(sfIssuerNode, UINT64, 27)
|
||||
TYPED_SFIELD(sfSubjectNode, UINT64, 28)
|
||||
TYPED_SFIELD(sfLockedAmount, UINT64, 29, SField::sMD_BaseTen|SField::sMD_Default)
|
||||
TYPED_SFIELD(sfConfidentialOutstandingAmount, UINT64, 30, SField::sMD_BaseTen|SField::sMD_Default)
|
||||
|
||||
// 128-bit
|
||||
TYPED_SFIELD(sfEmailHash, UINT128, 1)
|
||||
@@ -284,6 +286,16 @@ 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)
|
||||
|
||||
// account (common)
|
||||
TYPED_SFIELD(sfAccount, ACCOUNT, 1)
|
||||
|
||||
@@ -741,6 +741,7 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet,
|
||||
{sfMPTokenMetadata, soeOPTIONAL},
|
||||
{sfTransferFee, soeOPTIONAL},
|
||||
{sfMutableFlags, soeOPTIONAL},
|
||||
{sfIssuerElGamalPublicKey, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** This transaction type authorizes a MPToken instance */
|
||||
@@ -944,6 +945,82 @@ TRANSACTION(ttBATCH, 71, Batch,
|
||||
{sfBatchSigners, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** This transaction type converts into confidential MPT balance. */
|
||||
#if TRANSACTION_INCLUDE
|
||||
#include <xrpld/app/tx/detail/ConfidentialConvert.h>
|
||||
#endif
|
||||
TRANSACTION(ttCONFIDENTIAL_CONVERT, 72, ConfidentialConvert,
|
||||
Delegation::delegatable,
|
||||
featureConfidentialTransfer,
|
||||
noPriv,
|
||||
({
|
||||
{sfMPTokenIssuanceID, soeREQUIRED},
|
||||
{sfMPTAmount, soeREQUIRED},
|
||||
{sfHolderElGamalPublicKey, soeOPTIONAL},
|
||||
{sfHolderEncryptedAmount, soeREQUIRED},
|
||||
{sfIssuerEncryptedAmount, soeREQUIRED},
|
||||
{sfZKProof, soeREQUIRED},
|
||||
}))
|
||||
|
||||
/** This transaction type merges MPT inbox. */
|
||||
#if TRANSACTION_INCLUDE
|
||||
#include <xrpld/app/tx/detail/ConfidentialMergeInbox.h>
|
||||
#endif
|
||||
TRANSACTION(ttCONFIDENTIAL_MERGE_INBOX, 73, ConfidentialMergeInbox,
|
||||
Delegation::delegatable,
|
||||
featureConfidentialTransfer,
|
||||
noPriv,
|
||||
({
|
||||
{sfMPTokenIssuanceID, soeREQUIRED},
|
||||
}))
|
||||
|
||||
/** This transaction type converts back into public MPT balance. */
|
||||
#if TRANSACTION_INCLUDE
|
||||
#include <xrpld/app/tx/detail/ConfidentialConvertBack.h>
|
||||
#endif
|
||||
TRANSACTION(ttCONFIDENTIAL_CONVERT_BACK, 74, ConfidentialConvertBack,
|
||||
Delegation::delegatable,
|
||||
featureConfidentialTransfer,
|
||||
noPriv,
|
||||
({
|
||||
{sfMPTokenIssuanceID, soeREQUIRED},
|
||||
{sfMPTAmount, soeREQUIRED},
|
||||
{sfHolderEncryptedAmount, soeREQUIRED},
|
||||
{sfIssuerEncryptedAmount, soeREQUIRED},
|
||||
{sfZKProof, soeREQUIRED},
|
||||
}))
|
||||
|
||||
#if TRANSACTION_INCLUDE
|
||||
#include <xrpld/app/tx/detail/ConfidentialSend.h>
|
||||
#endif
|
||||
TRANSACTION(ttCONFIDENTIAL_SEND, 75, ConfidentialSend,
|
||||
Delegation::delegatable,
|
||||
featureConfidentialTransfer,
|
||||
noPriv,
|
||||
({
|
||||
{sfMPTokenIssuanceID, soeREQUIRED},
|
||||
{sfDestination, soeREQUIRED},
|
||||
{sfSenderEncryptedAmount, soeREQUIRED},
|
||||
{sfDestinationEncryptedAmount, soeREQUIRED},
|
||||
{sfIssuerEncryptedAmount, soeREQUIRED},
|
||||
{sfZKProof, soeREQUIRED},
|
||||
{sfCredentialIDs, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
#if TRANSACTION_INCLUDE
|
||||
#include <xrpld/app/tx/detail/ConfidentialClawback.h>
|
||||
#endif
|
||||
TRANSACTION(ttCONFIDENTIAL_CLAWBACK, 76, ConfidentialClawback,
|
||||
Delegation::delegatable,
|
||||
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
|
||||
|
||||
@@ -504,7 +504,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))
|
||||
(view.rules().enabled(featureSingleAssetVault) ||
|
||||
view.rules().enabled(featureConfidentialTransfer)))
|
||||
{
|
||||
if (auto const err =
|
||||
requireAuth(view, mptIssue, account, AuthType::StrongAuth);
|
||||
|
||||
607
src/libxrpl/protocol/ConfidentialTransfer.cpp
Normal file
607
src/libxrpl/protocol/ConfidentialTransfer.cpp
Normal file
@@ -0,0 +1,607 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpl/protocol/ConfidentialTransfer.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <openssl/rand.h>
|
||||
#include <openssl/sha.h>
|
||||
|
||||
namespace ripple {
|
||||
int
|
||||
secp256k1_elgamal_generate_keypair(
|
||||
secp256k1_context const* ctx,
|
||||
unsigned char* privkey,
|
||||
secp256k1_pubkey* pubkey)
|
||||
{
|
||||
// 1. Generate 32 random bytes for the private key
|
||||
do
|
||||
{
|
||||
if (RAND_bytes(privkey, 32) != 1)
|
||||
{
|
||||
return 0; // Failure
|
||||
}
|
||||
// 2. Verify the random data is a valid private key.
|
||||
} while (secp256k1_ec_seckey_verify(ctx, privkey) != 1);
|
||||
|
||||
// 3. Create the corresponding public key.
|
||||
if (secp256k1_ec_pubkey_create(ctx, pubkey, privkey) != 1)
|
||||
{
|
||||
return 0; // Failure
|
||||
}
|
||||
|
||||
return 1; // Success
|
||||
}
|
||||
|
||||
// ... implementation of secp256k1_elgamal_encrypt ...
|
||||
|
||||
int
|
||||
secp256k1_elgamal_encrypt(
|
||||
secp256k1_context const* ctx,
|
||||
secp256k1_pubkey* c1,
|
||||
secp256k1_pubkey* c2,
|
||||
secp256k1_pubkey const* pubkey_Q,
|
||||
uint64_t amount,
|
||||
unsigned char const* blinding_factor)
|
||||
{
|
||||
secp256k1_pubkey S;
|
||||
|
||||
// First, calculate C1 = k * G
|
||||
if (secp256k1_ec_pubkey_create(ctx, c1, blinding_factor) != 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Next, calculate the shared secret S = k * Q
|
||||
S = *pubkey_Q;
|
||||
if (secp256k1_ec_pubkey_tweak_mul(ctx, &S, blinding_factor) != 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- Handle the amount ---
|
||||
if (amount == 0)
|
||||
{
|
||||
// For amount = 0, C2 = S.
|
||||
*c2 = S;
|
||||
}
|
||||
else
|
||||
{
|
||||
// For non-zero amounts, proceed as before.
|
||||
unsigned char amount_scalar[32] = {0};
|
||||
secp256k1_pubkey M;
|
||||
secp256k1_pubkey const* points_to_add[2];
|
||||
|
||||
// Convert amount to a 32-byte BIG-ENDIAN scalar.
|
||||
for (int i = 0; i < 8; ++i)
|
||||
{
|
||||
amount_scalar[31 - i] = (amount >> (i * 8)) & 0xFF;
|
||||
}
|
||||
|
||||
// Calculate M = amount * G
|
||||
if (secp256k1_ec_pubkey_create(ctx, &M, amount_scalar) != 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Calculate C2 = M + S
|
||||
points_to_add[0] = &M;
|
||||
points_to_add[1] = &S;
|
||||
if (secp256k1_ec_pubkey_combine(ctx, c2, points_to_add, 2) != 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 1; // Success
|
||||
}
|
||||
|
||||
// ... implementation of secp256k1_elgamal_decrypt ...
|
||||
int
|
||||
secp256k1_elgamal_decrypt(
|
||||
secp256k1_context const* ctx,
|
||||
uint64_t* amount,
|
||||
secp256k1_pubkey const* c1,
|
||||
secp256k1_pubkey const* c2,
|
||||
unsigned char const* privkey)
|
||||
{
|
||||
secp256k1_pubkey S, M, G_point, current_M, next_M;
|
||||
secp256k1_pubkey const* points_to_add[2];
|
||||
unsigned char c2_bytes[33], s_bytes[33], m_bytes[33], current_m_bytes[33];
|
||||
size_t len;
|
||||
uint64_t i;
|
||||
|
||||
/* Create the scalar '1' in big-endian format */
|
||||
unsigned char one_scalar[32] = {0};
|
||||
one_scalar[31] = 1;
|
||||
|
||||
/* --- Executable Code --- */
|
||||
|
||||
// 1. Calculate S = privkey * C1
|
||||
S = *c1;
|
||||
if (secp256k1_ec_pubkey_tweak_mul(ctx, &S, privkey) != 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 2. Check for amount = 0 by comparing serialized points
|
||||
len = sizeof(c2_bytes);
|
||||
if (secp256k1_ec_pubkey_serialize(
|
||||
ctx, c2_bytes, &len, c2, SECP256K1_EC_COMPRESSED) != 1)
|
||||
return 0;
|
||||
len = sizeof(s_bytes);
|
||||
if (secp256k1_ec_pubkey_serialize(
|
||||
ctx, s_bytes, &len, &S, SECP256K1_EC_COMPRESSED) != 1)
|
||||
return 0;
|
||||
if (memcmp(c2_bytes, s_bytes, sizeof(c2_bytes)) == 0)
|
||||
{
|
||||
*amount = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 3. Recover M = C2 - S
|
||||
if (secp256k1_ec_pubkey_negate(ctx, &S) != 1)
|
||||
return 0;
|
||||
points_to_add[0] = c2;
|
||||
points_to_add[1] = &S;
|
||||
if (secp256k1_ec_pubkey_combine(ctx, &M, points_to_add, 2) != 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 4. Serialize M once for comparison in the loop
|
||||
len = sizeof(m_bytes);
|
||||
if (secp256k1_ec_pubkey_serialize(
|
||||
ctx, m_bytes, &len, &M, SECP256K1_EC_COMPRESSED) != 1)
|
||||
return 0;
|
||||
|
||||
// 5. Brute-force search loop
|
||||
if (secp256k1_ec_pubkey_create(ctx, &G_point, one_scalar) != 1)
|
||||
return 0;
|
||||
current_M = G_point;
|
||||
|
||||
for (i = 1; i <= 1000000; ++i)
|
||||
{
|
||||
len = sizeof(current_m_bytes);
|
||||
if (secp256k1_ec_pubkey_serialize(
|
||||
ctx,
|
||||
current_m_bytes,
|
||||
&len,
|
||||
¤t_M,
|
||||
SECP256K1_EC_COMPRESSED) != 1)
|
||||
return 0;
|
||||
if (memcmp(m_bytes, current_m_bytes, sizeof(m_bytes)) == 0)
|
||||
{
|
||||
*amount = i;
|
||||
return 1;
|
||||
}
|
||||
|
||||
points_to_add[0] = ¤t_M;
|
||||
points_to_add[1] = &G_point;
|
||||
if (secp256k1_ec_pubkey_combine(ctx, &next_M, points_to_add, 2) != 1)
|
||||
return 0;
|
||||
current_M = next_M;
|
||||
}
|
||||
|
||||
return 0; // Not found
|
||||
}
|
||||
|
||||
int
|
||||
secp256k1_elgamal_add(
|
||||
secp256k1_context const* ctx,
|
||||
secp256k1_pubkey* sum_c1,
|
||||
secp256k1_pubkey* sum_c2,
|
||||
secp256k1_pubkey const* a_c1,
|
||||
secp256k1_pubkey const* a_c2,
|
||||
secp256k1_pubkey const* b_c1,
|
||||
secp256k1_pubkey const* b_c2)
|
||||
{
|
||||
secp256k1_pubkey const* c1_points[2] = {a_c1, b_c1};
|
||||
if (secp256k1_ec_pubkey_combine(ctx, sum_c1, c1_points, 2) != 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
secp256k1_pubkey const* c2_points[2] = {a_c2, b_c2};
|
||||
if (secp256k1_ec_pubkey_combine(ctx, sum_c2, c2_points, 2) != 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int
|
||||
secp256k1_elgamal_subtract(
|
||||
secp256k1_context const* ctx,
|
||||
secp256k1_pubkey* diff_c1,
|
||||
secp256k1_pubkey* diff_c2,
|
||||
secp256k1_pubkey const* a_c1,
|
||||
secp256k1_pubkey const* a_c2,
|
||||
secp256k1_pubkey const* b_c1,
|
||||
secp256k1_pubkey const* b_c2)
|
||||
{
|
||||
// To subtract, we add the negation: (A - B) is (A + (-B))
|
||||
// Make a local, modifiable copy of B's points.
|
||||
secp256k1_pubkey neg_b_c1 = *b_c1;
|
||||
secp256k1_pubkey neg_b_c2 = *b_c2;
|
||||
|
||||
// Negate the copies
|
||||
if (secp256k1_ec_pubkey_negate(ctx, &neg_b_c1) != 1 ||
|
||||
secp256k1_ec_pubkey_negate(ctx, &neg_b_c2) != 1)
|
||||
{
|
||||
return 0; // Negation failed
|
||||
}
|
||||
|
||||
// Now, add A and the negated copies of B
|
||||
secp256k1_pubkey const* c1_points[2] = {a_c1, &neg_b_c1};
|
||||
if (secp256k1_ec_pubkey_combine(ctx, diff_c1, c1_points, 2) != 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
secp256k1_pubkey const* c2_points[2] = {a_c2, &neg_b_c2};
|
||||
if (secp256k1_ec_pubkey_combine(ctx, diff_c2, c2_points, 2) != 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1; // Success
|
||||
}
|
||||
|
||||
// Helper function to concatenate data for hashing
|
||||
static void
|
||||
build_hash_input(
|
||||
unsigned char* output_buffer,
|
||||
size_t buffer_size,
|
||||
unsigned char const* account_id, // 20 bytes
|
||||
unsigned char const* mpt_issuance_id // 24 bytes
|
||||
)
|
||||
{
|
||||
char const* domain_separator = "EncZero";
|
||||
size_t domain_len = strlen(domain_separator);
|
||||
size_t offset = 0;
|
||||
|
||||
// Ensure buffer is large enough (should be checked by caller if necessary)
|
||||
// Size = strlen("EncZero") + 20 + 24 = 7 + 20 + 24 = 51 bytes
|
||||
|
||||
memcpy(output_buffer + offset, domain_separator, domain_len);
|
||||
offset += domain_len;
|
||||
|
||||
memcpy(output_buffer + offset, account_id, 20);
|
||||
offset += 20;
|
||||
|
||||
memcpy(output_buffer + offset, mpt_issuance_id, 24);
|
||||
// offset += 24; // Final size is offset + 24
|
||||
}
|
||||
|
||||
// The canonical encrypted zero
|
||||
|
||||
int
|
||||
generate_canonical_encrypted_zero(
|
||||
secp256k1_context const* ctx,
|
||||
secp256k1_pubkey* enc_zero_c1,
|
||||
secp256k1_pubkey* enc_zero_c2,
|
||||
secp256k1_pubkey const* pubkey,
|
||||
unsigned char const* account_id, // 20 bytes
|
||||
unsigned char const* mpt_issuance_id // 24 bytes
|
||||
)
|
||||
{
|
||||
unsigned char deterministic_scalar[32];
|
||||
unsigned char hash_input[51]; // Size calculated above
|
||||
|
||||
/* 1. Create the input buffer for hashing */
|
||||
build_hash_input(
|
||||
hash_input, sizeof(hash_input), account_id, mpt_issuance_id);
|
||||
|
||||
/* 2. Hash the buffer to create the deterministic scalar 'r' */
|
||||
do
|
||||
{
|
||||
// Hash the concatenated bytes
|
||||
SHA256(hash_input, sizeof(hash_input), deterministic_scalar);
|
||||
|
||||
/* Note: If the hash output could be invalid (0 or >= n),
|
||||
* you might need to add a nonce/counter to hash_input
|
||||
* and re-hash in a loop until a valid scalar is produced. */
|
||||
} while (secp256k1_ec_seckey_verify(ctx, deterministic_scalar) != 1);
|
||||
|
||||
/* 3. Encrypt the amount 0 using the deterministic scalar */
|
||||
return secp256k1_elgamal_encrypt(
|
||||
ctx,
|
||||
enc_zero_c1,
|
||||
enc_zero_c2,
|
||||
pubkey,
|
||||
0, /* The amount is zero */
|
||||
deterministic_scalar);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
// Local/temporary variables to pass to makeEcPair.
|
||||
// Their contents will be discarded when the function returns.
|
||||
secp256k1_pubkey key1;
|
||||
secp256k1_pubkey key2;
|
||||
|
||||
// Call makeEcPair and return its result.
|
||||
return makeEcPair(buffer, key1, key2);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
TER
|
||||
proveEquality(
|
||||
Slice const& proof,
|
||||
Slice const& encAmt, // encrypted amount
|
||||
Slice const& pubkey,
|
||||
uint64_t const amount,
|
||||
uint256 const& txHash, // Transaction context data
|
||||
std::uint32_t const spendVersion)
|
||||
{
|
||||
if (proof.length() != ecEqualityProofLength)
|
||||
return tecINTERNAL;
|
||||
|
||||
secp256k1_pubkey c1;
|
||||
secp256k1_pubkey c2;
|
||||
|
||||
if (!makeEcPair(encAmt, c1, c2))
|
||||
return tecINTERNAL;
|
||||
|
||||
// todo: might need to change how its hashed
|
||||
Serializer s;
|
||||
s.addRaw(txHash.data(), txHash.bytes);
|
||||
s.add32(spendVersion);
|
||||
// auto const txContextId = s.getSHA512Half();
|
||||
|
||||
// todo: support equality
|
||||
// if (secp256k1_equality_verify(
|
||||
// secp256k1Context(),
|
||||
// reinterpret_cast<unsigned char const*>(proof.data()),
|
||||
// proof.length(), // Length of the proof byte array (98 bytes)
|
||||
// &c1,
|
||||
// &c2,
|
||||
// reinterpret_cast<unsigned char const*>(pubkey.data()),
|
||||
// amount,
|
||||
// txContextId.data(), // Transaction context data
|
||||
// txContextId.bytes // Length of context data
|
||||
// ) != 1)
|
||||
// return tecBAD_PROOF;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
Buffer
|
||||
encryptAmount(uint64_t amt, Slice const& pubKeySlice)
|
||||
{
|
||||
Buffer buf(ecGamalEncryptedTotalLength);
|
||||
|
||||
// Allocate ciphertext placeholders
|
||||
secp256k1_pubkey c1, c2;
|
||||
|
||||
// todo: might need to be updated using another RNG
|
||||
// Prepare a random blinding factor
|
||||
unsigned char blindingFactor[32];
|
||||
if (RAND_bytes(blindingFactor, 32) != 1)
|
||||
Throw<std::runtime_error>("Failed to generate random number");
|
||||
|
||||
secp256k1_pubkey pubKey;
|
||||
|
||||
std::memcpy(pubKey.data, pubKeySlice.data(), ecPubKeyLength);
|
||||
|
||||
// Encrypt the amount
|
||||
if (!secp256k1_elgamal_encrypt(
|
||||
secp256k1Context(), &c1, &c2, &pubKey, amt, blindingFactor))
|
||||
Throw<std::runtime_error>("Failed to encrypt amount");
|
||||
|
||||
// Serialize the ciphertext pair into the buffer
|
||||
if (!serializeEcPair(c1, c2, buf))
|
||||
Throw<std::runtime_error>(
|
||||
"Failed to serialize into 66 byte compressed format");
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
Buffer
|
||||
encryptCanonicalZeroAmount(
|
||||
Slice const& pubKeySlice,
|
||||
AccountID const& account,
|
||||
MPTID const& mptId)
|
||||
{
|
||||
Buffer buf(ecGamalEncryptedTotalLength);
|
||||
|
||||
// Allocate ciphertext placeholders
|
||||
secp256k1_pubkey c1, c2;
|
||||
secp256k1_pubkey pubKey;
|
||||
|
||||
std::memcpy(pubKey.data, pubKeySlice.data(), ecPubKeyLength);
|
||||
|
||||
// Encrypt the amount
|
||||
if (!generate_canonical_encrypted_zero(
|
||||
secp256k1Context(),
|
||||
&c1,
|
||||
&c2,
|
||||
&pubKey,
|
||||
account.data(),
|
||||
mptId.data()))
|
||||
Throw<std::runtime_error>("Failed to encrypt amount");
|
||||
|
||||
// Serialize the ciphertext pair into the buffer
|
||||
if (!serializeEcPair(c1, c2, buf))
|
||||
Throw<std::runtime_error>(
|
||||
"Failed to serialize into 66 byte compressed format");
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
TER
|
||||
verifyConfidentialSendProof(
|
||||
Slice const& proof,
|
||||
Slice const& encSenderBalance,
|
||||
Slice const& encSenderAmt,
|
||||
Slice const& encDestAmt,
|
||||
Slice const& encIssuerAmt,
|
||||
Slice const& senderPubKey,
|
||||
Slice const& destPubKey,
|
||||
Slice const& issuerPubKey,
|
||||
std::uint32_t const version,
|
||||
uint256 const& txHash)
|
||||
{
|
||||
// if (proof.length() != ecConfidentialSendProofLength)
|
||||
// return tecINTERNAL;
|
||||
|
||||
secp256k1_pubkey balC1, balC2;
|
||||
if (!makeEcPair(encSenderBalance, balC1, balC2))
|
||||
return tecINTERNAL;
|
||||
|
||||
secp256k1_pubkey senderC1, senderC2;
|
||||
if (!makeEcPair(encSenderAmt, senderC1, senderC2))
|
||||
return tecINTERNAL;
|
||||
|
||||
secp256k1_pubkey destC1, destC2;
|
||||
if (!makeEcPair(encDestAmt, destC1, destC2))
|
||||
return tecINTERNAL;
|
||||
|
||||
secp256k1_pubkey issuerC1, issuerC2;
|
||||
if (!makeEcPair(encIssuerAmt, issuerC1, issuerC2))
|
||||
return tecINTERNAL;
|
||||
|
||||
Serializer s;
|
||||
s.addRaw(txHash.data(), txHash.bytes);
|
||||
s.add32(version);
|
||||
// auto const txContextId = s.getSHA512Half();
|
||||
|
||||
// todo: equality and range proof verification
|
||||
// if (secp256k1_equal_range_verify(
|
||||
// secp256k1Context(),
|
||||
// reinterpret_cast<unsigned char const*>(proof.data()),
|
||||
// proof.length(),
|
||||
// txContextId.data(),
|
||||
// &balC1,
|
||||
// &balC2,
|
||||
// &senderC1,
|
||||
// &senderC2,
|
||||
// reinterpret_cast<unsigned char const*>(senderPubKey.data()),
|
||||
// &destC1,
|
||||
// &destC2,
|
||||
// reinterpret_cast<unsigned char const*>(destPubKey.data()),
|
||||
// &issuerC1,
|
||||
// &issuerC2,
|
||||
// reinterpret_cast<unsigned char const*>(issuerPubKey.data()),
|
||||
// txContextId.data(),
|
||||
// txContextId.bytes) != 1)
|
||||
// return tecBAD_PROOF;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
@@ -127,6 +127,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."),
|
||||
@@ -219,6 +220,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."),
|
||||
|
||||
2443
src/test/app/ConfidentialTransfer_test.cpp
Normal file
2443
src/test/app/ConfidentialTransfer_test.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@@ -19,13 +19,34 @@
|
||||
|
||||
#include <test/jtx.h>
|
||||
|
||||
#include <xrpl/protocol/ConfidentialTransfer.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include "test/jtx/mpt.h"
|
||||
#include <openssl/rand.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace jtx {
|
||||
|
||||
ripple::Buffer
|
||||
generatePlaceholderCiphertext()
|
||||
{
|
||||
Buffer buf(ecGamalEncryptedTotalLength);
|
||||
|
||||
buf.data()[0] = 0x02;
|
||||
buf.data()[ecGamalEncryptedLength] = 0x02;
|
||||
|
||||
buf.data()[ecGamalEncryptedLength - 1] = 0x01;
|
||||
buf.data()[ecGamalEncryptedTotalLength - 1] = 0x01;
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
void
|
||||
mptflags::operator()(Env& env) const
|
||||
{
|
||||
@@ -248,60 +269,79 @@ MPTTester::set(MPTSet const& arg)
|
||||
jv[sfTransferFee] = *arg.transferFee;
|
||||
if (arg.metadata)
|
||||
jv[sfMPTokenMetadata] = strHex(*arg.metadata);
|
||||
if (submit(arg, jv) == tesSUCCESS && (arg.flags || arg.mutableFlags))
|
||||
if (arg.pubKey)
|
||||
jv[sfIssuerElGamalPublicKey] = strHex(*arg.pubKey);
|
||||
if (submit(arg, jv) == tesSUCCESS)
|
||||
{
|
||||
auto require = [&](std::optional<Account> const& holder,
|
||||
bool unchanged) {
|
||||
auto flags = getFlags(holder);
|
||||
if (!unchanged)
|
||||
{
|
||||
if (arg.flags)
|
||||
if ((arg.flags || arg.mutableFlags))
|
||||
{
|
||||
auto require = [&](std::optional<Account> const& holder,
|
||||
bool unchanged) {
|
||||
auto flags = getFlags(holder);
|
||||
if (!unchanged)
|
||||
{
|
||||
if (*arg.flags & tfMPTLock)
|
||||
flags |= lsfMPTLocked;
|
||||
else if (*arg.flags & tfMPTUnlock)
|
||||
flags &= ~lsfMPTLocked;
|
||||
if (arg.flags)
|
||||
{
|
||||
if (*arg.flags & tfMPTLock)
|
||||
flags |= lsfMPTLocked;
|
||||
else if (*arg.flags & tfMPTUnlock)
|
||||
flags &= ~lsfMPTLocked;
|
||||
}
|
||||
|
||||
if (arg.mutableFlags)
|
||||
{
|
||||
if (*arg.mutableFlags & tmfMPTSetCanLock)
|
||||
flags |= lsfMPTCanLock;
|
||||
else if (*arg.mutableFlags & tmfMPTClearCanLock)
|
||||
flags &= ~lsfMPTCanLock;
|
||||
|
||||
if (*arg.mutableFlags & tmfMPTSetRequireAuth)
|
||||
flags |= lsfMPTRequireAuth;
|
||||
else if (*arg.mutableFlags & tmfMPTClearRequireAuth)
|
||||
flags &= ~lsfMPTRequireAuth;
|
||||
|
||||
if (*arg.mutableFlags & tmfMPTSetCanEscrow)
|
||||
flags |= lsfMPTCanEscrow;
|
||||
else if (*arg.mutableFlags & tmfMPTClearCanEscrow)
|
||||
flags &= ~lsfMPTCanEscrow;
|
||||
|
||||
if (*arg.mutableFlags & tmfMPTSetCanClawback)
|
||||
flags |= lsfMPTCanClawback;
|
||||
else if (*arg.mutableFlags & tmfMPTClearCanClawback)
|
||||
flags &= ~lsfMPTCanClawback;
|
||||
|
||||
if (*arg.mutableFlags & tmfMPTSetCanTrade)
|
||||
flags |= lsfMPTCanTrade;
|
||||
else if (*arg.mutableFlags & tmfMPTClearCanTrade)
|
||||
flags &= ~lsfMPTCanTrade;
|
||||
|
||||
if (*arg.mutableFlags & tmfMPTSetCanTransfer)
|
||||
flags |= lsfMPTCanTransfer;
|
||||
else if (*arg.mutableFlags & tmfMPTClearCanTransfer)
|
||||
flags &= ~lsfMPTCanTransfer;
|
||||
}
|
||||
}
|
||||
env_.require(mptflags(*this, flags, holder));
|
||||
};
|
||||
if (arg.account)
|
||||
require(std::nullopt, arg.holder.has_value());
|
||||
if (arg.holder)
|
||||
require(*arg.holder, false);
|
||||
}
|
||||
|
||||
if (arg.mutableFlags)
|
||||
{
|
||||
if (*arg.mutableFlags & tmfMPTSetCanLock)
|
||||
flags |= lsfMPTCanLock;
|
||||
else if (*arg.mutableFlags & tmfMPTClearCanLock)
|
||||
flags &= ~lsfMPTCanLock;
|
||||
|
||||
if (*arg.mutableFlags & tmfMPTSetRequireAuth)
|
||||
flags |= lsfMPTRequireAuth;
|
||||
else if (*arg.mutableFlags & tmfMPTClearRequireAuth)
|
||||
flags &= ~lsfMPTRequireAuth;
|
||||
|
||||
if (*arg.mutableFlags & tmfMPTSetCanEscrow)
|
||||
flags |= lsfMPTCanEscrow;
|
||||
else if (*arg.mutableFlags & tmfMPTClearCanEscrow)
|
||||
flags &= ~lsfMPTCanEscrow;
|
||||
|
||||
if (*arg.mutableFlags & tmfMPTSetCanClawback)
|
||||
flags |= lsfMPTCanClawback;
|
||||
else if (*arg.mutableFlags & tmfMPTClearCanClawback)
|
||||
flags &= ~lsfMPTCanClawback;
|
||||
|
||||
if (*arg.mutableFlags & tmfMPTSetCanTrade)
|
||||
flags |= lsfMPTCanTrade;
|
||||
else if (*arg.mutableFlags & tmfMPTClearCanTrade)
|
||||
flags &= ~lsfMPTCanTrade;
|
||||
|
||||
if (*arg.mutableFlags & tmfMPTSetCanTransfer)
|
||||
flags |= lsfMPTCanTransfer;
|
||||
else if (*arg.mutableFlags & tmfMPTClearCanTransfer)
|
||||
flags &= ~lsfMPTCanTransfer;
|
||||
}
|
||||
}
|
||||
env_.require(mptflags(*this, flags, holder));
|
||||
};
|
||||
if (arg.account)
|
||||
require(std::nullopt, arg.holder.has_value());
|
||||
if (arg.holder)
|
||||
require(*arg.holder, false);
|
||||
if (arg.pubKey)
|
||||
{
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return forObject([&](SLEP const& sle) -> bool {
|
||||
if (sle)
|
||||
{
|
||||
return strHex((*sle)[sfIssuerElGamalPublicKey]) ==
|
||||
strHex(getPubKey(issuer_));
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,6 +369,17 @@ MPTTester::checkDomainID(std::optional<uint256> expected) const
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
MPTTester::printMPT(Account const& holder_) const
|
||||
{
|
||||
return forObject(
|
||||
[&](SLEP const& sle) -> bool {
|
||||
std::cout << "\n" << sle->getJson();
|
||||
return true;
|
||||
},
|
||||
holder_);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
MPTTester::checkMPTokenAmount(
|
||||
Account const& holder_,
|
||||
@@ -347,6 +398,15 @@ MPTTester::checkMPTokenOutstandingAmount(std::int64_t expectedAmount) const
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
MPTTester::checkIssuanceConfidentialBalance(std::int64_t expectedAmount) const
|
||||
{
|
||||
return forObject([&](SLEP const& sle) {
|
||||
return expectedAmount ==
|
||||
(*sle)[~sfConfidentialOutstandingAmount].value_or(0);
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] bool
|
||||
MPTTester::checkFlags(
|
||||
uint32_t const expectedFlags,
|
||||
@@ -492,6 +552,48 @@ MPTTester::getBalance(Account const& account) const
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::int64_t
|
||||
MPTTester::getIssuanceConfidentialBalance() const
|
||||
{
|
||||
if (!id_)
|
||||
Throw<std::runtime_error>("MPT has not been created");
|
||||
|
||||
if (auto const sle = env_.le(keylet::mptIssuance(*id_)))
|
||||
return (*sle)[~sfConfidentialOutstandingAmount].value_or(0);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::optional<Buffer>
|
||||
MPTTester::getEncryptedBalance(
|
||||
Account const& account,
|
||||
EncryptedBalanceType option) const
|
||||
{
|
||||
if (!id_)
|
||||
Throw<std::runtime_error>("MPT has not been created");
|
||||
|
||||
if (auto const sle = env_.le(keylet::mptoken(*id_, account.id())))
|
||||
{
|
||||
if (option == HOLDER_ENCRYPTED_INBOX &&
|
||||
sle->isFieldPresent(sfConfidentialBalanceInbox))
|
||||
return Buffer(
|
||||
(*sle)[sfConfidentialBalanceInbox].data(),
|
||||
(*sle)[sfConfidentialBalanceInbox].size());
|
||||
if (option == HOLDER_ENCRYPTED_SPENDING &&
|
||||
sle->isFieldPresent(sfConfidentialBalanceSpending))
|
||||
return Buffer(
|
||||
(*sle)[sfConfidentialBalanceSpending].data(),
|
||||
(*sle)[sfConfidentialBalanceSpending].size());
|
||||
if (option == ISSUER_ENCRYPTED_BALANCE &&
|
||||
sle->isFieldPresent(sfIssuerEncryptedBalance))
|
||||
return Buffer(
|
||||
(*sle)[sfIssuerEncryptedBalance].data(),
|
||||
(*sle)[sfIssuerEncryptedBalance].size());
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
MPTTester::getFlags(std::optional<Account> const& holder) const
|
||||
{
|
||||
@@ -512,6 +614,533 @@ MPTTester::operator[](std::string const& name) const
|
||||
return MPT(name, issuanceID());
|
||||
}
|
||||
|
||||
void
|
||||
MPTTester::convert(MPTConvert const& arg)
|
||||
{
|
||||
Json::Value jv;
|
||||
if (arg.account)
|
||||
jv[sfAccount] = arg.account->human();
|
||||
else
|
||||
Throw<std::runtime_error>("Account not specified");
|
||||
|
||||
jv[jss::TransactionType] = jss::ConfidentialConvert;
|
||||
if (arg.id)
|
||||
jv[sfMPTokenIssuanceID] = to_string(*arg.id);
|
||||
else
|
||||
{
|
||||
if (!id_)
|
||||
Throw<std::runtime_error>("MPT has not been created");
|
||||
jv[sfMPTokenIssuanceID] = to_string(*id_);
|
||||
}
|
||||
|
||||
if (arg.amt)
|
||||
jv[sfMPTAmount.jsonName] = std::to_string(*arg.amt);
|
||||
if (arg.holderPubKey)
|
||||
jv[sfHolderElGamalPublicKey.jsonName] = strHex(*arg.holderPubKey);
|
||||
|
||||
if (arg.holderEncryptedAmt)
|
||||
jv[sfHolderEncryptedAmount.jsonName] = strHex(*arg.holderEncryptedAmt);
|
||||
else
|
||||
jv[sfHolderEncryptedAmount.jsonName] =
|
||||
strHex(encryptAmount(*arg.account, *arg.amt));
|
||||
|
||||
if (arg.issuerEncryptedAmt)
|
||||
jv[sfIssuerEncryptedAmount.jsonName] = strHex(*arg.issuerEncryptedAmt);
|
||||
else
|
||||
jv[sfIssuerEncryptedAmount.jsonName] =
|
||||
strHex(encryptAmount(issuer_, *arg.amt));
|
||||
|
||||
if (arg.proof)
|
||||
jv[sfZKProof.jsonName] = *arg.proof;
|
||||
|
||||
auto const holderAmt = getBalance(*arg.account);
|
||||
auto const prevConfidentialOutstanding = getIssuanceConfidentialBalance();
|
||||
|
||||
uint64_t prevInboxBalance =
|
||||
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
|
||||
uint64_t prevSpendingBalance =
|
||||
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
||||
uint64_t prevIssuerBalance =
|
||||
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
|
||||
|
||||
if (submit(arg, jv) == tesSUCCESS)
|
||||
{
|
||||
auto const postConfidentialOutstanding =
|
||||
getIssuanceConfidentialBalance();
|
||||
env_.require(mptbalance(*this, *arg.account, holderAmt - *arg.amt));
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return prevConfidentialOutstanding + *arg.amt ==
|
||||
postConfidentialOutstanding;
|
||||
}));
|
||||
|
||||
uint64_t postInboxBalance =
|
||||
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
|
||||
uint64_t postIssuerBalance =
|
||||
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
|
||||
uint64_t postSpendingBalance =
|
||||
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
||||
|
||||
// spending balance should not change
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return postSpendingBalance == prevSpendingBalance;
|
||||
}));
|
||||
|
||||
// issuer's encrypted balance is updated correctly
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return prevIssuerBalance + *arg.amt == postIssuerBalance;
|
||||
}));
|
||||
|
||||
// holder's inbox balance is updated correctly
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return prevInboxBalance + *arg.amt == postInboxBalance;
|
||||
}));
|
||||
|
||||
// sum of holder's inbox and spending balance should equal to issuer's
|
||||
// encrypted balance
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return postInboxBalance + postSpendingBalance == postIssuerBalance;
|
||||
}));
|
||||
|
||||
if (arg.holderPubKey)
|
||||
{
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return forObject(
|
||||
[&](SLEP const& sle) -> bool {
|
||||
if (sle)
|
||||
{
|
||||
return strHex((*sle)[sfHolderElGamalPublicKey]) ==
|
||||
strHex(getPubKey(*arg.account));
|
||||
}
|
||||
return false;
|
||||
},
|
||||
*arg.account);
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
MPTTester::send(MPTConfidentialSend const& arg)
|
||||
{
|
||||
Json::Value jv;
|
||||
if (arg.account)
|
||||
jv[sfAccount] = arg.account->human();
|
||||
else
|
||||
Throw<std::runtime_error>("Account not specified");
|
||||
|
||||
if (arg.dest)
|
||||
jv[sfDestination] = arg.dest->human();
|
||||
else
|
||||
Throw<std::runtime_error>("Destination not specified");
|
||||
|
||||
jv[jss::TransactionType] = jss::ConfidentialSend;
|
||||
if (arg.id)
|
||||
jv[sfMPTokenIssuanceID] = to_string(*arg.id);
|
||||
else
|
||||
{
|
||||
if (!id_)
|
||||
Throw<std::runtime_error>("MPT has not been created");
|
||||
jv[sfMPTokenIssuanceID] = to_string(*id_);
|
||||
}
|
||||
|
||||
// Generate the encrypted amounts if not provided
|
||||
if (arg.senderEncryptedAmt)
|
||||
jv[sfSenderEncryptedAmount] = strHex(*arg.senderEncryptedAmt);
|
||||
else
|
||||
jv[sfSenderEncryptedAmount] =
|
||||
strHex(encryptAmount(*arg.account, *arg.amt));
|
||||
|
||||
if (arg.destEncryptedAmt)
|
||||
jv[sfDestinationEncryptedAmount] = strHex(*arg.destEncryptedAmt);
|
||||
else
|
||||
jv[sfDestinationEncryptedAmount] =
|
||||
strHex(encryptAmount(*arg.dest, *arg.amt));
|
||||
|
||||
if (arg.issuerEncryptedAmt)
|
||||
jv[sfIssuerEncryptedAmount] = strHex(*arg.issuerEncryptedAmt);
|
||||
else
|
||||
jv[sfIssuerEncryptedAmount] = strHex(encryptAmount(issuer_, *arg.amt));
|
||||
|
||||
if (arg.proof)
|
||||
jv[sfZKProof] = *arg.proof;
|
||||
|
||||
if (arg.credentials)
|
||||
{
|
||||
auto& arr(jv[sfCredentialIDs.jsonName] = Json::arrayValue);
|
||||
for (auto const& hash : *arg.credentials)
|
||||
arr.append(hash);
|
||||
}
|
||||
|
||||
auto const senderPubAmt = getBalance(*arg.account);
|
||||
auto const destPubAmt = getBalance(*arg.dest);
|
||||
auto const prevCOA = getIssuanceConfidentialBalance();
|
||||
auto const prevOA = getIssuanceOutstandingBalance();
|
||||
|
||||
// Sender's previous confidential state
|
||||
uint64_t prevSenderInbox =
|
||||
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
|
||||
uint64_t prevSenderSpending =
|
||||
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
||||
uint64_t prevSenderIssuer =
|
||||
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
|
||||
|
||||
// Destination's previous confidential state
|
||||
uint64_t prevDestInbox =
|
||||
getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_INBOX);
|
||||
uint64_t prevDestSpending =
|
||||
getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_SPENDING);
|
||||
uint64_t prevDestIssuer =
|
||||
getDecryptedBalance(*arg.dest, ISSUER_ENCRYPTED_BALANCE);
|
||||
|
||||
if (submit(arg, jv) == tesSUCCESS)
|
||||
{
|
||||
auto const postCOA = getIssuanceConfidentialBalance();
|
||||
auto const postOA = getIssuanceOutstandingBalance();
|
||||
|
||||
// Sender's post confidential state
|
||||
uint64_t postSenderInbox =
|
||||
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
|
||||
uint64_t postSenderSpending =
|
||||
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
||||
uint64_t postSenderIssuer =
|
||||
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
|
||||
|
||||
// Destination's post confidential state
|
||||
uint64_t postDestInbox =
|
||||
getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_INBOX);
|
||||
uint64_t postDestSpending =
|
||||
getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_SPENDING);
|
||||
uint64_t postDestIssuer =
|
||||
getDecryptedBalance(*arg.dest, ISSUER_ENCRYPTED_BALANCE);
|
||||
|
||||
// Public balances unchanged
|
||||
env_.require(mptbalance(*this, *arg.account, senderPubAmt));
|
||||
env_.require(mptbalance(*this, *arg.dest, destPubAmt));
|
||||
|
||||
// OA and COA unchanged
|
||||
env_.require(requireAny([&]() -> bool { return prevOA == postOA; }));
|
||||
env_.require(requireAny([&]() -> bool { return prevCOA == postCOA; }));
|
||||
|
||||
// Verify sender changes
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return prevSenderSpending >= *arg.amt &&
|
||||
postSenderSpending == prevSenderSpending - *arg.amt;
|
||||
}));
|
||||
env_.require(requireAny(
|
||||
[&]() -> bool { return postSenderInbox == prevSenderInbox; }));
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return prevSenderIssuer >= *arg.amt &&
|
||||
postSenderIssuer == prevSenderIssuer - *arg.amt;
|
||||
}));
|
||||
|
||||
// Verify destination changes
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return postDestInbox == prevDestInbox + *arg.amt;
|
||||
}));
|
||||
env_.require(requireAny(
|
||||
[&]() -> bool { return postDestSpending == prevDestSpending; }));
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return postDestIssuer == prevDestIssuer + *arg.amt;
|
||||
}));
|
||||
|
||||
// Cross checks
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return postSenderInbox + postSenderSpending == postSenderIssuer;
|
||||
}));
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return postDestInbox + postDestSpending == postDestIssuer;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
MPTTester::confidentialClaw(MPTConfidentialClawback const& arg)
|
||||
{
|
||||
Json::Value jv;
|
||||
auto const account = arg.account ? *arg.account : issuer_;
|
||||
jv[sfAccount] = account.human();
|
||||
|
||||
if (arg.holder)
|
||||
jv[sfHolder] = arg.holder->human();
|
||||
else
|
||||
Throw<std::runtime_error>("Holder not specified");
|
||||
|
||||
jv[jss::TransactionType] = jss::ConfidentialClawback;
|
||||
if (arg.id)
|
||||
jv[sfMPTokenIssuanceID] = to_string(*arg.id);
|
||||
else if (id_)
|
||||
jv[sfMPTokenIssuanceID] = to_string(*id_);
|
||||
else
|
||||
Throw<std::runtime_error>("MPT has not been created");
|
||||
|
||||
if (arg.amt)
|
||||
jv[sfMPTAmount] = std::to_string(*arg.amt);
|
||||
|
||||
if (arg.proof)
|
||||
jv[sfZKProof] = *arg.proof;
|
||||
|
||||
auto const holderPubAmt = getBalance(*arg.holder);
|
||||
auto const prevCOA = getIssuanceConfidentialBalance();
|
||||
auto const prevOA = getIssuanceOutstandingBalance();
|
||||
|
||||
if (submit(arg, jv) == tesSUCCESS)
|
||||
{
|
||||
auto const postCOA = getIssuanceConfidentialBalance();
|
||||
auto const postOA = getIssuanceOutstandingBalance();
|
||||
|
||||
// Verify holder's public balance is unchanged
|
||||
env_.require(mptbalance(*this, *arg.holder, holderPubAmt));
|
||||
|
||||
// Verify COA and OA are reduced correctly
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return prevCOA >= *arg.amt && postCOA == prevCOA - *arg.amt;
|
||||
}));
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return prevOA >= *arg.amt && postOA == prevOA - *arg.amt;
|
||||
}));
|
||||
|
||||
// Verify holder's confidential balances are zeroed out
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return getDecryptedBalance(*arg.holder, HOLDER_ENCRYPTED_INBOX) ==
|
||||
0;
|
||||
}));
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return getDecryptedBalance(
|
||||
*arg.holder, HOLDER_ENCRYPTED_SPENDING) == 0;
|
||||
}));
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return getDecryptedBalance(*arg.holder, ISSUER_ENCRYPTED_BALANCE) ==
|
||||
0;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
MPTTester::generateKeyPair(Account const& account)
|
||||
{
|
||||
unsigned char privKey[ecPrivKeyLength];
|
||||
secp256k1_pubkey pubKey;
|
||||
if (!secp256k1_elgamal_generate_keypair(
|
||||
secp256k1Context(), privKey, &pubKey))
|
||||
Throw<std::runtime_error>("failed to generate key pair");
|
||||
|
||||
pubKeys.insert({account.id(), Buffer{pubKey.data, ecPubKeyLength}});
|
||||
privKeys.insert({account.id(), Buffer{privKey, ecPrivKeyLength}});
|
||||
}
|
||||
|
||||
Buffer
|
||||
MPTTester::getPubKey(Account const& account) const
|
||||
{
|
||||
auto it = pubKeys.find(account.id());
|
||||
if (it != pubKeys.end())
|
||||
{
|
||||
return it->second;
|
||||
}
|
||||
|
||||
Throw<std::runtime_error>("Account does not have public key");
|
||||
}
|
||||
|
||||
Buffer
|
||||
MPTTester::getPrivKey(Account const& account) const
|
||||
{
|
||||
auto it = privKeys.find(account.id());
|
||||
if (it != privKeys.end())
|
||||
{
|
||||
return it->second;
|
||||
}
|
||||
|
||||
Throw<std::runtime_error>("Account does not have private key");
|
||||
}
|
||||
|
||||
Buffer
|
||||
MPTTester::encryptAmount(Account const& account, uint64_t amt) const
|
||||
{
|
||||
return ripple::encryptAmount(amt, getPubKey(account));
|
||||
}
|
||||
|
||||
uint64_t
|
||||
MPTTester::decryptAmount(Account const& account, Buffer const& amt) const
|
||||
{
|
||||
secp256k1_pubkey c1;
|
||||
secp256k1_pubkey c2;
|
||||
|
||||
uint64_t decryptedAmt;
|
||||
|
||||
if (!makeEcPair(amt, c1, c2))
|
||||
Throw<std::runtime_error>(
|
||||
"Failed to convert into individual EC components");
|
||||
|
||||
if (!secp256k1_elgamal_decrypt(
|
||||
secp256k1Context(),
|
||||
&decryptedAmt,
|
||||
&c1,
|
||||
&c2,
|
||||
getPrivKey(account).data()))
|
||||
Throw<std::runtime_error>("Failed to decrypt amount");
|
||||
|
||||
return decryptedAmt;
|
||||
}
|
||||
|
||||
uint64_t
|
||||
MPTTester::getDecryptedBalance(
|
||||
Account const& account,
|
||||
EncryptedBalanceType balanceType) const
|
||||
|
||||
{
|
||||
auto maybeEncrypted = getEncryptedBalance(account, balanceType);
|
||||
auto accountToDecrypt =
|
||||
balanceType == ISSUER_ENCRYPTED_BALANCE ? issuer_ : account;
|
||||
return maybeEncrypted ? decryptAmount(accountToDecrypt, *maybeEncrypted)
|
||||
: 0;
|
||||
};
|
||||
|
||||
void
|
||||
MPTTester::mergeInbox(MPTMergeInbox const& arg)
|
||||
{
|
||||
Json::Value jv;
|
||||
if (arg.account)
|
||||
jv[sfAccount] = arg.account->human();
|
||||
else
|
||||
Throw<std::runtime_error>("Account not specified");
|
||||
if (arg.id)
|
||||
jv[sfMPTokenIssuanceID] = to_string(*arg.id);
|
||||
else
|
||||
{
|
||||
if (!id_)
|
||||
Throw<std::runtime_error>("MPT has not been created");
|
||||
jv[sfMPTokenIssuanceID] = to_string(*id_);
|
||||
}
|
||||
jv[sfTransactionType] = jss::ConfidentialMergeInbox;
|
||||
uint64_t prevInboxBalance =
|
||||
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
|
||||
uint64_t prevSpendingBalance =
|
||||
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
||||
uint64_t prevIssuerBalance =
|
||||
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
|
||||
if (submit(arg, jv) == tesSUCCESS)
|
||||
{
|
||||
uint64_t postInboxBalance =
|
||||
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
|
||||
uint64_t postSpendingBalance =
|
||||
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
||||
uint64_t postIssuerBalance =
|
||||
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
|
||||
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return postSpendingBalance ==
|
||||
prevInboxBalance + prevSpendingBalance &&
|
||||
postInboxBalance == 0;
|
||||
}));
|
||||
|
||||
env_.require(requireAny(
|
||||
[&]() -> bool { return prevIssuerBalance == postIssuerBalance; }));
|
||||
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return postSpendingBalance + postInboxBalance == postIssuerBalance;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
std::int64_t
|
||||
MPTTester::getIssuanceOutstandingBalance() const
|
||||
{
|
||||
if (!id_)
|
||||
Throw<std::runtime_error>("Issuance ID does not exist");
|
||||
|
||||
auto const sle = env_.current()->read(keylet::mptIssuance(*id_));
|
||||
|
||||
if (!sle || !sle->isFieldPresent(sfOutstandingAmount))
|
||||
Throw<std::runtime_error>(
|
||||
"Issuance object does not contain outstanding amount");
|
||||
|
||||
return (*sle)[sfOutstandingAmount];
|
||||
}
|
||||
|
||||
void
|
||||
MPTTester::convertBack(MPTConvertBack const& arg)
|
||||
{
|
||||
Json::Value jv;
|
||||
if (arg.account)
|
||||
jv[sfAccount] = arg.account->human();
|
||||
else
|
||||
Throw<std::runtime_error>("Account not specified");
|
||||
|
||||
jv[jss::TransactionType] = jss::ConfidentialConvertBack;
|
||||
if (arg.id)
|
||||
jv[sfMPTokenIssuanceID] = to_string(*arg.id);
|
||||
else
|
||||
{
|
||||
if (!id_)
|
||||
Throw<std::runtime_error>("MPT has not been created");
|
||||
jv[sfMPTokenIssuanceID] = to_string(*id_);
|
||||
}
|
||||
|
||||
if (arg.amt)
|
||||
jv[sfMPTAmount.jsonName] = std::to_string(*arg.amt);
|
||||
if (arg.holderEncryptedAmt)
|
||||
jv[sfHolderEncryptedAmount.jsonName] = strHex(*arg.holderEncryptedAmt);
|
||||
else
|
||||
jv[sfHolderEncryptedAmount.jsonName] =
|
||||
strHex(encryptAmount(*arg.account, *arg.amt));
|
||||
|
||||
if (arg.issuerEncryptedAmt)
|
||||
jv[sfIssuerEncryptedAmount.jsonName] = strHex(*arg.issuerEncryptedAmt);
|
||||
else
|
||||
jv[sfIssuerEncryptedAmount.jsonName] =
|
||||
strHex(encryptAmount(issuer_, *arg.amt));
|
||||
|
||||
if (arg.proof)
|
||||
jv[sfZKProof.jsonName] = *arg.proof;
|
||||
|
||||
auto const holderAmt = getBalance(*arg.account);
|
||||
auto const prevConfidentialOutstanding = getIssuanceConfidentialBalance();
|
||||
|
||||
uint64_t prevInboxBalance =
|
||||
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
|
||||
uint64_t prevSpendingBalance =
|
||||
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
||||
uint64_t prevIssuerBalance =
|
||||
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
|
||||
|
||||
if (submit(arg, jv) == tesSUCCESS)
|
||||
{
|
||||
auto const postConfidentialOutstanding =
|
||||
getIssuanceConfidentialBalance();
|
||||
env_.require(mptbalance(*this, *arg.account, holderAmt + *arg.amt));
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return prevConfidentialOutstanding - *arg.amt ==
|
||||
postConfidentialOutstanding;
|
||||
}));
|
||||
|
||||
uint64_t postInboxBalance =
|
||||
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
|
||||
uint64_t postIssuerBalance =
|
||||
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
|
||||
uint64_t postSpendingBalance =
|
||||
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
|
||||
|
||||
// inbox balance should not change
|
||||
env_.require(requireAny(
|
||||
[&]() -> bool { return postInboxBalance == prevInboxBalance; }));
|
||||
|
||||
// issuer's encrypted balance is updated correctly
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return prevIssuerBalance - *arg.amt == postIssuerBalance;
|
||||
}));
|
||||
|
||||
// holder's spending balance is updated correctly
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return prevSpendingBalance - *arg.amt == postSpendingBalance;
|
||||
}));
|
||||
|
||||
// sum of holder's inbox and spending balance should equal to issuer's
|
||||
// encrypted balance
|
||||
env_.require(requireAny([&]() -> bool {
|
||||
return postInboxBalance + postSpendingBalance == postIssuerBalance;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace jtx
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
|
||||
@@ -27,12 +27,18 @@
|
||||
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace jtx {
|
||||
|
||||
class MPTTester;
|
||||
|
||||
// Generates a syntactically valid placeholder ciphertext
|
||||
ripple::Buffer
|
||||
generatePlaceholderCiphertext();
|
||||
|
||||
// Check flags settings on MPT create
|
||||
class mptflags
|
||||
{
|
||||
@@ -145,6 +151,77 @@ struct MPTSet
|
||||
std::optional<std::string> metadata = std::nullopt;
|
||||
std::optional<Account> delegate = std::nullopt;
|
||||
std::optional<uint256> domainID = std::nullopt;
|
||||
std::optional<Buffer> pubKey = 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<Buffer> holderPubKey = std::nullopt;
|
||||
std::optional<Buffer> holderEncryptedAmt = std::nullopt;
|
||||
std::optional<Buffer> issuerEncryptedAmt = 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<std::vector<std::string>> credentials = 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<std::string> proof = std::nullopt;
|
||||
std::optional<Buffer> holderEncryptedAmt = std::nullopt;
|
||||
std::optional<Buffer> issuerEncryptedAmt = 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;
|
||||
};
|
||||
|
||||
@@ -155,8 +232,16 @@ class MPTTester
|
||||
std::unordered_map<std::string, Account> const holders_;
|
||||
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,
|
||||
};
|
||||
|
||||
MPTTester(Env& env, Account const& issuer, MPTInit const& constr = {});
|
||||
|
||||
void
|
||||
@@ -171,6 +256,22 @@ public:
|
||||
void
|
||||
set(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;
|
||||
|
||||
@@ -181,6 +282,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,
|
||||
@@ -234,9 +338,43 @@ 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;
|
||||
|
||||
bool
|
||||
printMPT(Account const& holder_) const;
|
||||
|
||||
void
|
||||
generateKeyPair(Account const& account);
|
||||
|
||||
Buffer
|
||||
getPubKey(Account const& account) const;
|
||||
|
||||
Buffer
|
||||
getPrivKey(Account const& account) const;
|
||||
|
||||
Buffer
|
||||
encryptAmount(Account const& account, uint64_t amt) const;
|
||||
|
||||
uint64_t
|
||||
decryptAmount(Account const& account, Buffer const& amt) const;
|
||||
|
||||
uint64_t
|
||||
getDecryptedBalance(
|
||||
Account const& account,
|
||||
EncryptedBalanceType balanceType) const;
|
||||
|
||||
std::int64_t
|
||||
getIssuanceOutstandingBalance() const;
|
||||
|
||||
private:
|
||||
using SLEP = std::shared_ptr<SLE const>;
|
||||
bool
|
||||
|
||||
170
src/xrpld/app/tx/detail/ConfidentialClawback.cpp
Normal file
170
src/xrpld/app/tx/detail/ConfidentialClawback.cpp
Normal file
@@ -0,0 +1,170 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/misc/DelegateUtils.h>
|
||||
#include <xrpld/app/tx/detail/ConfidentialClawback.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 ripple {
|
||||
|
||||
NotTEC
|
||||
ConfidentialClawback::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();
|
||||
|
||||
// Only issuer can clawback
|
||||
if (account != issuer)
|
||||
return temMALFORMED;
|
||||
|
||||
// Cannot clawback from self
|
||||
if (account == ctx.tx[sfHolder])
|
||||
return temMALFORMED;
|
||||
|
||||
auto const clawAmount = ctx.tx[sfMPTAmount];
|
||||
if (clawAmount == 0 || clawAmount > maxMPTokenAmount)
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
// if (ctx.tx[sfZKProof].length() != ecEqualityProofLength)
|
||||
// return temMALFORMED;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialClawback::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
|
||||
if (ctx.tx[sfMPTAmount] >
|
||||
(*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0))
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
|
||||
// todo: ZKP Verification
|
||||
// verify the MPT amount to clawback is the holder's confidential balance
|
||||
|
||||
// if (!isTesSuccess(terProof))
|
||||
// return tecBAD_PROOF;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialClawback::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;
|
||||
|
||||
auto const clawAmount = ctx_.tx[sfMPTAmount];
|
||||
|
||||
Slice const holderPubKey = (*sleHolderMPToken)[sfHolderElGamalPublicKey];
|
||||
Slice const issuerPubKey = (*sleIssuance)[sfIssuerElGamalPublicKey];
|
||||
|
||||
// Encrypt zero amount
|
||||
Buffer encZeroForHolder;
|
||||
Buffer encZeroForIssuer;
|
||||
try
|
||||
{
|
||||
encZeroForHolder =
|
||||
encryptCanonicalZeroAmount(holderPubKey, holder, mptIssuanceID);
|
||||
|
||||
encZeroForIssuer =
|
||||
encryptCanonicalZeroAmount(issuerPubKey, holder, mptIssuanceID);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(ctx_.journal.error())
|
||||
<< "ConfidentialClawback: Failed to generate canonical zero: "
|
||||
<< e.what();
|
||||
return tecINTERNAL;
|
||||
}
|
||||
|
||||
// Set holder's confidential balances to encrypted zero
|
||||
(*sleHolderMPToken)[sfConfidentialBalanceInbox] = encZeroForHolder;
|
||||
(*sleHolderMPToken)[sfConfidentialBalanceSpending] = encZeroForHolder;
|
||||
(*sleHolderMPToken)[sfIssuerEncryptedBalance] = encZeroForIssuer;
|
||||
(*sleHolderMPToken)[sfConfidentialBalanceVersion] = 0;
|
||||
|
||||
// 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 ripple
|
||||
48
src/xrpld/app/tx/detail/ConfidentialClawback.h
Normal file
48
src/xrpld/app/tx/detail/ConfidentialClawback.h
Normal file
@@ -0,0 +1,48 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_TX_CONFIDENTIALCLAWSBACK_H_INCLUDED
|
||||
#define RIPPLE_TX_CONFIDENTIALCLAWSBACK_H_INCLUDED
|
||||
|
||||
#include <xrpld/app/tx/detail/Transactor.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class ConfidentialClawback : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
|
||||
|
||||
explicit ConfidentialClawback(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
235
src/xrpld/app/tx/detail/ConfidentialConvert.cpp
Normal file
235
src/xrpld/app/tx/detail/ConfidentialConvert.cpp
Normal file
@@ -0,0 +1,235 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/misc/DelegateUtils.h>
|
||||
#include <xrpld/app/tx/detail/ConfidentialConvert.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 ripple {
|
||||
|
||||
NotTEC
|
||||
ConfidentialConvert::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[sfHolderEncryptedAmount].length() !=
|
||||
ecGamalEncryptedTotalLength ||
|
||||
ctx.tx[sfIssuerEncryptedAmount].length() != ecGamalEncryptedTotalLength)
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
if (ctx.tx[sfMPTAmount] > maxMPTokenAmount)
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
if (!isValidCiphertext(ctx.tx[sfHolderEncryptedAmount]) ||
|
||||
!isValidCiphertext(ctx.tx[sfIssuerEncryptedAmount]))
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfHolderElGamalPublicKey) &&
|
||||
ctx.tx[sfHolderElGamalPublicKey].length() != ecPubKeyLength)
|
||||
return temMALFORMED;
|
||||
|
||||
// if (ctx.tx[sfZKProof].length() != ecEqualityProofLength)
|
||||
// return temMALFORMED;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialConvert::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
// ensure that issuance exists
|
||||
auto const sleIssuance =
|
||||
ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID]));
|
||||
if (!sleIssuance)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
if (sleIssuance->isFlag(lsfMPTNoConfidentialTransfer))
|
||||
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
|
||||
|
||||
// issuer has not uploaded their pub key yet
|
||||
if (!sleIssuance->isFieldPresent(sfIssuerElGamalPublicKey))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
auto const sleMptoken = ctx.view.read(
|
||||
keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], ctx.tx[sfAccount]));
|
||||
if (!sleMptoken)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
auto const mptIssue = MPTIssue{ctx.tx[sfMPTokenIssuanceID]};
|
||||
STAmount const mptAmount = STAmount(
|
||||
MPTAmount{static_cast<MPTAmount::value_type>(ctx.tx[sfMPTAmount])},
|
||||
mptIssue);
|
||||
if (accountHolds(
|
||||
ctx.view,
|
||||
ctx.tx[sfAccount],
|
||||
mptIssue,
|
||||
FreezeHandling::fhZERO_IF_FROZEN,
|
||||
AuthHandling::ahZERO_IF_UNAUTHORIZED,
|
||||
ctx.j) < mptAmount)
|
||||
{
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
}
|
||||
|
||||
// must have pk to convert
|
||||
if (!sleMptoken->isFieldPresent(sfHolderElGamalPublicKey) &&
|
||||
!ctx.tx.isFieldPresent(sfHolderElGamalPublicKey))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// can't update if there's already a pk
|
||||
if (sleMptoken->isFieldPresent(sfHolderElGamalPublicKey) &&
|
||||
ctx.tx.isFieldPresent(sfHolderElGamalPublicKey))
|
||||
return tecDUPLICATE;
|
||||
|
||||
// auto const holderPubKey = ctx.tx.isFieldPresent(sfHolderElGamalPublicKey)
|
||||
// ? ctx.tx[sfHolderElGamalPublicKey]
|
||||
// : (*sleMptoken)[sfHolderElGamalPublicKey];
|
||||
|
||||
// todo: check zkproof/well formed
|
||||
|
||||
// check equality proof
|
||||
// auto checkEqualityProof = [&](auto const& encryptedAmount,
|
||||
// auto const& pubKey) -> TER {
|
||||
// return proveEquality(
|
||||
// ctx.tx[sfZKProof],
|
||||
// encryptedAmount,
|
||||
// pubKey,
|
||||
// ctx.tx[sfMPTAmount],
|
||||
// ctx.tx.getTransactionID(),
|
||||
// (*sleMptoken)[~sfConfidentialBalanceVersion].value_or(0));
|
||||
// };
|
||||
|
||||
// if (!isTesSuccess(checkEqualityProof(
|
||||
// ctx.tx[sfHolderEncryptedAmount], holderPubKey)) ||
|
||||
// !isTesSuccess(checkEqualityProof(
|
||||
// ctx.tx[sfIssuerEncryptedAmount],
|
||||
// (*sleIssuance)[sfIssuerElGamalPublicKey])))
|
||||
// {
|
||||
// return tecBAD_PROOF;
|
||||
// }
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialConvert::doApply()
|
||||
{
|
||||
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
|
||||
|
||||
auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, account_));
|
||||
if (!sleMptoken)
|
||||
return tecINTERNAL;
|
||||
|
||||
auto sleIssuance = view().peek(keylet::mptIssuance(mptIssuanceID));
|
||||
if (!sleIssuance)
|
||||
return tecINTERNAL;
|
||||
|
||||
auto const amtToConvert = ctx_.tx[sfMPTAmount];
|
||||
auto const amt = (*sleMptoken)[~sfMPTAmount].value_or(0);
|
||||
|
||||
if (ctx_.tx.isFieldPresent(sfHolderElGamalPublicKey))
|
||||
(*sleMptoken)[sfHolderElGamalPublicKey] =
|
||||
ctx_.tx[sfHolderElGamalPublicKey];
|
||||
|
||||
(*sleMptoken)[sfMPTAmount] = amt - amtToConvert;
|
||||
(*sleIssuance)[sfConfidentialOutstandingAmount] =
|
||||
(*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0) +
|
||||
amtToConvert;
|
||||
|
||||
Slice const holderEc = ctx_.tx[sfHolderEncryptedAmount];
|
||||
Slice const issuerEc = ctx_.tx[sfIssuerEncryptedAmount];
|
||||
|
||||
// todo: we should check sfConfidentialBalanceSpending depending on if we
|
||||
// encrypt zero amount
|
||||
if (sleMptoken->isFieldPresent(sfIssuerEncryptedBalance) &&
|
||||
sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) &&
|
||||
sleMptoken->isFieldPresent(sfConfidentialBalanceSpending))
|
||||
{
|
||||
// homomorphically add holder's encrypted balance
|
||||
{
|
||||
Buffer sum(ecGamalEncryptedTotalLength);
|
||||
if (TER const ter = homomorphicAdd(
|
||||
holderEc, (*sleMptoken)[sfConfidentialBalanceInbox], sum);
|
||||
!isTesSuccess(ter))
|
||||
return tecINTERNAL;
|
||||
|
||||
(*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;
|
||||
|
||||
(*sleMptoken)[sfIssuerEncryptedBalance] = sum;
|
||||
}
|
||||
}
|
||||
else if (
|
||||
!sleMptoken->isFieldPresent(sfIssuerEncryptedBalance) &&
|
||||
!sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) &&
|
||||
!sleMptoken->isFieldPresent(sfConfidentialBalanceSpending))
|
||||
{
|
||||
(*sleMptoken)[sfConfidentialBalanceInbox] = holderEc;
|
||||
(*sleMptoken)[sfIssuerEncryptedBalance] = issuerEc;
|
||||
(*sleMptoken)[sfConfidentialBalanceVersion] = 0;
|
||||
|
||||
try
|
||||
{
|
||||
// encrypt sfConfidentialBalanceSpending with zero balance
|
||||
Buffer out;
|
||||
out = encryptAmount(0, (*sleMptoken)[sfHolderElGamalPublicKey]);
|
||||
(*sleMptoken)[sfConfidentialBalanceSpending] = out;
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
return tecINTERNAL;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// both sfIssuerEncryptedBalance and sfConfidentialBalanceInbox should
|
||||
// exist together
|
||||
return tecINTERNAL;
|
||||
}
|
||||
|
||||
view().update(sleIssuance);
|
||||
view().update(sleMptoken);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
48
src/xrpld/app/tx/detail/ConfidentialConvert.h
Normal file
48
src/xrpld/app/tx/detail/ConfidentialConvert.h
Normal file
@@ -0,0 +1,48 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_TX_CONFIDENTIALCONVERT_H_INCLUDED
|
||||
#define RIPPLE_TX_CONFIDENTIALCONVERT_H_INCLUDED
|
||||
|
||||
#include <xrpld/app/tx/detail/Transactor.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class ConfidentialConvert : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
|
||||
|
||||
explicit ConfidentialConvert(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
197
src/xrpld/app/tx/detail/ConfidentialConvertBack.cpp
Normal file
197
src/xrpld/app/tx/detail/ConfidentialConvertBack.cpp
Normal file
@@ -0,0 +1,197 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/tx/detail/ConfidentialConvertBack.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 ripple {
|
||||
|
||||
NotTEC
|
||||
ConfidentialConvertBack::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[sfHolderEncryptedAmount].length() !=
|
||||
ecGamalEncryptedTotalLength ||
|
||||
ctx.tx[sfIssuerEncryptedAmount].length() != ecGamalEncryptedTotalLength)
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
if (ctx.tx[sfMPTAmount] == 0 || ctx.tx[sfMPTAmount] > maxMPTokenAmount)
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
if (!isValidCiphertext(ctx.tx[sfHolderEncryptedAmount]) ||
|
||||
!isValidCiphertext(ctx.tx[sfIssuerEncryptedAmount]))
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
// todo: update with correct size of proof since it might also contain range
|
||||
// proof
|
||||
// if (ctx.tx[sfZKProof].length() != ecEqualityProofLength)
|
||||
// return temMALFORMED;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialConvertBack::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
// ensure that issuance exists
|
||||
auto const sleIssuance =
|
||||
ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID]));
|
||||
if (!sleIssuance)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
if (sleIssuance->isFlag(lsfMPTNoConfidentialTransfer))
|
||||
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(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) <
|
||||
ctx.tx[sfMPTAmount])
|
||||
{
|
||||
return tecINSUFFICIENT_FUNDS;
|
||||
}
|
||||
|
||||
auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
|
||||
auto const account = ctx.tx[sfAccount];
|
||||
|
||||
// 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;
|
||||
|
||||
// todo: need addtional parsing, the proof should contain multiple proofs
|
||||
// auto checkEqualityProof = [&](auto const& encryptedAmount,
|
||||
// auto const& pubKey) -> TER {
|
||||
// return proveEquality(
|
||||
// ctx.tx[sfZKProof],
|
||||
// encryptedAmount,
|
||||
// pubKey,
|
||||
// ctx.tx[sfMPTAmount],
|
||||
// ctx.tx.getTransactionID(),
|
||||
// (*sleMptoken)[~sfConfidentialBalanceVersion].value_or(0));
|
||||
// };
|
||||
|
||||
// if (!isTesSuccess(checkEqualityProof(
|
||||
// ctx.tx[sfHolderEncryptedAmount],
|
||||
// (*sleMptoken)[sfHolderElGamalPublicKey])) ||
|
||||
// !isTesSuccess(checkEqualityProof(
|
||||
// ctx.tx[sfIssuerEncryptedAmount],
|
||||
// (*sleIssuance)[sfIssuerElGamalPublicKey])))
|
||||
// {
|
||||
// return tecBAD_PROOF;
|
||||
// }
|
||||
|
||||
// todo: also check range proof that
|
||||
// sfHolderEncryptedAmount <= sfConfidentialBalanceSpending AND
|
||||
// sfIssuerEncryptedAmount <= sfIssuerEncryptedBalance
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialConvertBack::doApply()
|
||||
{
|
||||
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
|
||||
|
||||
auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, account_));
|
||||
if (!sleMptoken)
|
||||
return tecINTERNAL;
|
||||
|
||||
auto sleIssuance = view().peek(keylet::mptIssuance(mptIssuanceID));
|
||||
if (!sleIssuance)
|
||||
return tecINTERNAL;
|
||||
|
||||
auto const amtToConvertBack = ctx_.tx[sfMPTAmount];
|
||||
auto const amt = (*sleMptoken)[~sfMPTAmount].value_or(0);
|
||||
|
||||
(*sleMptoken)[sfMPTAmount] = amt + amtToConvertBack;
|
||||
(*sleIssuance)[sfConfidentialOutstandingAmount] =
|
||||
(*sleIssuance)[sfConfidentialOutstandingAmount] - amtToConvertBack;
|
||||
|
||||
// it's fine if it reaches max uint32, it just resets to 0
|
||||
(*sleMptoken)[sfConfidentialBalanceVersion] =
|
||||
(*sleMptoken)[~sfConfidentialBalanceVersion].value_or(0u) + 1u;
|
||||
|
||||
// homomorphically subtract holder's encrypted balance
|
||||
{
|
||||
Buffer res(ecGamalEncryptedTotalLength);
|
||||
if (TER const ter = homomorphicSubtract(
|
||||
(*sleMptoken)[sfConfidentialBalanceSpending],
|
||||
ctx_.tx[sfHolderEncryptedAmount],
|
||||
res);
|
||||
!isTesSuccess(ter))
|
||||
return tecINTERNAL;
|
||||
|
||||
(*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;
|
||||
|
||||
(*sleMptoken)[sfIssuerEncryptedBalance] = res;
|
||||
}
|
||||
|
||||
view().update(sleIssuance);
|
||||
view().update(sleMptoken);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
48
src/xrpld/app/tx/detail/ConfidentialConvertBack.h
Normal file
48
src/xrpld/app/tx/detail/ConfidentialConvertBack.h
Normal file
@@ -0,0 +1,48 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_TX_CONFIDENTIALCONVERTBACK_H_INCLUDED
|
||||
#define RIPPLE_TX_CONFIDENTIALCONVERTBACK_H_INCLUDED
|
||||
|
||||
#include <xrpld/app/tx/detail/Transactor.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class ConfidentialConvertBack : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
|
||||
|
||||
explicit ConfidentialConvertBack(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
111
src/xrpld/app/tx/detail/ConfidentialMergeInbox.cpp
Normal file
111
src/xrpld/app/tx/detail/ConfidentialMergeInbox.cpp
Normal file
@@ -0,0 +1,111 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/tx/detail/ConfidentialMergeInbox.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 ripple {
|
||||
|
||||
NotTEC
|
||||
ConfidentialMergeInbox::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
|
||||
ConfidentialMergeInbox::preclaim(PreclaimContext const& ctx)
|
||||
{
|
||||
auto const sleIssuance =
|
||||
ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID]));
|
||||
if (!sleIssuance)
|
||||
return tecOBJECT_NOT_FOUND;
|
||||
|
||||
if (sleIssuance->isFlag(lsfMPTNoConfidentialTransfer))
|
||||
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))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialMergeInbox::doApply()
|
||||
{
|
||||
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
|
||||
auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, account_));
|
||||
if (!sleMptoken)
|
||||
return tecINTERNAL;
|
||||
|
||||
// homomorphically add holder's encrypted balance
|
||||
Buffer sum(ecGamalEncryptedTotalLength);
|
||||
if (TER const ter = homomorphicAdd(
|
||||
(*sleMptoken)[sfConfidentialBalanceSpending],
|
||||
(*sleMptoken)[sfConfidentialBalanceInbox],
|
||||
sum);
|
||||
!isTesSuccess(ter))
|
||||
return tecINTERNAL;
|
||||
|
||||
(*sleMptoken)[sfConfidentialBalanceSpending] = sum;
|
||||
|
||||
try
|
||||
{
|
||||
Buffer zeroEncyption;
|
||||
zeroEncyption = encryptCanonicalZeroAmount(
|
||||
(*sleMptoken)[sfHolderElGamalPublicKey], account_, mptIssuanceID);
|
||||
(*sleMptoken)[sfConfidentialBalanceInbox] = zeroEncyption;
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
return tecINTERNAL;
|
||||
}
|
||||
|
||||
// it's fine if it reaches max uint32, it just resets to 0
|
||||
(*sleMptoken)[sfConfidentialBalanceVersion] =
|
||||
(*sleMptoken)[~sfConfidentialBalanceVersion].value_or(0u) + 1u;
|
||||
|
||||
view().update(sleMptoken);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
48
src/xrpld/app/tx/detail/ConfidentialMergeInbox.h
Normal file
48
src/xrpld/app/tx/detail/ConfidentialMergeInbox.h
Normal file
@@ -0,0 +1,48 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_TX_CONFIDENTIALMERGEINBOX_H_INCLUDED
|
||||
#define RIPPLE_TX_CONFIDENTIALMERGEINBOX_H_INCLUDED
|
||||
|
||||
#include <xrpld/app/tx/detail/Transactor.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class ConfidentialMergeInbox : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
|
||||
|
||||
explicit ConfidentialMergeInbox(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
257
src/xrpld/app/tx/detail/ConfidentialSend.cpp
Normal file
257
src/xrpld/app/tx/detail/ConfidentialSend.cpp
Normal file
@@ -0,0 +1,257 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/misc/DelegateUtils.h>
|
||||
#include <xrpld/app/tx/detail/ConfidentialSend.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 ripple {
|
||||
|
||||
NotTEC
|
||||
ConfidentialSend::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();
|
||||
|
||||
// ConfidentialSend 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;
|
||||
|
||||
if (ctx.tx[sfSenderEncryptedAmount].length() !=
|
||||
ecGamalEncryptedTotalLength ||
|
||||
ctx.tx[sfDestinationEncryptedAmount].length() !=
|
||||
ecGamalEncryptedTotalLength ||
|
||||
ctx.tx[sfIssuerEncryptedAmount].length() != ecGamalEncryptedTotalLength)
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
if (!isValidCiphertext(ctx.tx[sfSenderEncryptedAmount]) ||
|
||||
!isValidCiphertext(ctx.tx[sfDestinationEncryptedAmount]) ||
|
||||
!isValidCiphertext(ctx.tx[sfIssuerEncryptedAmount]))
|
||||
return temBAD_CIPHERTEXT;
|
||||
|
||||
// if (ctx.tx[sfZKProof].length() != ecEqualityProofLength)
|
||||
// return temMALFORMED;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialSend::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(lsfMPTNoConfidentialTransfer))
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
// Check if issuance has issuer ElGamal public key
|
||||
if (!sleIssuance->isFieldPresent(sfIssuerElGamalPublicKey))
|
||||
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
|
||||
|
||||
// Check sender's MPToken
|
||||
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;
|
||||
|
||||
// Check destination's MPToken
|
||||
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;
|
||||
|
||||
// todo: check zkproof. equality proof and range proof, combined or separate
|
||||
// TBD. TER const terProof = verifyConfidentialSendProof(
|
||||
// ctx.tx[sfZKProof],
|
||||
// (*sleSender)[sfConfidentialBalanceSpending],
|
||||
// ctx.tx[sfSenderEncryptedAmount],
|
||||
// ctx.tx[sfDestinationEncryptedAmount],
|
||||
// ctx.tx[sfIssuerEncryptedAmount],
|
||||
// (*sleSender)[sfHolderElGamalPublicKey],
|
||||
// (*sleDestination)[sfHolderElGamalPublicKey],
|
||||
// (*sleIssuance)[sfIssuerElGamalPublicKey],
|
||||
// (*sleSender)[~sfConfidentialBalanceVersion].value_or(0),
|
||||
// ctx.tx.getTransactionID()
|
||||
// );
|
||||
|
||||
// if (!isTesSuccess(terProof))
|
||||
// return tecBAD_PROOF;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
ConfidentialSend::doApply()
|
||||
{
|
||||
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
|
||||
auto const destination = ctx_.tx[sfDestination];
|
||||
|
||||
auto sleSender = view().peek(keylet::mptoken(mptIssuanceID, account_));
|
||||
auto sleDestination =
|
||||
view().peek(keylet::mptoken(mptIssuanceID, destination));
|
||||
|
||||
auto sleDestAcct = view().peek(keylet::account(destination));
|
||||
|
||||
if (!sleSender || !sleDestination || !sleDestAcct)
|
||||
return tecINTERNAL;
|
||||
|
||||
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];
|
||||
|
||||
// Subtract from sender's spending balance
|
||||
{
|
||||
Slice const curSpending = (*sleSender)[sfConfidentialBalanceSpending];
|
||||
Buffer newSpending(ecGamalEncryptedTotalLength);
|
||||
|
||||
if (TER const ter =
|
||||
homomorphicSubtract(curSpending, senderEc, newSpending);
|
||||
!isTesSuccess(ter))
|
||||
return tecINTERNAL;
|
||||
|
||||
(*sleSender)[sfConfidentialBalanceSpending] = newSpending;
|
||||
}
|
||||
|
||||
// Subtract from issuer's balance
|
||||
{
|
||||
Slice const curIssuerEnc = (*sleSender)[sfIssuerEncryptedBalance];
|
||||
Buffer newIssuerEnc(ecGamalEncryptedTotalLength);
|
||||
|
||||
if (TER const ter =
|
||||
homomorphicSubtract(curIssuerEnc, issuerEc, newIssuerEnc);
|
||||
!isTesSuccess(ter))
|
||||
return tecINTERNAL;
|
||||
|
||||
(*sleSender)[sfIssuerEncryptedBalance] = newIssuerEnc;
|
||||
}
|
||||
|
||||
// Increment version
|
||||
(*sleSender)[sfConfidentialBalanceVersion] =
|
||||
(*sleSender)[~sfConfidentialBalanceVersion].value_or(0u) + 1u;
|
||||
|
||||
// Add to destination's inbox balance
|
||||
{
|
||||
Slice const curInbox = (*sleDestination)[sfConfidentialBalanceInbox];
|
||||
Buffer newInbox(ecGamalEncryptedTotalLength);
|
||||
|
||||
if (TER const ter = homomorphicAdd(curInbox, destEc, newInbox);
|
||||
!isTesSuccess(ter))
|
||||
return tecINTERNAL;
|
||||
|
||||
(*sleDestination)[sfConfidentialBalanceInbox] = newInbox;
|
||||
}
|
||||
|
||||
// Add to issuer's balance
|
||||
{
|
||||
Slice const curIssuerEnc = (*sleDestination)[sfIssuerEncryptedBalance];
|
||||
Buffer newIssuerEnc(ecGamalEncryptedTotalLength);
|
||||
|
||||
if (TER const ter =
|
||||
homomorphicAdd(curIssuerEnc, issuerEc, newIssuerEnc);
|
||||
!isTesSuccess(ter))
|
||||
return tecINTERNAL;
|
||||
|
||||
(*sleDestination)[sfIssuerEncryptedBalance] = newIssuerEnc;
|
||||
}
|
||||
|
||||
view().update(sleSender);
|
||||
view().update(sleDestination);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
} // namespace ripple
|
||||
48
src/xrpld/app/tx/detail/ConfidentialSend.h
Normal file
48
src/xrpld/app/tx/detail/ConfidentialSend.h
Normal file
@@ -0,0 +1,48 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_TX_CONFIDENTIALSEND_H_INCLUDED
|
||||
#define RIPPLE_TX_CONFIDENTIALSEND_H_INCLUDED
|
||||
|
||||
#include <xrpld/app/tx/detail/Transactor.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class ConfidentialSend : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
|
||||
|
||||
explicit ConfidentialSend(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
preclaim(PreclaimContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -93,6 +93,26 @@ MPTokenAuthorize::preclaim(PreclaimContext const& ctx)
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,10 @@ MPTokenIssuanceCreate::checkExtraFeatures(PreflightContext const& ctx)
|
||||
!ctx.rules.enabled(featureDynamicMPT))
|
||||
return false;
|
||||
|
||||
if (ctx.tx.getFlags() & tfMPTNoConfidentialTransfer &&
|
||||
!ctx.rules.enabled(featureConfidentialTransfer))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,18 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
|
||||
if (ctx.tx.isFieldPresent(sfDomainID) && ctx.tx.isFieldPresent(sfHolder))
|
||||
return temMALFORMED;
|
||||
|
||||
if (!ctx.rules.enabled(featureConfidentialTransfer) &&
|
||||
ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey))
|
||||
return temDISABLED;
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey) &&
|
||||
ctx.tx.isFieldPresent(sfHolder))
|
||||
return temMALFORMED;
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey) &&
|
||||
ctx.tx[sfIssuerElGamalPublicKey].length() != ecPubKeyLength)
|
||||
return temMALFORMED;
|
||||
|
||||
auto const txFlags = ctx.tx.getFlags();
|
||||
|
||||
// fails if both flags are set
|
||||
@@ -90,10 +102,12 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
|
||||
return temMALFORMED;
|
||||
|
||||
if (ctx.rules.enabled(featureSingleAssetVault) ||
|
||||
ctx.rules.enabled(featureDynamicMPT))
|
||||
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 && !ctx.tx.isFieldPresent(sfDomainID) &&
|
||||
!ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey) && !isMutate)
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
@@ -264,6 +278,19 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
// cannot update public key
|
||||
if (ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey) &&
|
||||
sleMptIssuance->isFieldPresent(sfIssuerElGamalPublicKey))
|
||||
{
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfIssuerElGamalPublicKey) &&
|
||||
sleMptIssuance->isFlag(tfMPTNoConfidentialTransfer))
|
||||
{
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -351,6 +378,16 @@ 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);
|
||||
}
|
||||
|
||||
view().update(sle);
|
||||
|
||||
return tesSUCCESS;
|
||||
|
||||
Reference in New Issue
Block a user