mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-27 09:00:32 +00:00
Add destination tag to ConfidentialMPTSend (#6988)
This commit is contained in:
@@ -1138,6 +1138,7 @@ TRANSACTION(ttCONFIDENTIAL_MPT_SEND, 88, ConfidentialMPTSend,
|
||||
({
|
||||
{sfMPTokenIssuanceID, soeREQUIRED},
|
||||
{sfDestination, soeREQUIRED},
|
||||
{sfDestinationTag, soeOPTIONAL},
|
||||
{sfSenderEncryptedAmount, soeREQUIRED},
|
||||
{sfDestinationEncryptedAmount, soeREQUIRED},
|
||||
{sfIssuerEncryptedAmount, soeREQUIRED},
|
||||
|
||||
@@ -69,6 +69,32 @@ public:
|
||||
return this->tx_->at(sfDestination);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfDestinationTag (soeOPTIONAL)
|
||||
* @return The field value, or std::nullopt if not present.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
protocol_autogen::Optional<SF_UINT32::type::value_type>
|
||||
getDestinationTag() const
|
||||
{
|
||||
if (hasDestinationTag())
|
||||
{
|
||||
return this->tx_->at(sfDestinationTag);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if sfDestinationTag is present.
|
||||
* @return True if the field is present, false otherwise.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
bool
|
||||
hasDestinationTag() const
|
||||
{
|
||||
return this->tx_->isFieldPresent(sfDestinationTag);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfSenderEncryptedAmount (soeREQUIRED)
|
||||
* @return The field value.
|
||||
@@ -266,6 +292,17 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfDestinationTag (soeOPTIONAL)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
ConfidentialMPTSendBuilder&
|
||||
setDestinationTag(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfDestinationTag] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfSenderEncryptedAmount (soeREQUIRED)
|
||||
* @return Reference to this builder for method chaining.
|
||||
|
||||
@@ -118,9 +118,17 @@ ConfidentialMPTSend::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
// Check if destination account exists
|
||||
auto const destination = ctx.tx[sfDestination];
|
||||
if (!ctx.view.exists(keylet::account(destination)))
|
||||
auto const sleDst = ctx.view.read(keylet::account(destination));
|
||||
if (!sleDst)
|
||||
return tecNO_TARGET;
|
||||
|
||||
// Check destination tag
|
||||
if (((sleDst->getFlags() & lsfRequireDestTag) != 0u) &&
|
||||
!ctx.tx.isFieldPresent(sfDestinationTag))
|
||||
{
|
||||
return tecDST_TAG_NEEDED;
|
||||
}
|
||||
|
||||
// Check if MPT issuance exists
|
||||
auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
|
||||
auto const sleIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID));
|
||||
|
||||
@@ -2400,6 +2400,28 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
});
|
||||
}
|
||||
|
||||
// destination requires destination tag but none provided
|
||||
{
|
||||
env(fset(carol, asfRequireDest));
|
||||
env.close();
|
||||
|
||||
mptAlice.send({
|
||||
.account = bob,
|
||||
.dest = carol,
|
||||
.amt = 10,
|
||||
.proof = getTrivialSendProofHex(),
|
||||
.senderEncryptedAmt = getTrivialCiphertext(),
|
||||
.destEncryptedAmt = getTrivialCiphertext(),
|
||||
.issuerEncryptedAmt = getTrivialCiphertext(),
|
||||
.amountCommitment = getTrivialCommitment(),
|
||||
.balanceCommitment = getTrivialCommitment(),
|
||||
.err = tecDST_TAG_NEEDED,
|
||||
});
|
||||
|
||||
env(fclear(carol, asfRequireDest));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// dave exists, but has no confidential fields (never converted)
|
||||
{
|
||||
mptAlice.send({
|
||||
@@ -7213,6 +7235,74 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
}
|
||||
|
||||
// Exercises ticket-specific error codes for confidential transfer transactions:
|
||||
|
||||
void
|
||||
testDestinationTag(FeatureBitset features)
|
||||
{
|
||||
testcase("test Destination Tag");
|
||||
|
||||
using namespace test::jtx;
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const carol("carol");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
|
||||
|
||||
mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanConfidentialAmount});
|
||||
mptAlice.authorize({.account = bob});
|
||||
mptAlice.authorize({.account = carol});
|
||||
|
||||
mptAlice.pay(alice, bob, 1000);
|
||||
mptAlice.pay(alice, carol, 1000);
|
||||
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.generateKeyPair(bob);
|
||||
mptAlice.generateKeyPair(carol);
|
||||
|
||||
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
|
||||
|
||||
mptAlice.convert({.account = bob, .amt = 100, .holderPubKey = mptAlice.getPubKey(bob)});
|
||||
mptAlice.mergeInbox({.account = bob});
|
||||
|
||||
mptAlice.convert({.account = carol, .amt = 50, .holderPubKey = mptAlice.getPubKey(carol)});
|
||||
mptAlice.mergeInbox({.account = carol});
|
||||
|
||||
// Set RequireDest on carol
|
||||
env(fset(carol, asfRequireDest));
|
||||
env.close();
|
||||
|
||||
// Send without destination tag — rejected
|
||||
mptAlice.send({
|
||||
.account = bob,
|
||||
.dest = carol,
|
||||
.amt = 10,
|
||||
.proof = getTrivialSendProofHex(),
|
||||
.senderEncryptedAmt = getTrivialCiphertext(),
|
||||
.destEncryptedAmt = getTrivialCiphertext(),
|
||||
.issuerEncryptedAmt = getTrivialCiphertext(),
|
||||
.amountCommitment = getTrivialCommitment(),
|
||||
.balanceCommitment = getTrivialCommitment(),
|
||||
.err = tecDST_TAG_NEEDED,
|
||||
});
|
||||
|
||||
// Send with destination tag — succeeds (passes preclaim,
|
||||
// reaches ZKP verification with the real proof)
|
||||
mptAlice.send({.account = bob, .dest = carol, .amt = 10, .destinationTag = 42});
|
||||
|
||||
// Verify the destination tag is in the confirmed transaction
|
||||
auto const tx = env.tx();
|
||||
BEAST_EXPECT(tx);
|
||||
BEAST_EXPECT(tx->isFieldPresent(sfDestinationTag));
|
||||
BEAST_EXPECT((*tx)[sfDestinationTag] == 42);
|
||||
|
||||
env(fclear(carol, asfRequireDest));
|
||||
env.close();
|
||||
|
||||
// Send without destination tag when not required — succeeds
|
||||
mptAlice.mergeInbox({.account = carol});
|
||||
mptAlice.send({.account = bob, .dest = carol, .amt = 10});
|
||||
}
|
||||
|
||||
// terPRE_TICKET when the ticket doesn't exist yet, and tefNO_TICKET when
|
||||
// the ticket has already been consumed or was never created.
|
||||
void
|
||||
@@ -8660,6 +8750,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
testSendDepositPreauth(features);
|
||||
testSendCredentialValidation(features);
|
||||
testSendWithAuditor(features);
|
||||
testDestinationTag(features);
|
||||
|
||||
// ConfidentialMPTClawback
|
||||
testClawback(features);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/delegate.h>
|
||||
#include <test/jtx/tag.h>
|
||||
#include <test/jtx/ter.h>
|
||||
#include <test/jtx/ticket.h>
|
||||
#include <test/jtx/txflags.h>
|
||||
@@ -242,6 +243,7 @@ struct MPTConfidentialSend
|
||||
std::optional<Buffer> amountCommitment = std::nullopt;
|
||||
std::optional<Buffer> balanceCommitment = std::nullopt;
|
||||
std::optional<Account> delegate = std::nullopt;
|
||||
std::optional<std::uint32_t> destinationTag = std::nullopt;
|
||||
std::optional<std::uint32_t> ticketSeq = std::nullopt;
|
||||
std::optional<std::uint32_t> ownerCount = std::nullopt;
|
||||
std::optional<std::uint32_t> holderCount = std::nullopt;
|
||||
@@ -612,6 +614,10 @@ private:
|
||||
if constexpr (requires { arg.delegate; })
|
||||
delegateAcct = arg.delegate;
|
||||
|
||||
std::optional<std::uint32_t> dstTag;
|
||||
if constexpr (requires { arg.destinationTag; })
|
||||
dstTag = arg.destinationTag;
|
||||
|
||||
if (ticketSeq && delegateAcct)
|
||||
env_(
|
||||
jv,
|
||||
@@ -623,6 +629,8 @@ private:
|
||||
env_(jv, expectedFlags, expectedTer, ticket::use(*ticketSeq));
|
||||
else if (delegateAcct)
|
||||
env_(jv, expectedFlags, expectedTer, delegate::as(*delegateAcct));
|
||||
else if (dstTag)
|
||||
env_(jv, expectedFlags, expectedTer, dtag(*dstTag));
|
||||
else
|
||||
env_(jv, expectedFlags, expectedTer);
|
||||
auto const err = env_.ter();
|
||||
|
||||
@@ -31,6 +31,7 @@ TEST(TransactionsConfidentialMPTSendTests, BuilderSettersRoundTrip)
|
||||
// Transaction-specific field values
|
||||
auto const mPTokenIssuanceIDValue = canonical_UINT192();
|
||||
auto const destinationValue = canonical_ACCOUNT();
|
||||
auto const destinationTagValue = canonical_UINT32();
|
||||
auto const senderEncryptedAmountValue = canonical_VL();
|
||||
auto const destinationEncryptedAmountValue = canonical_VL();
|
||||
auto const issuerEncryptedAmountValue = canonical_VL();
|
||||
@@ -55,6 +56,7 @@ TEST(TransactionsConfidentialMPTSendTests, BuilderSettersRoundTrip)
|
||||
};
|
||||
|
||||
// Set optional fields
|
||||
builder.setDestinationTag(destinationTagValue);
|
||||
builder.setAuditorEncryptedAmount(auditorEncryptedAmountValue);
|
||||
builder.setCredentialIDs(credentialIDsValue);
|
||||
|
||||
@@ -122,6 +124,14 @@ TEST(TransactionsConfidentialMPTSendTests, BuilderSettersRoundTrip)
|
||||
}
|
||||
|
||||
// Verify optional fields
|
||||
{
|
||||
auto const& expected = destinationTagValue;
|
||||
auto const actualOpt = tx.getDestinationTag();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfDestinationTag");
|
||||
EXPECT_TRUE(tx.hasDestinationTag());
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = auditorEncryptedAmountValue;
|
||||
auto const actualOpt = tx.getAuditorEncryptedAmount();
|
||||
@@ -156,6 +166,7 @@ TEST(TransactionsConfidentialMPTSendTests, BuilderFromStTxRoundTrip)
|
||||
// Transaction-specific field values
|
||||
auto const mPTokenIssuanceIDValue = canonical_UINT192();
|
||||
auto const destinationValue = canonical_ACCOUNT();
|
||||
auto const destinationTagValue = canonical_UINT32();
|
||||
auto const senderEncryptedAmountValue = canonical_VL();
|
||||
auto const destinationEncryptedAmountValue = canonical_VL();
|
||||
auto const issuerEncryptedAmountValue = canonical_VL();
|
||||
@@ -180,6 +191,7 @@ TEST(TransactionsConfidentialMPTSendTests, BuilderFromStTxRoundTrip)
|
||||
feeValue
|
||||
};
|
||||
|
||||
initialBuilder.setDestinationTag(destinationTagValue);
|
||||
initialBuilder.setAuditorEncryptedAmount(auditorEncryptedAmountValue);
|
||||
initialBuilder.setCredentialIDs(credentialIDsValue);
|
||||
|
||||
@@ -248,6 +260,13 @@ TEST(TransactionsConfidentialMPTSendTests, BuilderFromStTxRoundTrip)
|
||||
}
|
||||
|
||||
// Verify optional fields
|
||||
{
|
||||
auto const& expected = destinationTagValue;
|
||||
auto const actualOpt = rebuiltTx.getDestinationTag();
|
||||
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present";
|
||||
expectEqualField(expected, *actualOpt, "sfDestinationTag");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = auditorEncryptedAmountValue;
|
||||
auto const actualOpt = rebuiltTx.getAuditorEncryptedAmount();
|
||||
@@ -333,6 +352,8 @@ TEST(TransactionsConfidentialMPTSendTests, OptionalFieldsReturnNullopt)
|
||||
auto tx = builder.build(publicKey, secretKey);
|
||||
|
||||
// Verify optional fields are not present
|
||||
EXPECT_FALSE(tx.hasDestinationTag());
|
||||
EXPECT_FALSE(tx.getDestinationTag().has_value());
|
||||
EXPECT_FALSE(tx.hasAuditorEncryptedAmount());
|
||||
EXPECT_FALSE(tx.getAuditorEncryptedAmount().has_value());
|
||||
EXPECT_FALSE(tx.hasCredentialIDs());
|
||||
|
||||
Reference in New Issue
Block a user