Support Pedersen-ElGamal linkage for ConfidentialSend (#6289)

* support Pedersen Amount commitment for ConfidentialSend
* support Pedersen Balance commitment for ConfidentialSend
This commit is contained in:
yinyiqian1
2026-01-29 11:18:46 -05:00
committed by GitHub
parent 446f9fbe6d
commit fc8b7898c5
9 changed files with 548 additions and 191 deletions

View File

@@ -163,8 +163,49 @@ verifyClawbackEqualityProof(
Buffer
generateBlindingFactor();
/**
* @brief Verifies the cryptographic link between an ElGamal Ciphertext and a
* Pedersen Commitment for a transaction Amount.
*
* 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.
*/
TER
verifyPedersenLinkage(
verifyAmountPcmLinkage(
Slice const& proof,
Slice const& encAmt,
Slice const& pubKeySlice,
Slice const& pcmSlice,
uint256 const& contextHash);
/**
* @brief Verifies the cryptographic link between an ElGamal Ciphertext and a
* Pedersen Commitment for an account Balance.
*
* 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)
*
* Note: Swaps arguments (Pk <-> C1) to accommodate the different algebraic
* structure.
*
* @param proof The Zero Knowledge Proof bytes.
* @param encAmt The ElGamal ciphertext of the balance (C1, C2).
* @param pubKeySlice The sender's public key.
* @param pcmSlice The Pedersen Commitment to the balance.
* @param contextHash The unique context hash for this transaction.
* @return tesSUCCESS if the proof is valid, or an error code otherwise.
*/
TER
verifyBalancePcmLinkage(
Slice const& proof,
Slice const& encAmt,
Slice const& pubKeySlice,

View File

@@ -313,7 +313,8 @@ TYPED_SFIELD(sfAuditorEncryptedBalance, VL, 42)
TYPED_SFIELD(sfAuditorEncryptedAmount, VL, 43)
TYPED_SFIELD(sfAuditorElGamalPublicKey, VL, 44)
TYPED_SFIELD(sfBlindingFactor, VL, 45)
TYPED_SFIELD(sfPedersenCommitment, VL, 46)
TYPED_SFIELD(sfAmountCommitment, VL, 46)
TYPED_SFIELD(sfBalanceCommitment, VL, 47)
// account (common)
TYPED_SFIELD(sfAccount, ACCOUNT, 1)

View File

@@ -1107,7 +1107,7 @@ TRANSACTION(ttCONFIDENTIAL_CONVERT_BACK, 87, ConfidentialConvertBack,
{sfAuditorEncryptedAmount, soeOPTIONAL},
{sfBlindingFactor, soeREQUIRED},
{sfZKProof, soeREQUIRED},
{sfPedersenCommitment, soeREQUIRED}
{sfBalanceCommitment, soeREQUIRED},
}))
#if TRANSACTION_INCLUDE
@@ -1123,9 +1123,11 @@ TRANSACTION(ttCONFIDENTIAL_SEND, 88, ConfidentialSend,
{sfSenderEncryptedAmount, soeREQUIRED},
{sfDestinationEncryptedAmount, soeREQUIRED},
{sfIssuerEncryptedAmount, soeREQUIRED},
{sfZKProof, soeREQUIRED},
{sfCredentialIDs, soeOPTIONAL},
{sfAuditorEncryptedAmount, soeOPTIONAL},
{sfZKProof, soeREQUIRED},
{sfAmountCommitment, soeREQUIRED},
{sfBalanceCommitment, soeREQUIRED},
{sfCredentialIDs, soeOPTIONAL},
}))
#if TRANSACTION_INCLUDE

View File

@@ -479,7 +479,48 @@ checkEncryptedAmountFormat(STObject const& object)
}
TER
verifyPedersenLinkage(
verifyAmountPcmLinkage(
Slice const& proof,
Slice const& encAmt,
Slice const& pubKeySlice,
Slice const& pcmSlice,
uint256 const& contextHash)
{
if (proof.length() != ecPedersenProofLength)
return tecINTERNAL;
secp256k1_pubkey c1, c2;
if (!makeEcPair(encAmt, c1, c2))
return tecINTERNAL; // LCOV_EXCL_LINE
secp256k1_pubkey pubKey;
if (pubKeySlice.size() != ecPubKeyLength)
return tecINTERNAL; // LCOV_EXCL_LINE
secp256k1_pubkey pcm;
if (pcmSlice.size() != ecPedersenCommitmentLength)
return tecINTERNAL; // LCOV_EXCL_LINE
std::memcpy(pubKey.data, pubKeySlice.data(), ecPubKeyLength);
std::memcpy(pcm.data, pcmSlice.data(), ecPedersenCommitmentLength);
if (secp256k1_elgamal_pedersen_link_verify(
secp256k1Context(),
proof.data(),
&c1,
&c2,
&pubKey,
&pcm,
contextHash.data()) != 1)
{
return tecBAD_PROOF;
}
return tesSUCCESS;
}
TER
verifyBalancePcmLinkage(
Slice const& proof,
Slice const& encAmt,
Slice const& pubKeySlice,
@@ -493,12 +534,17 @@ verifyPedersenLinkage(
secp256k1_pubkey c2;
if (!makeEcPair(encAmt, c1, c2))
return tecINTERNAL;
return tecINTERNAL; // LCOV_EXCL_LINE
secp256k1_pubkey pubKey;
std::memcpy(pubKey.data, pubKeySlice.data(), ecPubKeyLength);
if (pubKeySlice.size() != ecPubKeyLength)
return tecINTERNAL; // LCOV_EXCL_LINE
secp256k1_pubkey pcm;
if (pcmSlice.size() != ecPedersenCommitmentLength)
return tecINTERNAL; // LCOV_EXCL_LINE
std::memcpy(pubKey.data, pubKeySlice.data(), ecPubKeyLength);
std::memcpy(pcm.data, pcmSlice.data(), ecPubKeyLength);
if (secp256k1_elgamal_pedersen_link_verify(
@@ -509,7 +555,9 @@ verifyPedersenLinkage(
&c1,
&pcm,
contextHash.data()) != 1)
{
return tecBAD_PROOF;
}
return tesSUCCESS;
}

View File

@@ -48,6 +48,30 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
return trivialCiphertext;
}
static std::string const&
getTrivialSendProofHex(size_t nRecipients)
{
static std::string const trivialProofHex = [nRecipients]() {
size_t const sizeEquality =
getMultiCiphertextEqualityProofSize(nRecipients);
size_t const totalSize = sizeEquality + (2 * ecPedersenProofLength);
Buffer buf(totalSize);
std::memset(buf.data(), 0, totalSize);
for (std::size_t i = 0; i < totalSize; i += ecGamalEncryptedLength)
{
buf.data()[i] = 0x02;
if (i + ecGamalEncryptedLength - 1 < totalSize)
buf.data()[i + ecGamalEncryptedLength - 1] = 0x01;
}
return strHex(buf);
}();
return trivialProofHex;
}
void
testConvert(FeatureBitset features)
{
@@ -1026,12 +1050,9 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
{.account = bob,
.dest = carol,
.amt = 10,
.senderEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.destEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.issuerEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.senderEncryptedAmt = Buffer(ecGamalEncryptedTotalLength),
.destEncryptedAmt = Buffer(ecGamalEncryptedTotalLength),
.issuerEncryptedAmt = Buffer(ecGamalEncryptedTotalLength),
.err = temDISABLED});
}
@@ -1073,26 +1094,11 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
{.account = alice,
.dest = carol,
.amt = 10,
.senderEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.destEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.issuerEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.err = temMALFORMED});
// can not send to self
mptAlice.send(
{.account = bob,
.dest = bob,
.amt = 10,
.senderEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.destEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.issuerEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.err = temMALFORMED});
{.account = bob, .dest = bob, .amt = 10, .err = temMALFORMED});
// sender encrypted amount wrong length
mptAlice.send(
@@ -1100,59 +1106,51 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.dest = carol,
.amt = 10,
.senderEncryptedAmt = Buffer(10),
.destEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.issuerEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.err = temBAD_CIPHERTEXT});
// dest encrypted amount wrong length
mptAlice.send(
{.account = bob,
.dest = carol,
.amt = 10,
.senderEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.destEncryptedAmt = Buffer(10),
.issuerEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.err = temBAD_CIPHERTEXT});
// issuer encrypted amount wrong length
mptAlice.send(
{.account = bob,
.dest = carol,
.amt = 10,
.senderEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.destEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.issuerEncryptedAmt = Buffer(10),
.err = temBAD_CIPHERTEXT});
// auto const ciphertextHex = generatePlaceholderCiphertext();
// sender encrypted amount malformed
mptAlice.send(
{.account = bob,
.dest = carol,
.amt = 10,
.senderEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.proof = getTrivialSendProofHex(3),
.senderEncryptedAmt = Buffer(ecGamalEncryptedTotalLength),
.amountCommitment = Buffer(ecPedersenCommitmentLength),
.balanceCommitment = Buffer(ecPedersenCommitmentLength),
.err = temBAD_CIPHERTEXT});
// dest encrypted amount malformed
mptAlice.send(
{.account = bob,
.dest = carol,
.amt = 10,
.destEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.proof = getTrivialSendProofHex(3),
.destEncryptedAmt = Buffer(ecGamalEncryptedTotalLength),
.amountCommitment = Buffer(ecPedersenCommitmentLength),
.balanceCommitment = Buffer(ecPedersenCommitmentLength),
.err = temBAD_CIPHERTEXT});
// issuer encrypted amount malformed
mptAlice.send(
{.account = bob,
.dest = carol,
.amt = 10,
.issuerEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.proof = getTrivialSendProofHex(3),
.issuerEncryptedAmt = Buffer(ecGamalEncryptedTotalLength),
.amountCommitment = Buffer(ecPedersenCommitmentLength),
.balanceCommitment = Buffer(ecPedersenCommitmentLength),
.err = temBAD_CIPHERTEXT});
// invalid proof length
@@ -1160,13 +1158,29 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
{.account = bob,
.dest = carol,
.amt = 10,
.senderEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.destEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.issuerEncryptedAmt =
Buffer(ripple::ecGamalEncryptedTotalLength),
.proof = std::string(10, 'A'),
.amountCommitment = Buffer(ecPedersenCommitmentLength),
.balanceCommitment = Buffer(ecPedersenCommitmentLength),
.err = temMALFORMED});
// invalid amount Pedersen commitment length
mptAlice.send(
{.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.amountCommitment = Buffer(100),
.balanceCommitment = Buffer(ecPedersenCommitmentLength),
.err = temMALFORMED});
// invalid balance Pedersen commitment length
mptAlice.send(
{.account = bob,
.dest = carol,
.amt = 10,
.proof = getTrivialSendProofHex(3),
.amountCommitment = Buffer(ecPedersenCommitmentLength),
.balanceCommitment = Buffer(100),
.err = temMALFORMED});
}
}
@@ -1253,10 +1267,10 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
jv[sfSenderEncryptedAmount] = strHex(getTrivialCiphertext());
jv[sfDestinationEncryptedAmount] = strHex(getTrivialCiphertext());
jv[sfIssuerEncryptedAmount] = strHex(getTrivialCiphertext());
auto const dummyProofSize =
secp256k1_mpt_prove_same_plaintext_multi_size(3);
jv[sfZKProof.jsonName] = strHex(Buffer(dummyProofSize));
jv[sfAmountCommitment] = strHex(Buffer(ecPedersenCommitmentLength));
jv[sfBalanceCommitment] =
strHex(Buffer(ecPedersenCommitmentLength));
jv[sfZKProof] = getTrivialSendProofHex(3);
env(jv, ter(tecOBJECT_NOT_FOUND));
}
@@ -1268,8 +1282,12 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
{.account = bob,
.dest = unknown,
.amt = 10,
.senderEncryptedAmt = getTrivialCiphertext(),
.destEncryptedAmt = getTrivialCiphertext(),
.issuerEncryptedAmt = getTrivialCiphertext(),
.proof = getTrivialSendProofHex(3),
.amountCommitment = Buffer(ecPedersenCommitmentLength),
.balanceCommitment = Buffer(ecPedersenCommitmentLength),
.err = tecNO_TARGET});
}
@@ -1279,11 +1297,23 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
{.account = bob,
.dest = dave,
.amt = 10,
.senderEncryptedAmt = getTrivialCiphertext(),
.destEncryptedAmt = getTrivialCiphertext(),
.issuerEncryptedAmt = getTrivialCiphertext(),
.proof = getTrivialSendProofHex(3),
.amountCommitment = Buffer(ecPedersenCommitmentLength),
.balanceCommitment = Buffer(ecPedersenCommitmentLength),
.err = tecNO_PERMISSION});
mptAlice.send(
{.account = dave,
.dest = carol,
.amt = 10,
.senderEncryptedAmt = getTrivialCiphertext(),
.destEncryptedAmt = getTrivialCiphertext(),
.issuerEncryptedAmt = getTrivialCiphertext(),
.proof = getTrivialSendProofHex(3),
.amountCommitment = Buffer(ecPedersenCommitmentLength),
.balanceCommitment = Buffer(ecPedersenCommitmentLength),
.err = tecNO_PERMISSION});
}
@@ -1293,7 +1323,12 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
{.account = bob,
.dest = eve,
.amt = 10,
.senderEncryptedAmt = getTrivialCiphertext(),
.destEncryptedAmt = getTrivialCiphertext(),
.issuerEncryptedAmt = getTrivialCiphertext(),
.proof = getTrivialSendProofHex(3),
.amountCommitment = Buffer(ecPedersenCommitmentLength),
.balanceCommitment = Buffer(ecPedersenCommitmentLength),
.err = tecOBJECT_NOT_FOUND});
}
@@ -1468,14 +1503,11 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
.account = carol,
});
auto const dummyProofSize =
secp256k1_mpt_prove_same_plaintext_multi_size(3);
mptAlice.send(
{.account = bob,
.dest = carol,
.amt = 10,
.proof = strHex(Buffer(dummyProofSize)),
.proof = getTrivialSendProofHex(3),
.err = tecBAD_PROOF});
}
}

View File

@@ -819,11 +819,14 @@ MPTTester::getSchnorrProof(Account const& account, uint256 const& ctxHash) const
std::optional<Buffer>
MPTTester::getConfidentialSendProof(
Account const& sender,
std::uint64_t const amount,
std::vector<ConfidentialRecipient> const& recipients,
Slice const& blindingFactor,
std::size_t const nRecipients,
uint256 const& contextHash) const
uint256 const& contextHash,
PedersenProofParams const& amountParams,
PedersenProofParams const& balanceParams) const
{
if (recipients.size() != nRecipients)
return std::nullopt;
@@ -831,6 +834,10 @@ MPTTester::getConfidentialSendProof(
if (blindingFactor.size() != ecBlindingFactorLength)
return std::nullopt;
auto const senderPubKey = getPubKey(sender);
if (!senderPubKey)
return std::nullopt;
auto const ctx = secp256k1Context();
std::vector<secp256k1_pubkey> r(nRecipients);
@@ -874,14 +881,15 @@ MPTTester::getConfidentialSendProof(
blindingFactor.data() + ecBlindingFactorLength);
}
size_t proofLen =
size_t sizeEquality =
secp256k1_mpt_prove_same_plaintext_multi_size(nRecipients);
Buffer proof(proofLen);
Buffer equalityProof(sizeEquality);
// Get the multi-ciphertext equality proof
if (secp256k1_mpt_prove_same_plaintext_multi(
ctx,
proof.data(),
&proofLen,
equalityProof.data(),
&sizeEquality,
amount,
nRecipients,
r.data(),
@@ -893,6 +901,31 @@ MPTTester::getConfidentialSendProof(
return std::nullopt;
}
auto const amountLinkageProof = getAmountLinkageProof(
*senderPubKey,
Buffer(blindingFactor.data(), ecBlindingFactorLength),
contextHash,
amountParams);
auto const balanceLinkageProof = getBalanceLinkageProof(
sender, contextHash, *senderPubKey, balanceParams);
auto const sizeAmountLinkage = amountLinkageProof.size();
auto const sizeBalanceLinkage = balanceLinkageProof.size();
size_t const proofSize =
sizeEquality + sizeAmountLinkage + sizeBalanceLinkage;
Buffer proof(proofSize);
auto ptr = proof.data();
std::memcpy(ptr, equalityProof.data(), sizeEquality);
ptr += sizeEquality;
std::memcpy(ptr, amountLinkageProof.data(), sizeAmountLinkage);
ptr += sizeAmountLinkage;
std::memcpy(ptr, balanceLinkageProof.data(), sizeBalanceLinkage);
return proof;
}
@@ -941,7 +974,7 @@ MPTTester::getConvertBackProof(
auto const holderPubKey = getPubKey(holder);
if (holderPubKey)
{
Buffer const pedersenProof = generatePedersenLinkageProof(
Buffer const pedersenProof = getBalanceLinkageProof(
holder, contextHash, *holderPubKey, pcParams);
// todo: incoporate range proof
@@ -1264,7 +1297,72 @@ MPTTester::send(MPTConfidentialSend const& arg)
if (auditorAmt)
jv[sfAuditorEncryptedAmount] = strHex(*auditorAmt);
// fill in the proof if not provided
if (arg.credentials)
{
auto& arr(jv[sfCredentialIDs.jsonName] = Json::arrayValue);
for (auto const& hash : *arg.credentials)
arr.append(hash);
}
// Sender's previous confidential state
auto const prevSenderInbox =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
auto const prevSenderSpending =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
auto const prevSenderIssuer =
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
if (!prevSenderInbox || !prevSenderSpending || !prevSenderIssuer)
Throw<std::runtime_error>("Failed to get Pre-send balance");
std::optional<uint64_t> prevSenderAuditor;
if (arg.auditorEncryptedAmt || auditor_)
{
prevSenderAuditor =
getDecryptedBalance(*arg.account, AUDITOR_ENCRYPTED_BALANCE);
if (!prevSenderAuditor)
Throw<std::runtime_error>("Failed to get Pre-send balance");
}
// Destination's previous confidential state
auto const prevDestInbox =
getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_INBOX);
auto const prevDestSpending =
getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_SPENDING);
auto const prevDestIssuer =
getDecryptedBalance(*arg.dest, ISSUER_ENCRYPTED_BALANCE);
if (!prevDestInbox || !prevDestSpending || !prevDestIssuer)
Throw<std::runtime_error>("Failed to get Pre-send balance");
std::optional<uint64_t> prevDestAuditor;
if (arg.auditorEncryptedAmt || auditor_)
{
prevDestAuditor =
getDecryptedBalance(*arg.dest, AUDITOR_ENCRYPTED_BALANCE);
if (!prevDestAuditor)
Throw<std::runtime_error>("Failed to get Pre-send balance");
}
// Fill in the commitment if not provided
Buffer amountCommitment, balanceCommitment;
auto const amountBlindingFactor = generateBlindingFactor();
if (arg.amountCommitment)
amountCommitment = *arg.amountCommitment;
else
amountCommitment =
getPedersenCommitment(*arg.amt, amountBlindingFactor);
jv[sfAmountCommitment] = strHex(amountCommitment);
auto const balanceBlindingFactor = generateBlindingFactor();
if (arg.balanceCommitment)
balanceCommitment = *arg.balanceCommitment;
else
balanceCommitment =
getPedersenCommitment(*prevSenderSpending, balanceBlindingFactor);
jv[sfBalanceCommitment] = strHex(balanceCommitment);
// Fill in the proof if not provided
if (arg.proof)
jv[sfZKProof] = *arg.proof;
else
@@ -1306,8 +1404,33 @@ MPTTester::send(MPTConfidentialSend const& arg)
recipients.push_back({Slice(*auditorPubKey), *auditorAmt});
}
auto const proof = getConfidentialSendProof(
*arg.amt, recipients, blindingFactor, nRecipients, ctxHash);
auto const prevEncryptedSenderSpending =
getEncryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
std::optional<Buffer> proof;
// Skip proof generation if encrypted balance is missing (e.g.,
// feature disabled), or when the sender and destination are the
// same (malformed case causing pcm to be zero). This prevents a
// crash and allows certain error cases to be tested.
if (arg.account != arg.dest && prevEncryptedSenderSpending)
{
proof = getConfidentialSendProof(
*arg.account,
*arg.amt,
recipients,
blindingFactor,
nRecipients,
ctxHash,
{.pedersenCommitment = amountCommitment,
.amt = *arg.amt,
.encryptedAmt = senderAmt,
.blindingFactor = amountBlindingFactor},
{.pedersenCommitment = balanceCommitment,
.amt = *prevSenderSpending,
.encryptedAmt = *prevEncryptedSenderSpending,
.blindingFactor = balanceBlindingFactor});
}
if (proof)
jv[sfZKProof.jsonName] = strHex(*proof);
@@ -1320,55 +1443,11 @@ MPTTester::send(MPTConfidentialSend const& arg)
}
}
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
auto const prevSenderInbox =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_INBOX);
auto const prevSenderSpending =
getDecryptedBalance(*arg.account, HOLDER_ENCRYPTED_SPENDING);
auto const prevSenderIssuer =
getDecryptedBalance(*arg.account, ISSUER_ENCRYPTED_BALANCE);
if (!prevSenderInbox || !prevSenderSpending || !prevSenderIssuer)
Throw<std::runtime_error>("Failed to get Pre-send balance");
std::optional<uint64_t> prevSenderAuditor;
if (arg.auditorEncryptedAmt || auditor_)
{
prevSenderAuditor =
getDecryptedBalance(*arg.account, AUDITOR_ENCRYPTED_BALANCE);
if (!prevSenderAuditor)
Throw<std::runtime_error>("Failed to get Pre-send balance");
}
// Destination's previous confidential state
auto const prevDestInbox =
getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_INBOX);
auto const prevDestSpending =
getDecryptedBalance(*arg.dest, HOLDER_ENCRYPTED_SPENDING);
auto const prevDestIssuer =
getDecryptedBalance(*arg.dest, ISSUER_ENCRYPTED_BALANCE);
if (!prevDestInbox || !prevDestSpending || !prevDestIssuer)
Throw<std::runtime_error>("Failed to get Pre-send balance");
std::optional<uint64_t> prevDestAuditor;
if (arg.auditorEncryptedAmt || auditor_)
{
prevDestAuditor =
getDecryptedBalance(*arg.dest, AUDITOR_ENCRYPTED_BALANCE);
if (!prevDestAuditor)
Throw<std::runtime_error>("Failed to get Pre-send balance");
}
if (submit(arg, jv) == tesSUCCESS)
{
auto const postCOA = getIssuanceConfidentialBalance();
@@ -1797,7 +1876,7 @@ MPTTester::convertBack(MPTConvertBack const& arg)
pedersenCommitment =
getPedersenCommitment(*prevSpendingBalance, pcBlindingFactor);
jv[sfPedersenCommitment] = strHex(pedersenCommitment);
jv[sfBalanceCommitment] = strHex(pedersenCommitment);
if (arg.proof)
jv[sfZKProof.jsonName] = strHex(*arg.proof);
@@ -1908,7 +1987,60 @@ MPTTester::convertBack(MPTConvertBack const& arg)
}
Buffer
MPTTester::generatePedersenLinkageProof(
MPTTester::getAmountLinkageProof(
Buffer const& pubKey,
Buffer const& blindingFactor,
uint256 const& contextHash,
PedersenProofParams const& params) const
{
if (params.blindingFactor.size() != ecBlindingFactorLength ||
params.pedersenCommitment.size() != ecPedersenCommitmentLength ||
pubKey.size() != ecPubKeyLength ||
params.encryptedAmt.size() != ecGamalEncryptedTotalLength ||
blindingFactor.size() != ecBlindingFactorLength)
return Buffer(ecPedersenProofLength);
secp256k1_pubkey c1, c2;
auto const ctx = secp256k1Context();
if (!secp256k1_ec_pubkey_parse(
ctx, &c1, params.encryptedAmt.data(), ecGamalEncryptedLength) ||
!secp256k1_ec_pubkey_parse(
ctx,
&c2,
params.encryptedAmt.data() + ecGamalEncryptedLength,
ecGamalEncryptedLength))
{
return Buffer();
}
secp256k1_pubkey pk;
std::memcpy(pk.data, pubKey.data(), ecPubKeyLength);
secp256k1_pubkey pcm;
std::memcpy(
pcm.data, params.pedersenCommitment.data(), ecPedersenCommitmentLength);
Buffer proof(ecPedersenProofLength);
if (secp256k1_elgamal_pedersen_link_prove(
ctx,
proof.data(),
&c1,
&c2,
&pk,
&pcm,
params.amt,
blindingFactor.data(),
params.blindingFactor.data(),
contextHash.data()) != 1)
{
Throw<std::runtime_error>("Amount Linkage Proof generation failed");
}
return proof;
}
Buffer
MPTTester::getBalanceLinkageProof(
Account const& account,
uint256 const& contextHash,
Buffer const& pubKey,

View File

@@ -215,6 +215,8 @@ struct MPTConfidentialSend
std::optional<std::vector<std::string>> credentials = std::nullopt;
// not an txn param, only used for autofilling
std::optional<Buffer> blindingFactor = std::nullopt;
std::optional<Buffer> amountCommitment = std::nullopt;
std::optional<Buffer> balanceCommitment = 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;
@@ -465,11 +467,14 @@ public:
std::optional<Buffer>
getConfidentialSendProof(
Account const& sender,
std::uint64_t const amount,
std::vector<ConfidentialRecipient> const& recipients,
Slice const& blindingFactor,
std::size_t const nRecipients,
uint256 const& contextHash) const;
uint256 const& contextHash,
PedersenProofParams const& amountParams,
PedersenProofParams const& balanceParams) const;
Buffer
getConvertBackProof(
@@ -486,7 +491,14 @@ public:
getMPTokenVersion(Account const account) const;
Buffer
generatePedersenLinkageProof(
getAmountLinkageProof(
Buffer const& pubKey,
Buffer const& blindingFactor,
uint256 const& contextHash,
PedersenProofParams const& params) const;
Buffer
getBalanceLinkageProof(
Account const& account,
uint256 const& contextHash,
Buffer const& pubKey,

View File

@@ -28,7 +28,7 @@ ConfidentialConvertBack::preflight(PreflightContext const& ctx)
if (ctx.tx[sfBlindingFactor].size() != ecBlindingFactorLength)
return temMALFORMED;
if (ctx.tx[sfPedersenCommitment].size() != ecPedersenCommitmentLength)
if (ctx.tx[sfBalanceCommitment].size() != ecPedersenCommitmentLength)
return temMALFORMED;
// check encrypted amount format after the above basic checks
@@ -90,11 +90,11 @@ verifyProofs(
// verify el gamal pedersen linkage
{
Buffer const pedersen{ptr, ecPedersenProofLength};
if (auto const ter = verifyPedersenLinkage(
if (auto const ter = verifyBalancePcmLinkage(
pedersen,
(*mptoken)[sfConfidentialBalanceSpending],
holderPubKey,
tx[sfPedersenCommitment],
tx[sfBalanceCommitment],
contextHash);
!isTesSuccess(ter))
{

View File

@@ -45,8 +45,16 @@ ConfidentialSend::preflight(PreflightContext const& ctx)
// Check the length of the ZKProof
auto const recipientCount = getConfidentialRecipientCount(hasAuditor);
if (ctx.tx[sfZKProof].length() !=
getMultiCiphertextEqualityProofSize(recipientCount))
auto const sizeEquality =
getMultiCiphertextEqualityProofSize(recipientCount);
auto const sizePedersenLinkage = 2 * ecPedersenProofLength;
if (ctx.tx[sfZKProof].length() != sizeEquality + sizePedersenLinkage)
return temMALFORMED;
// Check the length of Pedersen commitments
if (ctx.tx[sfBalanceCommitment].size() != ecPedersenCommitmentLength ||
ctx.tx[sfAmountCommitment].size() != ecPedersenCommitmentLength)
return temMALFORMED;
// Check the encrypted amount formats, this is more expensive so put it at
@@ -62,6 +70,125 @@ ConfidentialSend::preflight(PreflightContext const& ctx)
return tesSUCCESS;
}
TER
verifySendProofs(
PreclaimContext const& ctx,
std::shared_ptr<SLE const> const& sleSenderMPToken,
std::shared_ptr<SLE const> const& sleDestinationMPToken,
std::shared_ptr<SLE const> const& sleIssuance)
{
// Sanity check
if (!sleSenderMPToken || !sleDestinationMPToken || !sleIssuance)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
auto const recipientCount = getConfidentialRecipientCount(hasAuditor);
auto const proof = ctx.tx[sfZKProof];
size_t remainingLength = proof.size();
size_t currentOffset = 0;
// Extract equality proof
auto const sizeEquality =
getMultiCiphertextEqualityProofSize(recipientCount);
if (remainingLength < sizeEquality)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const equalityProof = proof.substr(currentOffset, sizeEquality);
currentOffset += sizeEquality;
remainingLength -= sizeEquality;
// Extract Pedersen linkage proof for amount commitment
if (remainingLength < ecPedersenProofLength)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const amountLinkageProof =
proof.substr(currentOffset, ecPedersenProofLength);
currentOffset += ecPedersenProofLength;
remainingLength -= ecPedersenProofLength;
// Extract Pedersen linkage proof for balance commitment
if (remainingLength < ecPedersenProofLength)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const balanceLinkageProof =
proof.substr(currentOffset, ecPedersenProofLength);
currentOffset += ecPedersenProofLength;
remainingLength -= ecPedersenProofLength;
// todo: Extract range proof once the lib is ready
if (remainingLength != 0)
return tecINTERNAL; // LCOV_EXCL_LINE
// Prepare receipient list
std::vector<ConfidentialRecipient> recipients;
recipients.reserve(recipientCount);
recipients.push_back(
{(*sleSenderMPToken)[sfHolderElGamalPublicKey],
ctx.tx[sfSenderEncryptedAmount]});
recipients.push_back(
{(*sleDestinationMPToken)[sfHolderElGamalPublicKey],
ctx.tx[sfDestinationEncryptedAmount]});
recipients.push_back(
{(*sleIssuance)[sfIssuerElGamalPublicKey],
ctx.tx[sfIssuerEncryptedAmount]});
if (hasAuditor)
{
recipients.push_back(
{(*sleIssuance)[sfAuditorElGamalPublicKey],
ctx.tx[sfAuditorEncryptedAmount]});
}
// Prepare the context hash
auto const contextHash = getSendContextHash(
ctx.tx[sfAccount],
ctx.tx[sfSequence],
ctx.tx[sfMPTokenIssuanceID],
ctx.tx[sfDestination],
(*sleSenderMPToken)[~sfConfidentialBalanceVersion].value_or(0));
// Verify the multi-ciphertext equality proof
if (auto const ter = verifyMultiCiphertextEqualityProof(
equalityProof, recipients, recipientCount, contextHash);
!isTesSuccess(ter))
{
JLOG(ctx.j.trace()) << "ConfidentialSend: Equality proof failed.";
return ter;
}
// Verify amount linkage
if (auto const ter = verifyAmountPcmLinkage(
amountLinkageProof,
ctx.tx[sfSenderEncryptedAmount],
(*sleSenderMPToken)[sfHolderElGamalPublicKey],
ctx.tx[sfAmountCommitment],
contextHash);
!isTesSuccess(ter))
{
JLOG(ctx.j.trace()) << "ConfidentialSend: Amount linkage proof failed.";
return ter;
}
// Verify balance linkage
if (auto const ter = verifyBalancePcmLinkage(
balanceLinkageProof,
(*sleSenderMPToken)[sfConfidentialBalanceSpending],
(*sleSenderMPToken)[sfHolderElGamalPublicKey],
ctx.tx[sfBalanceCommitment],
contextHash);
!isTesSuccess(ter))
{
JLOG(ctx.j.trace())
<< "ConfidentialSend: Balance linkage proof failed.";
return ter;
}
return tesSUCCESS;
}
TER
ConfidentialSend::preclaim(PreclaimContext const& ctx)
{
@@ -155,52 +282,8 @@ ConfidentialSend::preclaim(PreclaimContext const& ctx)
!isTesSuccess(ter))
return ter;
auto const contextHash = getSendContextHash(
account,
ctx.tx[sfSequence],
mptIssuanceID,
destination,
(*sleSenderMPToken)[~sfConfidentialBalanceVersion].value_or(0));
auto const expectedRecipients =
getConfidentialRecipientCount(requiresAuditor);
// Prepare encrypted amount info for proof verification
std::vector<ConfidentialRecipient> recipients;
recipients.reserve(expectedRecipients);
// Add sender encrypted amount info
recipients.push_back(
{(*sleSenderMPToken)[sfHolderElGamalPublicKey],
ctx.tx[sfSenderEncryptedAmount]});
// Add destination encrypted amount info
recipients.push_back(
{(*sleDestinationMPToken)[sfHolderElGamalPublicKey],
ctx.tx[sfDestinationEncryptedAmount]});
// Add issuer encrypted amount info
recipients.push_back(
{(*sleIssuance)[sfIssuerElGamalPublicKey],
ctx.tx[sfIssuerEncryptedAmount]});
// Add auditor encrypted amount info if present
if (requiresAuditor)
{
recipients.push_back(
{(*sleIssuance)[sfAuditorElGamalPublicKey],
ctx.tx[sfAuditorEncryptedAmount]});
}
// Verify the multi-ciphertext equality proof
if (auto const ter = verifyMultiCiphertextEqualityProof(
ctx.tx[sfZKProof], recipients, expectedRecipients, contextHash);
!isTesSuccess(ter))
{
return ter;
}
return tesSUCCESS;
return verifySendProofs(
ctx, sleSenderMPToken, sleDestinationMPToken, sleIssuance);
}
TER
@@ -209,13 +292,14 @@ ConfidentialSend::doApply()
auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID];
auto const destination = ctx_.tx[sfDestination];
auto sleSender = view().peek(keylet::mptoken(mptIssuanceID, account_));
auto sleDestination =
auto sleSenderMPToken =
view().peek(keylet::mptoken(mptIssuanceID, account_));
auto sleDestinationMPToken =
view().peek(keylet::mptoken(mptIssuanceID, destination));
auto sleDestAcct = view().peek(keylet::account(destination));
if (!sleSender || !sleDestination || !sleDestAcct)
if (!sleSenderMPToken || !sleDestinationMPToken || !sleDestAcct)
return tecINTERNAL;
if (auto err = verifyDepositPreauth(
@@ -236,7 +320,8 @@ ConfidentialSend::doApply()
// Subtract from sender's spending balance
{
Slice const curSpending = (*sleSender)[sfConfidentialBalanceSpending];
Slice const curSpending =
(*sleSenderMPToken)[sfConfidentialBalanceSpending];
Buffer newSpending(ecGamalEncryptedTotalLength);
if (TER const ter =
@@ -244,12 +329,13 @@ ConfidentialSend::doApply()
!isTesSuccess(ter))
return tecINTERNAL;
(*sleSender)[sfConfidentialBalanceSpending] = newSpending;
(*sleSenderMPToken)[sfConfidentialBalanceSpending] = newSpending;
}
// Subtract from issuer's balance
{
Slice const curIssuerEnc = (*sleSender)[sfIssuerEncryptedBalance];
Slice const curIssuerEnc =
(*sleSenderMPToken)[sfIssuerEncryptedBalance];
Buffer newIssuerEnc(ecGamalEncryptedTotalLength);
if (TER const ter =
@@ -257,13 +343,14 @@ ConfidentialSend::doApply()
!isTesSuccess(ter))
return tecINTERNAL;
(*sleSender)[sfIssuerEncryptedBalance] = newIssuerEnc;
(*sleSenderMPToken)[sfIssuerEncryptedBalance] = newIssuerEnc;
}
// Subtract from auditor's balance if present
if (auditorEc)
{
Slice const curAuditorEnc = (*sleSender)[sfAuditorEncryptedBalance];
Slice const curAuditorEnc =
(*sleSenderMPToken)[sfAuditorEncryptedBalance];
Buffer newAuditorEnc(ecGamalEncryptedTotalLength);
if (TER const ter =
@@ -271,24 +358,26 @@ ConfidentialSend::doApply()
!isTesSuccess(ter))
return tecINTERNAL;
(*sleSender)[sfAuditorEncryptedBalance] = newAuditorEnc;
(*sleSenderMPToken)[sfAuditorEncryptedBalance] = newAuditorEnc;
}
// Add to destination's inbox balance
{
Slice const curInbox = (*sleDestination)[sfConfidentialBalanceInbox];
Slice const curInbox =
(*sleDestinationMPToken)[sfConfidentialBalanceInbox];
Buffer newInbox(ecGamalEncryptedTotalLength);
if (TER const ter = homomorphicAdd(curInbox, destEc, newInbox);
!isTesSuccess(ter))
return tecINTERNAL;
(*sleDestination)[sfConfidentialBalanceInbox] = newInbox;
(*sleDestinationMPToken)[sfConfidentialBalanceInbox] = newInbox;
}
// Add to issuer's balance
{
Slice const curIssuerEnc = (*sleDestination)[sfIssuerEncryptedBalance];
Slice const curIssuerEnc =
(*sleDestinationMPToken)[sfIssuerEncryptedBalance];
Buffer newIssuerEnc(ecGamalEncryptedTotalLength);
if (TER const ter =
@@ -296,14 +385,14 @@ ConfidentialSend::doApply()
!isTesSuccess(ter))
return tecINTERNAL;
(*sleDestination)[sfIssuerEncryptedBalance] = newIssuerEnc;
(*sleDestinationMPToken)[sfIssuerEncryptedBalance] = newIssuerEnc;
}
// Add to auditor's balance if present
if (auditorEc)
{
Slice const curAuditorEnc =
(*sleDestination)[sfAuditorEncryptedBalance];
(*sleDestinationMPToken)[sfAuditorEncryptedBalance];
Buffer newAuditorEnc(ecGamalEncryptedTotalLength);
if (TER const ter =
@@ -311,15 +400,15 @@ ConfidentialSend::doApply()
!isTesSuccess(ter))
return tecINTERNAL;
(*sleDestination)[sfAuditorEncryptedBalance] = newAuditorEnc;
(*sleDestinationMPToken)[sfAuditorEncryptedBalance] = newAuditorEnc;
}
// increment version
incrementConfidentialVersion(*sleSender);
incrementConfidentialVersion(*sleDestination);
incrementConfidentialVersion(*sleSenderMPToken);
incrementConfidentialVersion(*sleDestinationMPToken);
view().update(sleSender);
view().update(sleDestination);
view().update(sleSenderMPToken);
view().update(sleDestinationMPToken);
return tesSUCCESS;
}
} // namespace ripple