Add equality proof to ConvertBack and refactor to reduce redundancy (#6220)

This commit is contained in:
Shawn Xie
2026-01-16 10:28:55 -05:00
committed by GitHub
parent fa055c2bd5
commit ec6d7cb91d
8 changed files with 364 additions and 139 deletions

View File

@@ -47,6 +47,13 @@ getConvertContextHash(
uint192 const& issuanceID,
std::uint64_t amount);
uint256
getConvertBackContextHash(
AccountID const& account,
std::uint32_t sequence,
uint192 const& issuanceID,
std::uint64_t amount,
std::uint32_t version);
/**
* @brief Generates a new secp256k1 key pair.
*/
@@ -286,6 +293,39 @@ getEqualityProofs(Slice const& zkp);
NotTEC
checkEncryptedAmountFormat(STObject const& object);
// Helper struct to bundle the ElGamal Public Key and the associated Ciphertext
struct EncryptedAmountInfo
{
Slice const publicKey;
Slice const encryptedAmount;
};
/**
* Verifies equality proofs for Holder, Issuer, and optionally Auditor.
*/
TER
verifyEqualityProofs(
std::uint64_t amount,
std::vector<Buffer> const& zkps,
EncryptedAmountInfo const& holder,
EncryptedAmountInfo const& issuer,
std::optional<EncryptedAmountInfo> const& auditor,
uint256 const& contextHash);
// returns the number of entries
size_t inline getEqualityProofSize(bool const hasAuditor)
{
// Be careful if we ever need to change the numbers below, it will be a
// breaking change!
return (hasAuditor ? 3 : 2);
}
// returns the total byte length of all the equality proofs combined
size_t inline getEqualityProofLength(bool const hasAuditor)
{
return getEqualityProofSize(hasAuditor) * ecEqualityProofLength;
}
} // namespace ripple
#endif

View File

@@ -53,6 +53,23 @@ getConvertContextHash(
return s.getSHA512Half();
}
uint256
getConvertBackContextHash(
AccountID const& account,
std::uint32_t sequence,
uint192 const& issuanceID,
std::uint64_t amount,
std::uint32_t version)
{
Serializer s;
addCommonZKPFields(
s, ttCONFIDENTIAL_CONVERT_BACK, account, sequence, issuanceID, amount);
s.addInteger(version);
return s.getSHA512Half();
}
int
secp256k1_elgamal_generate_keypair(
secp256k1_context const* ctx,
@@ -1081,4 +1098,51 @@ checkEncryptedAmountFormat(STObject const& object)
return tesSUCCESS;
}
TER
verifyEqualityProofs(
std::uint64_t amount,
std::vector<Buffer> const& zkps,
EncryptedAmountInfo const& holder,
EncryptedAmountInfo const& issuer,
std::optional<EncryptedAmountInfo> const& auditor,
uint256 const& contextHash)
{
// Sanity check: Ensure we have enough proofs
size_t const required = getEqualityProofSize(auditor.has_value());
if (zkps.size() != required)
return tecINTERNAL; // LCOV_EXCL_LINE
// 1. Verify Holder Proof (Index 0)
if (!isTesSuccess(verifyEqualityProof(
amount,
zkps[0],
holder.publicKey,
holder.encryptedAmount,
contextHash)))
return tecBAD_PROOF;
// 2. Verify Issuer Proof (Index 1)
if (!isTesSuccess(verifyEqualityProof(
amount,
zkps[1],
issuer.publicKey,
issuer.encryptedAmount,
contextHash)))
return tecBAD_PROOF;
// 3. Verify Auditor Proof (Index 2) - if applicable
if (auditor)
{
if (!isTesSuccess(verifyEqualityProof(
amount,
zkps[2],
auditor->publicKey,
auditor->encryptedAmount,
contextHash)))
return tecBAD_PROOF;
}
return tesSUCCESS;
}
} // namespace ripple

View File

@@ -1644,7 +1644,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
mptAlice.convertBack({
.account = bob,
.amt = 30,
.proof = "123",
});
}
@@ -1693,7 +1692,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
mptAlice.convertBack({
.account = bob,
.amt = 30,
.proof = "123",
});
}
@@ -1723,10 +1721,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
mptAlice.generateKeyPair(bob);
mptAlice.convertBack(
{.account = bob,
.amt = 30,
.proof = "123",
.err = temDISABLED});
{.account = bob, .amt = 30, .err = temDISABLED});
}
{
@@ -1763,41 +1758,31 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
});
mptAlice.convertBack(
{.account = alice,
.amt = 30,
.proof = "123",
.err = temMALFORMED});
{.account = alice, .amt = 30, .err = temMALFORMED});
mptAlice.convertBack(
{.account = bob,
.amt = 0,
.proof = "123",
.err = temBAD_AMOUNT});
{.account = bob, .amt = 0, .err = temBAD_AMOUNT});
mptAlice.convertBack(
{.account = bob,
.amt = maxMPTokenAmount + 1,
.proof = "123",
.err = temBAD_AMOUNT});
mptAlice.convertBack(
{.account = bob,
.amt = 30,
.proof = "123",
.holderEncryptedAmt = Buffer{},
.err = temBAD_CIPHERTEXT});
mptAlice.convertBack(
{.account = bob,
.amt = 30,
.proof = "123",
.issuerEncryptedAmt = Buffer{},
.err = temBAD_CIPHERTEXT});
mptAlice.convertBack(
{.account = bob,
.amt = 30,
.proof = "123",
.holderEncryptedAmt =
Buffer{badCiphertext, ecGamalEncryptedTotalLength},
.err = temBAD_CIPHERTEXT});
@@ -1805,7 +1790,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
mptAlice.convertBack(
{.account = bob,
.amt = 30,
.proof = "123",
.issuerEncryptedAmt =
Buffer{badCiphertext, ecGamalEncryptedTotalLength},
.err = temBAD_CIPHERTEXT});
@@ -1840,10 +1824,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
mptAlice.generateKeyPair(bob);
mptAlice.convertBack(
{.account = bob,
.amt = 30,
.proof = "123",
.err = tecOBJECT_NOT_FOUND});
{.account = bob, .amt = 30, .err = tecOBJECT_NOT_FOUND});
}
// tfMPTCanPrivacy is not set on issuance
@@ -1867,10 +1848,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
mptAlice.generateKeyPair(bob);
mptAlice.convertBack(
{.account = bob,
.amt = 30,
.proof = "123",
.err = tecNO_PERMISSION});
{.account = bob, .amt = 30, .err = tecNO_PERMISSION});
}
// no mptoken
@@ -1892,10 +1870,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
{.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
mptAlice.convertBack(
{.account = bob,
.amt = 30,
.proof = "123",
.err = tecOBJECT_NOT_FOUND});
{.account = bob, .amt = 30, .err = tecOBJECT_NOT_FOUND});
}
// bob doesn't have encrypted balances
@@ -1923,10 +1898,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
mptAlice.generateKeyPair(bob);
mptAlice.convertBack(
{.account = bob,
.amt = 30,
.proof = "123",
.err = tecNO_PERMISSION});
{.account = bob, .amt = 30, .err = tecNO_PERMISSION});
}
// bob tries to convert back more than COA
@@ -1974,10 +1946,7 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
});
mptAlice.convertBack(
{.account = bob,
.amt = 300,
.proof = "123",
.err = tecINSUFFICIENT_FUNDS});
{.account = bob, .amt = 300, .err = tecINSUFFICIENT_FUNDS});
}
// cannot convert if locked or unauth
@@ -2014,19 +1983,21 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
mptAlice.mergeInbox({.account = bob});
mptAlice.set({.account = alice, .holder = bob, .flags = tfMPTLock});
mptAlice.convertBack(
{.account = bob, .amt = 10, .proof = "123", .err = tecLOCKED});
mptAlice.convertBack({.account = bob, .amt = 10, .err = tecLOCKED});
mptAlice.set(
{.account = alice, .holder = bob, .flags = tfMPTUnlock});
mptAlice.convertBack({.account = bob, .amt = 10, .proof = "123"});
mptAlice.convertBack({
.account = bob,
.amt = 10,
});
mptAlice.authorize(
{.account = alice, .holder = bob, .flags = tfMPTUnauthorize});
mptAlice.convertBack(
{.account = bob, .amt = 10, .proof = "123", .err = tecNO_AUTH});
{.account = bob, .amt = 10, .err = tecNO_AUTH});
mptAlice.authorize({
.account = alice,
@@ -2036,7 +2007,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
mptAlice.convertBack({
.account = bob,
.amt = 10,
.proof = "123",
});
}
}
@@ -2926,8 +2896,10 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
// make sure there's no confidential outstanding balance
// for the next toggle test
mptAlice.convertBack(
{.account = bob, .amt = amt, .proof = "123"});
mptAlice.convertBack({
.account = bob,
.amt = amt,
});
}
};
@@ -2997,7 +2969,10 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
});
// bob convert back all confidential balance
mptAlice.convertBack({.account = bob, .amt = 50, .proof = "123"});
mptAlice.convertBack({
.account = bob,
.amt = 50,
});
// now clear lsfMPTCanPrivacy should succeed,
// because there's no confidential outstanding balance

View File

@@ -805,13 +805,13 @@ MPTTester::getClawbackProof(
}
Buffer
MPTTester::getConvertProof(
MPTTester::generateEqualityZKP(
Account const& holder,
std::uint64_t amount,
uint256 const& ctxHash,
CiphertextComponents holderCiphertext,
CiphertextComponents issuerCiphertext,
std::optional<CiphertextComponents> auditorCiphertext) const
CiphertextComponents const& holderCiphertext,
CiphertextComponents const& issuerCiphertext,
std::optional<CiphertextComponents> const& auditorCiphertext) const
{
if (!id_)
Throw<std::runtime_error>("MPT has not been created");
@@ -908,6 +908,46 @@ MPTTester::getConvertProof(
return zkp;
}
Buffer
MPTTester::getConvertProof(
Account const& holder,
std::uint64_t amount,
uint256 const& ctxHash,
CiphertextComponents holderCiphertext,
CiphertextComponents issuerCiphertext,
std::optional<CiphertextComponents> auditorCiphertext) const
{
return generateEqualityZKP(
holder,
amount,
ctxHash,
holderCiphertext,
issuerCiphertext,
auditorCiphertext);
}
Buffer
MPTTester::getConvertBackProof(
Account const& holder,
std::uint64_t amount,
uint256 const& ctxHash,
CiphertextComponents holderCiphertext,
CiphertextComponents issuerCiphertext,
std::optional<CiphertextComponents> auditorCiphertext) const
{
Buffer const equalityZkp = generateEqualityZKP(
holder,
amount,
ctxHash,
holderCiphertext,
issuerCiphertext,
auditorCiphertext);
// todo: incoporate pederson and range proof
return equalityZkp;
}
std::optional<Buffer>
MPTTester::getEncryptedBalance(
Account const& account,
@@ -1514,6 +1554,22 @@ MPTTester::getIssuanceOutstandingBalance() const
return (*sle)[sfOutstandingAmount];
}
std::uint32_t
MPTTester::getMPTokenVersion(Account const account) const
{
if (!id_)
Throw<std::runtime_error>("Issuance ID does not exist");
auto const sle = env_.current()->read(keylet::mptoken(*id_, account));
// return 0 here instead of throwing an exception since tests for
// preclaim will check if the MPToken exists
if (!sle)
return 0;
return (*sle)[~sfConfidentialBalanceVersion].value_or(0);
}
void
MPTTester::convertBack(MPTConvertBack const& arg)
{
@@ -1535,17 +1591,26 @@ MPTTester::convertBack(MPTConvertBack const& arg)
if (arg.amt)
jv[sfMPTAmount.jsonName] = std::to_string(*arg.amt);
CiphertextComponents holderCiphertext;
if (arg.holderEncryptedAmt)
jv[sfHolderEncryptedAmount.jsonName] = strHex(*arg.holderEncryptedAmt);
else
{
holderCiphertext = encryptAmount(*arg.account, *arg.amt);
jv[sfHolderEncryptedAmount.jsonName] =
strHex(encryptAmount(*arg.account, *arg.amt).ciphertext);
strHex(holderCiphertext.ciphertext);
}
CiphertextComponents issuerCiphertext;
if (arg.issuerEncryptedAmt)
jv[sfIssuerEncryptedAmount.jsonName] = strHex(*arg.issuerEncryptedAmt);
else
{
issuerCiphertext = encryptAmount(issuer_, *arg.amt);
jv[sfIssuerEncryptedAmount.jsonName] =
strHex(encryptAmount(issuer_, *arg.amt).ciphertext);
strHex(issuerCiphertext.ciphertext);
}
std::optional<CiphertextComponents> auditorCiphertext;
if (arg.auditorEncryptedAmt)
@@ -1560,6 +1625,23 @@ MPTTester::convertBack(MPTConvertBack const& arg)
if (arg.proof)
jv[sfZKProof.jsonName] = *arg.proof;
else
{
auto const version = getMPTokenVersion(*arg.account);
// if the caller generated ciphertexts themselves, they should also
// generate the proof themselves from the randomness factor
uint256 const ctxHash = getConvertBackContextHash(
arg.account->id(), env_.seq(*arg.account), *id_, *arg.amt, version);
Buffer proof = getConvertBackProof(
*arg.account,
*arg.amt,
ctxHash,
holderCiphertext,
issuerCiphertext,
auditorCiphertext);
jv[sfZKProof] = strHex(proof);
}
auto const holderAmt = getBalance(*arg.account);
auto const prevConfidentialOutstanding = getIssuanceConfidentialBalance();

View File

@@ -437,6 +437,15 @@ public:
Buffer const& privateKey,
uint256 const& txHash) const;
Buffer
generateEqualityZKP(
Account const& holder,
std::uint64_t amount,
uint256 const& ctxHash,
CiphertextComponents const& holderCiphertext,
CiphertextComponents const& issuerCiphertext,
std::optional<CiphertextComponents> const& auditorCiphertext) const;
Buffer
getConvertProof(
Account const& holder,
@@ -446,6 +455,18 @@ public:
CiphertextComponents issuerCiphertext,
std::optional<CiphertextComponents> auditorCiphertext) const;
Buffer
getConvertBackProof(
Account const& holder,
std::uint64_t amount,
uint256 const& ctxHash,
CiphertextComponents holderCiphertext,
CiphertextComponents issuerCiphertext,
std::optional<CiphertextComponents> auditorCiphertext) const;
std::uint32_t
getMPTokenVersion(Account const account) const;
private:
using SLEP = SLE::const_pointer;
bool

View File

@@ -31,9 +31,8 @@ ConfidentialConvert::preflight(PreflightContext const& ctx)
ctx.tx[sfHolderElGamalPublicKey].length() != ecPubKeyLength)
return temMALFORMED;
auto const expectedCount =
ctx.tx.isFieldPresent(sfAuditorEncryptedAmount) ? 3 : 2;
if (ctx.tx[sfZKProof].size() != expectedCount * ecEqualityProofLength)
if (ctx.tx[sfZKProof].size() !=
getEqualityProofLength(ctx.tx.isFieldPresent(sfAuditorEncryptedAmount)))
return temMALFORMED;
return tesSUCCESS;
@@ -61,9 +60,17 @@ ConfidentialConvert::preclaim(PreclaimContext const& ctx)
return tecNO_PERMISSION;
bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
bool const requiresAuditor =
sleIssuance->isFieldPresent(sfAuditorElGamalPublicKey);
// tx must include auditor ciphertext if the issuance has enabled auditing
if (sleIssuance->isFieldPresent(sfAuditorElGamalPublicKey) && !hasAuditor)
// tx must include auditor ciphertext if the issuance has enabled
// auditing
if (requiresAuditor && !hasAuditor)
return tecNO_PERMISSION;
// if auditing is not supported then user should not upload auditor
// ciphertext
if (!requiresAuditor && hasAuditor)
return tecNO_PERMISSION;
auto const sleMptoken = ctx.view.read(
@@ -108,44 +115,26 @@ ConfidentialConvert::preclaim(PreclaimContext const& ctx)
std::vector<Buffer> const zkps = getEqualityProofs(ctx.tx[sfZKProof]);
auto const& amount = ctx.tx[sfMPTAmount];
// Prepare Auditor Info
std::optional<EncryptedAmountInfo> auditor;
if (hasAuditor)
{
auditor.emplace(
EncryptedAmountInfo{
(*sleIssuance)[sfAuditorElGamalPublicKey],
ctx.tx[sfAuditorEncryptedAmount]});
}
// we already checked proof size in preflight, still do sanity check here
// since we are going to access individual vector entries
auto const expectedCount = ctx.tx[sfZKProof].size() / ecEqualityProofLength;
if (zkps.size() != expectedCount)
return tecINTERNAL; // LCOV_EXCL_LINE
// check equality proof
if (!isTesSuccess(verifyEqualityProof(
amount,
zkps[0],
holderPubKey,
ctx.tx[sfHolderEncryptedAmount],
contextHash)) ||
!isTesSuccess(verifyEqualityProof(
amount,
zkps[1],
return verifyEqualityProofs(
ctx.tx[sfMPTAmount],
zkps,
EncryptedAmountInfo{
holderPubKey, ctx.tx[sfHolderEncryptedAmount]}, // Holder
EncryptedAmountInfo{
(*sleIssuance)[sfIssuerElGamalPublicKey],
ctx.tx[sfIssuerEncryptedAmount],
contextHash)))
{
return tecBAD_PROOF;
}
// Verify Auditor proof if present
if (hasAuditor &&
!isTesSuccess(verifyEqualityProof(
amount,
zkps[2],
(*sleIssuance)[sfAuditorElGamalPublicKey],
ctx.tx[sfAuditorEncryptedAmount],
contextHash)))
{
return tecBAD_PROOF;
}
return tesSUCCESS;
ctx.tx[sfIssuerEncryptedAmount]}, // Issuer
auditor,
contextHash);
}
TER

View File

@@ -8,8 +8,20 @@
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <cstddef>
namespace ripple {
size_t
expectedProofLength(std::shared_ptr<SLE const> const& issuance)
{
auto const equalityProofLength = getEqualityProofLength(
issuance->isFieldPresent(sfAuditorElGamalPublicKey));
// todo: add pederson and range proof length
return equalityProofLength;
}
NotTEC
ConfidentialConvertBack::preflight(PreflightContext const& ctx)
{
@@ -26,10 +38,57 @@ ConfidentialConvertBack::preflight(PreflightContext const& ctx)
if (ctx.tx[sfMPTAmount] == 0 || ctx.tx[sfMPTAmount] > maxMPTokenAmount)
return temBAD_AMOUNT;
// 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
verifyProofs(
STTx const& tx,
std::shared_ptr<SLE const> const& issuance,
std::shared_ptr<SLE const> const& mptoken)
{
if (expectedProofLength(issuance) != tx[sfZKProof].size())
return tecBAD_PROOF;
auto const mptIssuanceID = tx[sfMPTokenIssuanceID];
auto const account = tx[sfAccount];
auto const amount = tx[sfMPTAmount];
auto const contextHash = getConvertBackContextHash(
account,
tx[sfSequence],
mptIssuanceID,
amount,
(*mptoken)[~sfConfidentialBalanceVersion].value_or(0));
// Prepare Auditor Info
std::optional<EncryptedAmountInfo> auditor;
bool const hasAuditor = issuance->isFieldPresent(sfAuditorElGamalPublicKey);
if (hasAuditor)
{
auditor.emplace(
EncryptedAmountInfo{
(*issuance)[sfAuditorElGamalPublicKey],
tx[sfAuditorEncryptedAmount]});
}
// verify equality proofs
{
auto const equalityZkps = getEqualityProofs(
Slice{tx[sfZKProof].data(), getEqualityProofLength(hasAuditor)});
return verifyEqualityProofs(
amount,
equalityZkps,
EncryptedAmountInfo{
(*mptoken)[sfHolderElGamalPublicKey],
tx[sfHolderEncryptedAmount]}, // Holder
EncryptedAmountInfo{
(*issuance)[sfIssuerElGamalPublicKey],
tx[sfIssuerEncryptedAmount]}, // Issuer
auditor, // Optional auditor
contextHash);
}
return tesSUCCESS;
}
@@ -37,27 +96,39 @@ ConfidentialConvertBack::preflight(PreflightContext const& ctx)
TER
ConfidentialConvertBack::preclaim(PreclaimContext const& ctx)
{
auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID];
auto const account = ctx.tx[sfAccount];
auto const amount = ctx.tx[sfMPTAmount];
// ensure that issuance exists
auto const sleIssuance =
ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID]));
auto const sleIssuance = ctx.view.read(keylet::mptIssuance(mptIssuanceID));
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
if (!sleIssuance->isFlag(lsfMPTCanPrivacy))
return tecNO_PERMISSION;
// tx must include auditor ciphertext if the issuance has enabled auditing
if (sleIssuance->isFieldPresent(sfAuditorElGamalPublicKey) &&
!ctx.tx.isFieldPresent(sfAuditorEncryptedAmount))
bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
bool const requiresAuditor =
sleIssuance->isFieldPresent(sfAuditorElGamalPublicKey);
// tx must include auditor ciphertext if the issuance has enabled
// auditing
if (requiresAuditor && !hasAuditor)
return tecNO_PERMISSION;
// if auditing is not supported then user should not upload auditor
// ciphertext
if (!requiresAuditor && hasAuditor)
return tecNO_PERMISSION;
// already checked in preflight, but should also check that issuer on
// the issuance isn't the account either
if (sleIssuance->getAccountID(sfIssuer) == ctx.tx[sfAccount])
if (sleIssuance->getAccountID(sfIssuer) == account)
return tefINTERNAL; // LCOV_EXCL_LINE
auto const sleMptoken = ctx.view.read(
keylet::mptoken(ctx.tx[sfMPTokenIssuanceID], ctx.tx[sfAccount]));
auto const sleMptoken =
ctx.view.read(keylet::mptoken(mptIssuanceID, account));
if (!sleMptoken)
return tecOBJECT_NOT_FOUND;
@@ -70,15 +141,11 @@ ConfidentialConvertBack::preclaim(PreclaimContext const& ctx)
// 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])
if ((*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0) < amount)
{
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);
@@ -90,31 +157,9 @@ ConfidentialConvertBack::preclaim(PreclaimContext const& ctx)
!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
if (TER const res = verifyProofs(ctx.tx, sleIssuance, sleMptoken);
!isTesSuccess(res))
return res;
return tesSUCCESS;
}

View File

@@ -88,9 +88,18 @@ ConfidentialSend::preclaim(PreclaimContext const& ctx)
if (!sleIssuance->isFieldPresent(sfIssuerElGamalPublicKey))
return tecNO_PERMISSION;
// tx must include auditor ciphertext if the issuance has enabled auditing
if (sleIssuance->isFieldPresent(sfAuditorElGamalPublicKey) &&
!ctx.tx.isFieldPresent(sfAuditorEncryptedAmount))
bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
bool const requiresAuditor =
sleIssuance->isFieldPresent(sfAuditorElGamalPublicKey);
// tx must include auditor ciphertext if the issuance has enabled
// auditing
if (requiresAuditor && !hasAuditor)
return tecNO_PERMISSION;
// if auditing is not supported then user should not upload auditor
// ciphertext
if (!requiresAuditor && hasAuditor)
return tecNO_PERMISSION;
// already checked in preflight, but should also check that issuer on the