Handle edge cases checks in flag setting and token deletion (#6596)

This commit is contained in:
Shawn Xie
2026-03-24 11:41:27 -04:00
committed by GitHub
parent 3f749ecf76
commit 4c0e6012e3
8 changed files with 500 additions and 92 deletions

View File

@@ -404,48 +404,39 @@ Buffer
generateBlindingFactor();
/**
* @brief Verifies the cryptographic link between an ElGamal Ciphertext and a
* Pedersen Commitment for a transaction Amount.
* @brief Distinguishes the two algebraic structures used in
* ElGamal-Pedersen linkage proofs.
*
* It proves that the ElGamal ciphertext `encAmt` encrypts the same value `m`
* as the Pedersen Commitment `pcmSlice`, using the randomness `r`.
* Proves Enc(m) <-> Pcm(m)
*
* @param proof The Zero Knowledge Proof bytes.
* @param encAmt The ElGamal ciphertext of the amount (C1, C2).
* @param pubKeySlice The sender's public key.
* @param pcmSlice The Pedersen Commitment to the amount.
* @param contextHash The unique context hash for this transaction.
* @return tesSUCCESS if the proof is valid, or an error code otherwise.
* - amount: The ciphertext was created with randomness `r`.
* Verification order: C1, C2, Pk, Pcm.
* - balance: The ciphertext was created with the secret key `s`.
* Verification order: Pk, C2, C1, Pcm (swaps Pk <-> C1).
*/
TER
verifyAmountPcmLinkage(
Slice const& proof,
Slice const& encAmt,
Slice const& pubKeySlice,
Slice const& pcmSlice,
uint256 const& contextHash);
enum class PcmLinkageType { amount, balance };
/**
* @brief Verifies the cryptographic link between an ElGamal Ciphertext and a
* Pedersen Commitment for an account Balance.
* Pedersen Commitment.
*
* It proves that the ElGamal ciphertext `encAmt` encrypts the same value `b`
* as the Pedersen Commitment `pcmSlice`, using the secret key `s`.
* Proves Enc(b) <-> Pcm(b)
* Proves that the ElGamal ciphertext `encAmt` encrypts the same value
* as the Pedersen Commitment `pcmSlice`.
*
* Note: Swaps arguments (Pk <-> C1) to accommodate the different algebraic
* structure.
* The `type` parameter selects the argument ordering passed to the
* underlying secp256k1 verification call to accommodate the different
* algebraic structures used for amounts (randomness `r`) vs balances
* (secret key `s`).
*
* @param type Whether this is an amount or balance linkage proof.
* @param proof The Zero Knowledge Proof bytes.
* @param encAmt The ElGamal ciphertext of the balance (C1, C2).
* @param encAmt The ElGamal ciphertext (C1, C2).
* @param pubKeySlice The sender's public key.
* @param pcmSlice The Pedersen Commitment to the balance.
* @param pcmSlice The Pedersen Commitment.
* @param contextHash The unique context hash for this transaction.
* @return tesSUCCESS if the proof is valid, or an error code otherwise.
*/
TER
verifyBalancePcmLinkage(
verifyPcmLinkage(
PcmLinkageType type,
Slice const& proof,
Slice const& encAmt,
Slice const& pubKeySlice,

View File

@@ -1739,6 +1739,13 @@ removeEmptyHolding(
if (mptoken->at(sfMPTAmount) != 0)
return tecHAS_OBLIGATIONS;
// Don't delete if the token still has confidential balances
if (mptoken->isFieldPresent(sfConfidentialBalanceInbox) ||
mptoken->isFieldPresent(sfConfidentialBalanceSpending))
{
return tecHAS_OBLIGATIONS;
}
return authorizeMPToken(
view,
{}, // priorBalance

View File

@@ -512,7 +512,8 @@ checkEncryptedAmountFormat(STObject const& object)
}
TER
verifyAmountPcmLinkage(
verifyPcmLinkage(
PcmLinkageType type,
Slice const& proof,
Slice const& encAmt,
Slice const& pubKeySlice,
@@ -548,7 +549,13 @@ verifyAmountPcmLinkage(
return tecINTERNAL; // LCOV_EXCL_LINE
}
if (auto res = secp256k1_elgamal_pedersen_link_verify(
// For amount linkage (randomness r): order is C1, C2, Pk, Pcm.
// For balance linkage (secret key s): order is Pk, C2, C1, Pcm
// (swaps Pk <-> C1 to accommodate the different algebraic structure).
int res;
if (type == PcmLinkageType::amount)
{
res = secp256k1_elgamal_pedersen_link_verify(
secp256k1Context(),
proof.data(),
&pair->c1,
@@ -556,54 +563,10 @@ verifyAmountPcmLinkage(
&pubKey,
&pcm,
contextHash.data());
res != 1)
{
return tecBAD_PROOF;
}
return tesSUCCESS;
}
TER
verifyBalancePcmLinkage(
Slice const& proof,
Slice const& encAmt,
Slice const& pubKeySlice,
Slice const& pcmSlice,
uint256 const& contextHash)
{
if (proof.length() != ecPedersenProofLength)
return tecINTERNAL;
auto const pair = makeEcPair(encAmt);
if (!pair)
return tecINTERNAL; // LCOV_EXCL_LINE
if (pubKeySlice.size() != ecPubKeyLength)
return tecINTERNAL; // LCOV_EXCL_LINE
if (pcmSlice.size() != ecPedersenCommitmentLength)
return tecINTERNAL; // LCOV_EXCL_LINE
secp256k1_pubkey pubKey;
if (auto res = secp256k1_ec_pubkey_parse(
secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength);
res != 1)
else
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
secp256k1_pubkey pcm;
if (auto res = secp256k1_ec_pubkey_parse(
secp256k1Context(), &pcm, pcmSlice.data(), ecPedersenCommitmentLength);
res != 1)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}
// Note: c2, c1 order - the linkage proof expects the message-containing
// component (c2 = m*G + r*Pk) before the blinding component (c1 = r*G).
if (auto res = secp256k1_elgamal_pedersen_link_verify(
res = secp256k1_elgamal_pedersen_link_verify(
secp256k1Context(),
proof.data(),
&pubKey,
@@ -611,11 +574,11 @@ verifyBalancePcmLinkage(
&pair->c1,
&pcm,
contextHash.data());
res != 1)
{
return tecBAD_PROOF;
}
if (res != 1)
return tecBAD_PROOF;
return tesSUCCESS;
}

View File

@@ -124,7 +124,8 @@ verifyProofs(
return tecINTERNAL; // LCOV_EXCL_LINE
// verify el gamal pedersen linkage
if (auto const ter = verifyBalancePcmLinkage(
if (auto const ter = verifyPcmLinkage(
PcmLinkageType::balance,
pedersenProof,
(*mptoken)[sfConfidentialBalanceSpending],
holderPubKey,

View File

@@ -1,3 +1,4 @@
#include <xrpl/ledger/View.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -46,6 +47,16 @@ ConfidentialMPTMergeInbox::preclaim(PreclaimContext const& ctx)
!sleMptoken->isFieldPresent(sfHolderEncryptionKey))
return tecNO_PERMISSION;
// Check lock
auto const account = ctx.tx[sfAccount];
MPTIssue const mptIssue(ctx.tx[sfMPTokenIssuanceID]);
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;
return tesSUCCESS;
}

View File

@@ -157,7 +157,8 @@ verifySendProofs(
}
// Verify amount linkage
if (auto const ter = verifyAmountPcmLinkage(
if (auto const ter = verifyPcmLinkage(
PcmLinkageType::amount,
amountLinkageProof,
ctx.tx[sfSenderEncryptedAmount],
(*sleSenderMPToken)[sfHolderEncryptionKey],
@@ -169,7 +170,8 @@ verifySendProofs(
}
// Verify balance linkage
if (auto const ter = verifyBalancePcmLinkage(
if (auto const ter = verifyPcmLinkage(
PcmLinkageType::balance,
balanceLinkageProof,
(*sleSenderMPToken)[sfConfidentialBalanceSpending],
(*sleSenderMPToken)[sfHolderEncryptionKey],

View File

@@ -137,6 +137,11 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
if (hasAuditorElGamalKey && !hasIssuerElGamalKey)
return temMALFORMED;
// Cannot set keys while clearing confidential amount
if ((hasIssuerElGamalKey || hasAuditorElGamalKey) && mutableFlags &&
(*mutableFlags & tmfMPTClearCanConfidentialAmount))
return temINVALID_FLAG;
if (hasIssuerElGamalKey && !isValidCompressedECPoint(ctx.tx[sfIssuerEncryptionKey]))
return temMALFORMED;
@@ -291,14 +296,20 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
}
// Check if the transaction is enabling confidential amounts
bool const enablesConfidentialAmount =
mutableFlags && (*mutableFlags & tmfMPTSetCanConfidentialAmount);
// Encryption keys can only be set if confidential amounts are already
// enabled on the issuance OR if the transaction is enabling it
if (ctx.tx.isFieldPresent(sfIssuerEncryptionKey) &&
!sleMptIssuance->isFlag(lsfMPTCanConfidentialAmount))
!sleMptIssuance->isFlag(lsfMPTCanConfidentialAmount) && !enablesConfidentialAmount)
{
return tecNO_PERMISSION;
}
if (ctx.tx.isFieldPresent(sfAuditorEncryptionKey) &&
!sleMptIssuance->isFlag(lsfMPTCanConfidentialAmount))
!sleMptIssuance->isFlag(lsfMPTCanConfidentialAmount) && !enablesConfidentialAmount)
{
return tecNO_PERMISSION;
}

View File

@@ -1,6 +1,7 @@
#include <test/jtx.h>
#include <test/jtx/ticket.h>
#include <test/jtx/trust.h>
#include <test/jtx/vault.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Protocol.h>
@@ -536,6 +537,85 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
}
}
void
testSet(FeatureBitset features)
{
testcase("Set");
using namespace test::jtx;
// Set keys on issuance that already has confidential amounts enabled
{
Env env{*this, features};
Account const alice("alice");
Account const auditor("auditor");
MPTTester mptAlice(env, alice, {.holders = {}, .auditor = auditor});
mptAlice.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanConfidentialAmount,
});
mptAlice.generateKeyPair(alice);
mptAlice.generateKeyPair(auditor);
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
.auditorPubKey = mptAlice.getPubKey(auditor),
});
}
// Enable confidential amounts flag only (no keys)
{
Env env{*this, features};
Account const alice("alice");
MPTTester mptAlice(env, alice, {.holders = {}});
mptAlice.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanLock,
});
mptAlice.set({
.account = alice,
.mutableFlags = tmfMPTSetCanConfidentialAmount,
});
}
// Set keys when enabling confidential amounts in the same tx
{
Env env{*this, features};
Account const alice("alice");
Account const auditor("auditor");
MPTTester mptAlice(env, alice, {.holders = {}, .auditor = auditor});
mptAlice.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanLock,
});
mptAlice.generateKeyPair(alice);
mptAlice.generateKeyPair(auditor);
mptAlice.set({
.account = alice,
.mutableFlags = tmfMPTSetCanConfidentialAmount,
.issuerPubKey = mptAlice.getPubKey(alice),
.auditorPubKey = mptAlice.getPubKey(auditor),
});
// Verify lsfMPTCanConfidentialAmount flag is set
BEAST_EXPECT(mptAlice.checkFlags(
lsfMPTCanTransfer | lsfMPTCanLock | lsfMPTCanConfidentialAmount));
// Verify keys are persisted on the issuance
auto const sle = env.le(keylet::mptIssuance(mptAlice.issuanceID()));
BEAST_EXPECT(sle);
BEAST_EXPECT(sle->isFieldPresent(sfIssuerEncryptionKey));
BEAST_EXPECT(sle->isFieldPresent(sfAuditorEncryptionKey));
}
}
void
testSetPreflight(FeatureBitset features)
{
@@ -633,6 +713,14 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.err = temMALFORMED,
});
// Cannot set keys while clearing confidential amount
mptAlice.set({
.account = alice,
.mutableFlags = tmfMPTClearCanConfidentialAmount,
.issuerPubKey = mptAlice.getPubKey(alice),
.err = temINVALID_FLAG,
});
// Cannot set Holder and auditor Keys in the same transaction
mptAlice.set({
.account = alice,
@@ -641,31 +729,174 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.err = temMALFORMED,
});
}
}
// issuance has disabled confidential transfer
void
testSetPreclaim(FeatureBitset features)
{
testcase("Set preclaim");
using namespace test::jtx;
// Cannot set issuer key if confidential amounts not enabled
{
Env env{*this, features};
Account const alice("alice");
MPTTester mptAlice(env, alice, {.holders = {}});
mptAlice.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanLock,
});
mptAlice.generateKeyPair(alice);
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
.err = tecNO_PERMISSION,
});
}
// Cannot update issuer public key once set
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
MPTTester mptAlice(env, alice, {.holders = {bob}});
// no tfMPTCanConfidentialAmount flag enabled
mptAlice.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanConfidentialAmount,
});
mptAlice.generateKeyPair(alice);
mptAlice.generateKeyPair(bob);
// First set issuer key - should succeed
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
});
// Try to update issuer key - should fail
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(bob),
.err = tecNO_PERMISSION,
});
}
// Cannot update issuer and auditor public keys once set
// Note: trying to set only auditor key fails in preflight (temMALFORMED)
// so we must provide both keys, which fails on issuer key check first
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
Account const auditor("auditor");
MPTTester mptAlice(env, alice, {.holders = {bob}, .auditor = auditor});
mptAlice.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanConfidentialAmount,
});
mptAlice.generateKeyPair(alice);
mptAlice.generateKeyPair(bob);
mptAlice.generateKeyPair(auditor);
// Set issuer and auditor keys - should succeed
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
.auditorPubKey = mptAlice.getPubKey(auditor),
});
// Try to update both keys - fails on issuer key check first
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(bob),
.auditorPubKey = mptAlice.getPubKey(alice),
.err = tecNO_PERMISSION,
});
}
// Cannot set auditor key if confidential amounts not enabled
{
Env env{*this, features};
Account const alice("alice");
Account const auditor("auditor");
MPTTester mptAlice(env, alice, {.holders = {}, .auditor = auditor});
mptAlice.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanLock,
});
mptAlice.authorize({
.account = bob,
});
mptAlice.pay(alice, bob, 100);
mptAlice.generateKeyPair(alice);
mptAlice.generateKeyPair(bob);
mptAlice.generateKeyPair(auditor);
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
.auditorPubKey = mptAlice.getPubKey(auditor),
.err = tecNO_PERMISSION,
});
}
// Cannot set keys when mutation of canConfidentialAmount is disallowed
{
Env env{*this, features};
Account const alice("alice");
MPTTester mptAlice(env, alice, {.holders = {}});
// Create with tmfMPTCannotMutateCanConfidentialAmount
mptAlice.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanLock,
.mutableFlags = tmfMPTCannotMutateCanConfidentialAmount,
});
mptAlice.generateKeyPair(alice);
// Trying to enable confidential amounts and set keys fails
// because the issuance cannot mutate canConfidentialAmount
mptAlice.set({
.account = alice,
.mutableFlags = tmfMPTSetCanConfidentialAmount,
.issuerPubKey = mptAlice.getPubKey(alice),
.err = tecNO_PERMISSION,
});
}
// Set issuer key first, then auditor key in a separate tx
{
Env env{*this, features};
Account const alice("alice");
Account const auditor("auditor");
MPTTester mptAlice(env, alice, {.holders = {}, .auditor = auditor});
mptAlice.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanConfidentialAmount,
});
mptAlice.generateKeyPair(alice);
mptAlice.generateKeyPair(auditor);
// Set issuer key only
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
});
// Set auditor key in a separate tx - requires issuer key in tx
// (preflight enforces auditor key requires issuer key)
// This fails because issuer key is already set on ledger
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
.auditorPubKey = mptAlice.getPubKey(auditor),
.err = tecNO_PERMISSION,
});
}
@@ -1340,6 +1571,116 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.err = tecNO_PERMISSION,
});
}
// holder is locked
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanConfidentialAmount,
});
mptAlice.authorize({
.account = bob,
});
mptAlice.pay(alice, bob, 100);
mptAlice.generateKeyPair(alice);
mptAlice.generateKeyPair(bob);
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
mptAlice.convert({
.account = bob,
.amt = 50,
.holderPubKey = mptAlice.getPubKey(bob),
});
// lock bob
mptAlice.set({
.account = alice,
.holder = bob,
.flags = tfMPTLock,
});
mptAlice.mergeInbox({
.account = bob,
.err = tecLOCKED,
});
// unlock bob
mptAlice.set({
.account = alice,
.holder = bob,
.flags = tfMPTUnlock,
});
// should succeed now
mptAlice.mergeInbox({
.account = bob,
});
}
// holder not authorized
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({
.ownerCount = 1,
.flags =
tfMPTCanTransfer | tfMPTCanLock | tfMPTCanConfidentialAmount | tfMPTRequireAuth,
});
mptAlice.authorize({
.account = bob,
});
mptAlice.authorize({
.account = alice,
.holder = bob,
});
mptAlice.pay(alice, bob, 100);
mptAlice.generateKeyPair(alice);
mptAlice.generateKeyPair(bob);
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
mptAlice.convert({
.account = bob,
.amt = 50,
.holderPubKey = mptAlice.getPubKey(bob),
});
// unauthorize bob
mptAlice.authorize({
.account = alice,
.holder = bob,
.flags = tfMPTUnauthorize,
});
mptAlice.mergeInbox({
.account = bob,
.err = tecNO_AUTH,
});
// authorize bob again
mptAlice.authorize({
.account = alice,
.holder = bob,
});
// should succeed now
mptAlice.mergeInbox({
.account = bob,
});
}
}
void
@@ -2599,6 +2940,85 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.flags = tfMPTUnauthorize,
});
}
// removeEmptyHolding: vault share MPToken with confidential balance
// fields should not be deleted on VaultWithdraw
{
Env env{*this, features | featureSingleAssetVault};
Account const issuer("issuer");
Account const owner("owner");
Account const depositor("depositor");
MPTTester mptt{env, issuer, {.holders = {owner, depositor}}};
mptt.create({
.flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanClawback,
});
PrettyAsset asset = mptt.issuanceID();
mptt.authorize({.account = owner});
mptt.authorize({.account = depositor});
env(pay(issuer, depositor, asset(1000)));
env.close();
test::jtx::Vault vault{env};
auto [tx, vaultKeylet] = vault.create({.owner = owner, .asset = asset});
env(tx);
env.close();
// Get the share MPTID from vault
auto const vaultSle = env.le(vaultKeylet);
BEAST_EXPECT(vaultSle != nullptr);
MPTID share = vaultSle->at(sfShareMPTID);
// Depositor deposits into vault
tx = vault.deposit(
{.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)});
env(tx);
env.close();
// Verify depositor has share tokens
auto shareMpt = env.le(keylet::mptoken(share, depositor.id()));
BEAST_EXPECT(shareMpt != nullptr);
// Inject confidential balance fields on the share MPToken
// to simulate a scenario where vault shares somehow have
// confidential balances
env.app().openLedger().modify([&](OpenView& view, beast::Journal) {
// Set lsfMPTCanConfidentialAmount on the share issuance
// so the invariant allows encrypted fields on the MPToken
auto issuance = std::const_pointer_cast<SLE>(view.read(keylet::mptIssuance(share)));
if (!issuance)
return false;
issuance->setFlag(lsfMPTCanConfidentialAmount);
view.rawReplace(issuance);
auto const k = keylet::mptoken(share, depositor.id());
auto sle = std::const_pointer_cast<SLE>(view.read(k));
if (!sle)
return false;
// Inject dummy confidential balance fields
Buffer dummyCiphertext(ecGamalEncryptedTotalLength);
std::memset(dummyCiphertext.data(), 0, ecGamalEncryptedTotalLength);
dummyCiphertext.data()[0] = ecCompressedPrefixEvenY;
dummyCiphertext.data()[ecGamalEncryptedLength] = ecCompressedPrefixEvenY;
dummyCiphertext.data()[ecGamalEncryptedLength - 1] = 0x01;
dummyCiphertext.data()[ecGamalEncryptedTotalLength - 1] = 0x01;
sle->setFieldVL(sfConfidentialBalanceSpending, dummyCiphertext);
sle->setFieldVL(sfConfidentialBalanceInbox, dummyCiphertext);
sle->setFieldVL(sfIssuerEncryptedBalance, dummyCiphertext);
view.rawReplace(sle);
return true;
});
// Withdraw everything - which should fail because of the confidential balance fields
tx = vault.withdraw(
{.depositor = depositor, .id = vaultKeylet.key, .amount = asset(100)});
env(tx);
// The share MPToken should still exist because the
// withdrawal failed due to confidential balance obligations
shareMpt = env.le(keylet::mptoken(share, depositor.id()));
BEAST_EXPECT(shareMpt != nullptr);
}
}
void
@@ -6102,7 +6522,9 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
testMergeInboxPreflight(features);
testMergeInboxPreclaim(features);
testSet(features);
testSetPreflight(features);
testSetPreclaim(features);
// ConfidentialMPTSend
testSend(features);