mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-30 07:55:51 +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,
|
lsfMPTCanTrade = 0x00000010,
|
||||||
lsfMPTCanTransfer = 0x00000020,
|
lsfMPTCanTransfer = 0x00000020,
|
||||||
lsfMPTCanClawback = 0x00000040,
|
lsfMPTCanClawback = 0x00000040,
|
||||||
|
lsfMPTNoConfidentialTransfer = 0x00000080,
|
||||||
|
|
||||||
lsmfMPTCanMutateCanLock = 0x00000002,
|
lsmfMPTCanMutateCanLock = 0x00000002,
|
||||||
lsmfMPTCanMutateRequireAuth = 0x00000004,
|
lsmfMPTCanMutateRequireAuth = 0x00000004,
|
||||||
|
|||||||
@@ -181,6 +181,20 @@ std::size_t constexpr permissionMaxSize = 10;
|
|||||||
/** The maximum number of transactions that can be in a batch. */
|
/** The maximum number of transactions that can be in a batch. */
|
||||||
std::size_t constexpr maxBatchTxCount = 8;
|
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
|
} // namespace ripple
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ enum TEMcodes : TERUnderlyingType {
|
|||||||
temARRAY_TOO_LARGE,
|
temARRAY_TOO_LARGE,
|
||||||
temBAD_TRANSFER_FEE,
|
temBAD_TRANSFER_FEE,
|
||||||
temINVALID_INNER_BATCH,
|
temINVALID_INNER_BATCH,
|
||||||
|
temBAD_CIPHERTEXT,
|
||||||
};
|
};
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
@@ -366,6 +367,7 @@ enum TECcodes : TERUnderlyingType {
|
|||||||
// backward compatibility with historical data on non-prod networks, can be
|
// backward compatibility with historical data on non-prod networks, can be
|
||||||
// reclaimed after those networks reset.
|
// reclaimed after those networks reset.
|
||||||
tecNO_DELEGATE_PERMISSION = 198,
|
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 tfMPTCanTrade = lsfMPTCanTrade;
|
||||||
constexpr std::uint32_t const tfMPTCanTransfer = lsfMPTCanTransfer;
|
constexpr std::uint32_t const tfMPTCanTransfer = lsfMPTCanTransfer;
|
||||||
constexpr std::uint32_t const tfMPTCanClawback = lsfMPTCanClawback;
|
constexpr std::uint32_t const tfMPTCanClawback = lsfMPTCanClawback;
|
||||||
|
constexpr std::uint32_t const tfMPTNoConfidentialTransfer = lsfMPTNoConfidentialTransfer;
|
||||||
constexpr std::uint32_t const tfMPTokenIssuanceCreateMask =
|
constexpr std::uint32_t const tfMPTokenIssuanceCreateMask =
|
||||||
~(tfUniversal | tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback);
|
~(tfUniversal | tfMPTCanLock | tfMPTRequireAuth | tfMPTCanEscrow | tfMPTCanTrade | tfMPTCanTransfer | tfMPTCanClawback | tfMPTNoConfidentialTransfer);
|
||||||
|
|
||||||
// MPTokenIssuanceCreate MutableFlags:
|
// MPTokenIssuanceCreate MutableFlags:
|
||||||
// Indicating specific fields or flags may be changed after issuance.
|
// Indicating specific fields or flags may be changed after issuance.
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
// Add new amendments to the top of this list.
|
// Add new amendments to the top of this list.
|
||||||
// Keep it sorted in reverse chronological order.
|
// Keep it sorted in reverse chronological order.
|
||||||
|
|
||||||
|
XRPL_FEATURE(ConfidentialTransfer, Supported::no, VoteBehavior::DefaultNo)
|
||||||
XRPL_FEATURE(PermissionDelegationV1_1, Supported::no, VoteBehavior::DefaultNo)
|
XRPL_FEATURE(PermissionDelegationV1_1, Supported::no, VoteBehavior::DefaultNo)
|
||||||
XRPL_FIX (DirectoryLimit, Supported::yes, VoteBehavior::DefaultNo)
|
XRPL_FIX (DirectoryLimit, Supported::yes, VoteBehavior::DefaultNo)
|
||||||
XRPL_FIX (IncludeKeyletFields, 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},
|
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
||||||
{sfDomainID, soeOPTIONAL},
|
{sfDomainID, soeOPTIONAL},
|
||||||
{sfMutableFlags, soeDEFAULT},
|
{sfMutableFlags, soeDEFAULT},
|
||||||
|
{sfIssuerElGamalPublicKey, soeOPTIONAL},
|
||||||
|
{sfConfidentialOutstandingAmount, soeDEFAULT},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
/** A ledger object which tracks MPToken
|
/** A ledger object which tracks MPToken
|
||||||
@@ -429,6 +431,11 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
|
|||||||
{sfOwnerNode, soeREQUIRED},
|
{sfOwnerNode, soeREQUIRED},
|
||||||
{sfPreviousTxnID, soeREQUIRED},
|
{sfPreviousTxnID, soeREQUIRED},
|
||||||
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
||||||
|
{sfConfidentialBalanceInbox, soeOPTIONAL},
|
||||||
|
{sfConfidentialBalanceSpending, soeOPTIONAL},
|
||||||
|
{sfConfidentialBalanceVersion, soeDEFAULT},
|
||||||
|
{sfIssuerEncryptedBalance, soeOPTIONAL},
|
||||||
|
{sfHolderElGamalPublicKey, soeOPTIONAL},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
/** A ledger object which tracks Oracle
|
/** A ledger object which tracks Oracle
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50)
|
|||||||
TYPED_SFIELD(sfOracleDocumentID, UINT32, 51)
|
TYPED_SFIELD(sfOracleDocumentID, UINT32, 51)
|
||||||
TYPED_SFIELD(sfPermissionValue, UINT32, 52)
|
TYPED_SFIELD(sfPermissionValue, UINT32, 52)
|
||||||
TYPED_SFIELD(sfMutableFlags, UINT32, 53)
|
TYPED_SFIELD(sfMutableFlags, UINT32, 53)
|
||||||
|
TYPED_SFIELD(sfConfidentialBalanceVersion, UINT32, 54)
|
||||||
|
|
||||||
// 64-bit integers (common)
|
// 64-bit integers (common)
|
||||||
TYPED_SFIELD(sfIndexNext, UINT64, 1)
|
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(sfIssuerNode, UINT64, 27)
|
||||||
TYPED_SFIELD(sfSubjectNode, UINT64, 28)
|
TYPED_SFIELD(sfSubjectNode, UINT64, 28)
|
||||||
TYPED_SFIELD(sfLockedAmount, UINT64, 29, SField::sMD_BaseTen|SField::sMD_Default)
|
TYPED_SFIELD(sfLockedAmount, UINT64, 29, SField::sMD_BaseTen|SField::sMD_Default)
|
||||||
|
TYPED_SFIELD(sfConfidentialOutstandingAmount, UINT64, 30, SField::sMD_BaseTen|SField::sMD_Default)
|
||||||
|
|
||||||
// 128-bit
|
// 128-bit
|
||||||
TYPED_SFIELD(sfEmailHash, UINT128, 1)
|
TYPED_SFIELD(sfEmailHash, UINT128, 1)
|
||||||
@@ -284,6 +286,16 @@ TYPED_SFIELD(sfAssetClass, VL, 28)
|
|||||||
TYPED_SFIELD(sfProvider, VL, 29)
|
TYPED_SFIELD(sfProvider, VL, 29)
|
||||||
TYPED_SFIELD(sfMPTokenMetadata, VL, 30)
|
TYPED_SFIELD(sfMPTokenMetadata, VL, 30)
|
||||||
TYPED_SFIELD(sfCredentialType, VL, 31)
|
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)
|
// account (common)
|
||||||
TYPED_SFIELD(sfAccount, ACCOUNT, 1)
|
TYPED_SFIELD(sfAccount, ACCOUNT, 1)
|
||||||
|
|||||||
@@ -741,6 +741,7 @@ TRANSACTION(ttMPTOKEN_ISSUANCE_SET, 56, MPTokenIssuanceSet,
|
|||||||
{sfMPTokenMetadata, soeOPTIONAL},
|
{sfMPTokenMetadata, soeOPTIONAL},
|
||||||
{sfTransferFee, soeOPTIONAL},
|
{sfTransferFee, soeOPTIONAL},
|
||||||
{sfMutableFlags, soeOPTIONAL},
|
{sfMutableFlags, soeOPTIONAL},
|
||||||
|
{sfIssuerElGamalPublicKey, soeOPTIONAL},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
/** This transaction type authorizes a MPToken instance */
|
/** This transaction type authorizes a MPToken instance */
|
||||||
@@ -944,6 +945,82 @@ TRANSACTION(ttBATCH, 71, Batch,
|
|||||||
{sfBatchSigners, soeOPTIONAL},
|
{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.
|
/** This system-generated transaction type is used to update the status of the various amendments.
|
||||||
|
|
||||||
For details, see: https://xrpl.org/amendments.html
|
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
|
// Only if auth check is needed, as it needs to do an additional read
|
||||||
// operation. Note featureSingleAssetVault will affect error codes.
|
// operation. Note featureSingleAssetVault will affect error codes.
|
||||||
if (zeroIfUnauthorized == ahZERO_IF_UNAUTHORIZED &&
|
if (zeroIfUnauthorized == ahZERO_IF_UNAUTHORIZED &&
|
||||||
view.rules().enabled(featureSingleAssetVault))
|
(view.rules().enabled(featureSingleAssetVault) ||
|
||||||
|
view.rules().enabled(featureConfidentialTransfer)))
|
||||||
{
|
{
|
||||||
if (auto const err =
|
if (auto const err =
|
||||||
requireAuth(view, mptIssue, account, AuthType::StrongAuth);
|
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(tecLIMIT_EXCEEDED, "Limit exceeded."),
|
||||||
MAKE_ERROR(tecPSEUDO_ACCOUNT, "This operation is not allowed against a pseudo-account."),
|
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(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(tefALREADY, "The exact transaction was already in this ledger."),
|
||||||
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),
|
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(temARRAY_TOO_LARGE, "Malformed: Array is too large."),
|
||||||
MAKE_ERROR(temBAD_TRANSFER_FEE, "Malformed: Transfer fee is outside valid range."),
|
MAKE_ERROR(temBAD_TRANSFER_FEE, "Malformed: Transfer fee is outside valid range."),
|
||||||
MAKE_ERROR(temINVALID_INNER_BATCH, "Malformed: Invalid inner batch transaction."),
|
MAKE_ERROR(temINVALID_INNER_BATCH, "Malformed: Invalid inner batch transaction."),
|
||||||
|
MAKE_ERROR(temBAD_CIPHERTEXT, "Malformed: Invalid ciphertext."),
|
||||||
|
|
||||||
MAKE_ERROR(terRETRY, "Retry transaction."),
|
MAKE_ERROR(terRETRY, "Retry transaction."),
|
||||||
MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."),
|
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 <test/jtx.h>
|
||||||
|
|
||||||
|
#include <xrpl/protocol/ConfidentialTransfer.h>
|
||||||
#include <xrpl/protocol/SField.h>
|
#include <xrpl/protocol/SField.h>
|
||||||
#include <xrpl/protocol/jss.h>
|
#include <xrpl/protocol/jss.h>
|
||||||
|
|
||||||
|
#include "test/jtx/mpt.h"
|
||||||
|
#include <openssl/rand.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
namespace ripple {
|
namespace ripple {
|
||||||
namespace test {
|
namespace test {
|
||||||
namespace jtx {
|
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
|
void
|
||||||
mptflags::operator()(Env& env) const
|
mptflags::operator()(Env& env) const
|
||||||
{
|
{
|
||||||
@@ -248,60 +269,79 @@ MPTTester::set(MPTSet const& arg)
|
|||||||
jv[sfTransferFee] = *arg.transferFee;
|
jv[sfTransferFee] = *arg.transferFee;
|
||||||
if (arg.metadata)
|
if (arg.metadata)
|
||||||
jv[sfMPTokenMetadata] = strHex(*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,
|
if ((arg.flags || arg.mutableFlags))
|
||||||
bool unchanged) {
|
{
|
||||||
auto flags = getFlags(holder);
|
auto require = [&](std::optional<Account> const& holder,
|
||||||
if (!unchanged)
|
bool unchanged) {
|
||||||
{
|
auto flags = getFlags(holder);
|
||||||
if (arg.flags)
|
if (!unchanged)
|
||||||
{
|
{
|
||||||
if (*arg.flags & tfMPTLock)
|
if (arg.flags)
|
||||||
flags |= lsfMPTLocked;
|
{
|
||||||
else if (*arg.flags & tfMPTUnlock)
|
if (*arg.flags & tfMPTLock)
|
||||||
flags &= ~lsfMPTLocked;
|
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.pubKey)
|
||||||
{
|
{
|
||||||
if (*arg.mutableFlags & tmfMPTSetCanLock)
|
env_.require(requireAny([&]() -> bool {
|
||||||
flags |= lsfMPTCanLock;
|
return forObject([&](SLEP const& sle) -> bool {
|
||||||
else if (*arg.mutableFlags & tmfMPTClearCanLock)
|
if (sle)
|
||||||
flags &= ~lsfMPTCanLock;
|
{
|
||||||
|
return strHex((*sle)[sfIssuerElGamalPublicKey]) ==
|
||||||
if (*arg.mutableFlags & tmfMPTSetRequireAuth)
|
strHex(getPubKey(issuer_));
|
||||||
flags |= lsfMPTRequireAuth;
|
}
|
||||||
else if (*arg.mutableFlags & tmfMPTClearRequireAuth)
|
return false;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
|
[[nodiscard]] bool
|
||||||
MPTTester::checkMPTokenAmount(
|
MPTTester::checkMPTokenAmount(
|
||||||
Account const& holder_,
|
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
|
[[nodiscard]] bool
|
||||||
MPTTester::checkFlags(
|
MPTTester::checkFlags(
|
||||||
uint32_t const expectedFlags,
|
uint32_t const expectedFlags,
|
||||||
@@ -492,6 +552,48 @@ MPTTester::getBalance(Account const& account) const
|
|||||||
return 0;
|
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
|
std::uint32_t
|
||||||
MPTTester::getFlags(std::optional<Account> const& holder) const
|
MPTTester::getFlags(std::optional<Account> const& holder) const
|
||||||
{
|
{
|
||||||
@@ -512,6 +614,533 @@ MPTTester::operator[](std::string const& name) const
|
|||||||
return MPT(name, issuanceID());
|
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 jtx
|
||||||
} // namespace test
|
} // namespace test
|
||||||
} // namespace ripple
|
} // namespace ripple
|
||||||
|
|||||||
@@ -27,12 +27,18 @@
|
|||||||
|
|
||||||
#include <xrpl/protocol/UintTypes.h>
|
#include <xrpl/protocol/UintTypes.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
namespace ripple {
|
namespace ripple {
|
||||||
namespace test {
|
namespace test {
|
||||||
namespace jtx {
|
namespace jtx {
|
||||||
|
|
||||||
class MPTTester;
|
class MPTTester;
|
||||||
|
|
||||||
|
// Generates a syntactically valid placeholder ciphertext
|
||||||
|
ripple::Buffer
|
||||||
|
generatePlaceholderCiphertext();
|
||||||
|
|
||||||
// Check flags settings on MPT create
|
// Check flags settings on MPT create
|
||||||
class mptflags
|
class mptflags
|
||||||
{
|
{
|
||||||
@@ -145,6 +151,77 @@ struct MPTSet
|
|||||||
std::optional<std::string> metadata = std::nullopt;
|
std::optional<std::string> metadata = std::nullopt;
|
||||||
std::optional<Account> delegate = std::nullopt;
|
std::optional<Account> delegate = std::nullopt;
|
||||||
std::optional<uint256> domainID = 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;
|
std::optional<TER> err = std::nullopt;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -155,8 +232,16 @@ class MPTTester
|
|||||||
std::unordered_map<std::string, Account> const holders_;
|
std::unordered_map<std::string, Account> const holders_;
|
||||||
std::optional<MPTID> id_;
|
std::optional<MPTID> id_;
|
||||||
bool close_;
|
bool close_;
|
||||||
|
std::unordered_map<AccountID, Buffer> pubKeys;
|
||||||
|
std::unordered_map<AccountID, Buffer> privKeys;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
enum EncryptedBalanceType {
|
||||||
|
ISSUER_ENCRYPTED_BALANCE,
|
||||||
|
HOLDER_ENCRYPTED_INBOX,
|
||||||
|
HOLDER_ENCRYPTED_SPENDING,
|
||||||
|
};
|
||||||
|
|
||||||
MPTTester(Env& env, Account const& issuer, MPTInit const& constr = {});
|
MPTTester(Env& env, Account const& issuer, MPTInit const& constr = {});
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -171,6 +256,22 @@ public:
|
|||||||
void
|
void
|
||||||
set(MPTSet const& set = {});
|
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
|
[[nodiscard]] bool
|
||||||
checkDomainID(std::optional<uint256> expected) const;
|
checkDomainID(std::optional<uint256> expected) const;
|
||||||
|
|
||||||
@@ -181,6 +282,9 @@ public:
|
|||||||
[[nodiscard]] bool
|
[[nodiscard]] bool
|
||||||
checkMPTokenOutstandingAmount(std::int64_t expectedAmount) const;
|
checkMPTokenOutstandingAmount(std::int64_t expectedAmount) const;
|
||||||
|
|
||||||
|
[[nodiscard]] bool
|
||||||
|
checkIssuanceConfidentialBalance(std::int64_t expectedAmount) const;
|
||||||
|
|
||||||
[[nodiscard]] bool
|
[[nodiscard]] bool
|
||||||
checkFlags(
|
checkFlags(
|
||||||
uint32_t const expectedFlags,
|
uint32_t const expectedFlags,
|
||||||
@@ -234,9 +338,43 @@ public:
|
|||||||
std::int64_t
|
std::int64_t
|
||||||
getBalance(Account const& account) const;
|
getBalance(Account const& account) const;
|
||||||
|
|
||||||
|
std::int64_t
|
||||||
|
getIssuanceConfidentialBalance() const;
|
||||||
|
|
||||||
|
std::optional<Buffer>
|
||||||
|
getEncryptedBalance(
|
||||||
|
Account const& account,
|
||||||
|
EncryptedBalanceType option = HOLDER_ENCRYPTED_INBOX) const;
|
||||||
|
|
||||||
MPT
|
MPT
|
||||||
operator[](std::string const& name) const;
|
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:
|
private:
|
||||||
using SLEP = std::shared_ptr<SLE const>;
|
using SLEP = std::shared_ptr<SLE const>;
|
||||||
bool
|
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))
|
sleMpt->isFlag(lsfMPTLocked))
|
||||||
return tecNO_PERMISSION;
|
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;
|
return tesSUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ MPTokenIssuanceCreate::checkExtraFeatures(PreflightContext const& ctx)
|
|||||||
!ctx.rules.enabled(featureDynamicMPT))
|
!ctx.rules.enabled(featureDynamicMPT))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
if (ctx.tx.getFlags() & tfMPTNoConfidentialTransfer &&
|
||||||
|
!ctx.rules.enabled(featureConfidentialTransfer))
|
||||||
|
return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,18 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
|
|||||||
if (ctx.tx.isFieldPresent(sfDomainID) && ctx.tx.isFieldPresent(sfHolder))
|
if (ctx.tx.isFieldPresent(sfDomainID) && ctx.tx.isFieldPresent(sfHolder))
|
||||||
return temMALFORMED;
|
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();
|
auto const txFlags = ctx.tx.getFlags();
|
||||||
|
|
||||||
// fails if both flags are set
|
// fails if both flags are set
|
||||||
@@ -90,10 +102,12 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
|
|||||||
return temMALFORMED;
|
return temMALFORMED;
|
||||||
|
|
||||||
if (ctx.rules.enabled(featureSingleAssetVault) ||
|
if (ctx.rules.enabled(featureSingleAssetVault) ||
|
||||||
ctx.rules.enabled(featureDynamicMPT))
|
ctx.rules.enabled(featureDynamicMPT) ||
|
||||||
|
ctx.rules.enabled(featureConfidentialTransfer))
|
||||||
{
|
{
|
||||||
// Is this transaction actually changing anything ?
|
// 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;
|
return temMALFORMED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,6 +278,19 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
|
|||||||
return tecNO_PERMISSION;
|
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;
|
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);
|
view().update(sle);
|
||||||
|
|
||||||
return tesSUCCESS;
|
return tesSUCCESS;
|
||||||
|
|||||||
Reference in New Issue
Block a user