diff --git a/src/test/app/ConfidentialTransfer_test.cpp b/src/test/app/ConfidentialTransfer_test.cpp index e6859d8d3d..9ff9270a7d 100644 --- a/src/test/app/ConfidentialTransfer_test.cpp +++ b/src/test/app/ConfidentialTransfer_test.cpp @@ -2191,6 +2191,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase .account = bob, .dest = bob, .amt = 10, + .proof = getTrivialSendProofHex(), .err = temMALFORMED, }); @@ -2897,22 +2898,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase auto& mptAlice = confEnv.mpt; { - // Bob has 60, tries to send 70. Invalid remaining balance. - mptAlice.send({ - .account = bob, - .dest = carol, - .amt = 70, - .err = tecBAD_PROOF, - }); - - // Bob has 60, tries to send 61. Invalid remaining balance. - mptAlice.send({ - .account = bob, - .dest = carol, - .amt = 61, - .err = tecBAD_PROOF, - }); - // Bob has 60, sends 60. Remainder is exactly 0. Valid remaining balance. mptAlice.send({ .account = bob, @@ -2933,12 +2918,12 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase }); // Bob has 100, tries to send 2^64-1. Invalid remaining balance. - mptAlice.send({ - .account = bob, - .dest = carol, - .amt = std::numeric_limits::max(), - .err = tecBAD_PROOF, - }); + { + ConfidentialSendSetup const setup( + mptAlice, bob, carol, alice, std::numeric_limits::max()); + auto const forged = getForgedSendProof(mptAlice, env, bob, carol, setup); + mptAlice.send(setup.sendArgs(bob, carol, forged, tecBAD_PROOF)); + } // Bob sends 1, remaining 99. mptAlice.send({ @@ -2947,14 +2932,6 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase .amt = 1, .err = tesSUCCESS, }); - - // Bob sends 100, but only has 99. Invalid remaining balance. - mptAlice.send({ - .account = bob, - .dest = carol, - .amt = 100, - .err = tecBAD_PROOF, - }); } // send when spending balance is 0 (key registered, inbox merged, but nothing converted) @@ -2971,18 +2948,13 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase // Trying to send any amount with 0 spending balance must fail: // the range proof for < 0 is invalid. - mptAlice2.send({ - .account = bob2, - .dest = carol2, - .amt = 1, - .err = tecBAD_PROOF, - }); + ConfidentialSendSetup const setup(mptAlice2, bob2, carol2, alice2, 1); + auto const forged = getForgedSendProof(mptAlice2, env2, bob2, carol2, setup); + mptAlice2.send(setup.sendArgs(bob2, carol2, forged, tecBAD_PROOF)); BEAST_EXPECT( mptAlice2.getDecryptedBalance(bob2, MPTTester::holderEncryptedSpending) == 0); } - - // todo: test m exceeding range, require using scala and refactor } /* The equality proof library and range proof library do not @@ -3462,7 +3434,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase uint256 const convertBackContextHash = getConvertBackContextHash(bob.id(), mptAlice.issuanceID(), env.seq(bob), version); - Buffer const proof = mptAlice.getConvertBackProof( + auto const proof = mptAlice.getConvertBackProof( bob, convertBackAmt, convertBackContextHash, @@ -3472,6 +3444,8 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase .encryptedAmt = encryptedSpendingBalance, .blindingFactor = pcBlindingFactor, }); + if (!BEAST_EXPECT(proof.has_value())) + return; { json::Value jv; @@ -3483,7 +3457,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase jv[sfIssuerEncryptedAmount.jsonName] = strHex(convertBackIssuerCiphertext); jv[sfBlindingFactor.jsonName] = strHex(convertBackBlindingFactor); jv[sfBalanceCommitment.jsonName] = strHex(pedersenCommitment); - jv[sfZKProof.jsonName] = strHex(proof); + jv[sfZKProof.jsonName] = strHex(requireOptionalRef(proof, "Missing proof")); env(jv, Ter(tesSUCCESS)); } @@ -5283,7 +5257,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version); Buffer const badPedersenCommitment = mptAlice.getPedersenCommitment(1, pcBlindingFactor); - Buffer const proof = mptAlice.getConvertBackProof( + auto const proof = mptAlice.getConvertBackProof( bob, amt, contextHash, @@ -5293,6 +5267,8 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase .encryptedAmt = encryptedSpendingBalance, .blindingFactor = pcBlindingFactor, }); + if (!BEAST_EXPECT(proof.has_value())) + return; mptAlice.convertBack({ .account = bob, @@ -5313,7 +5289,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase uint256 const contextHash = getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version); - Buffer const proof = mptAlice.getConvertBackProof( + auto const proof = mptAlice.getConvertBackProof( bob, amt, contextHash, @@ -5323,6 +5299,8 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase .encryptedAmt = encryptedSpendingBalance, .blindingFactor = generateBlindingFactor(), // wrong blinding factor }); + if (!BEAST_EXPECT(proof.has_value())) + return; mptAlice.convertBack({ .account = bob, @@ -5337,22 +5315,26 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase } // Test 3: Proof generated with wrong balance value. - // The proof claims balance=1 but the encrypted spending balance contains - // the actual balance. Verification fails because the values don't match. + // The sigma proof claims balance=20 but the pedersen commitment and + // encrypted spending balance were built for the actual balance (40). + // we cannot call mpt_get_convert_back_proof because it has client-side + // verification. { uint256 const contextHash = getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version); - Buffer const proof = mptAlice.getConvertBackProof( + uint64_t constexpr claimedBalance = 20; // wrong: real balance is 40 + + auto const proof = getForgedConvertBackProof( + mptAlice, bob, + claimedBalance, + spendingBalance, amt, - contextHash, - { - .pedersenCommitment = pedersenCommitment, - .amt = 1, // wrong balance - .encryptedAmt = encryptedSpendingBalance, - .blindingFactor = pcBlindingFactor, - }); + pedersenCommitment, + encryptedSpendingBalance, + pcBlindingFactor, + contextHash); mptAlice.convertBack({ .account = bob, @@ -5375,7 +5357,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version); Buffer const badPedersenCommitment = mptAlice.getPedersenCommitment(1, pcBlindingFactor); - Buffer const proof = mptAlice.getConvertBackProof( + auto const proof = mptAlice.getConvertBackProof( bob, amt, contextHash, @@ -5385,6 +5367,8 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase .encryptedAmt = encryptedSpendingBalance, .blindingFactor = pcBlindingFactor, }); + if (!BEAST_EXPECT(proof.has_value())) + return; mptAlice.convertBack({ .account = bob, @@ -5405,7 +5389,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase { uint256 const badContextHash{1}; - Buffer const proof = mptAlice.getConvertBackProof( + auto const proof = mptAlice.getConvertBackProof( bob, amt, badContextHash, // wrong context hash @@ -5415,6 +5399,8 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase .encryptedAmt = encryptedSpendingBalance, .blindingFactor = pcBlindingFactor, }); + if (!BEAST_EXPECT(proof.has_value())) + return; mptAlice.convertBack({ .account = bob, @@ -5434,7 +5420,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase uint256 const contextHash = getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version); - Buffer const proof = mptAlice.getConvertBackProof( + auto const proof = mptAlice.getConvertBackProof( bob, amt, contextHash, @@ -5444,6 +5430,8 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase .encryptedAmt = encryptedSpendingBalance, .blindingFactor = pcBlindingFactor, }); + if (!BEAST_EXPECT(proof.has_value())) + return; mptAlice.convertBack({ .account = bob, @@ -5919,22 +5907,26 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase // linkage, and that the remaining balance is non-negative. // Test 1: Proof generated with wrong balance value. - // The sigma proof claims balance=1 but the spending balance contains the - // actual balance. The compact proof's balance-linkage check fails. + // The sigma proof claims balance=20 but the pedersen commitment and + // encrypted spending balance were built for the actual balance (40). + // we cannot call mpt_get_convert_back_proof because it has client-side + // verification. { uint256 const contextHash = getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version); - Buffer const proof = mptAlice.getConvertBackProof( + uint64_t constexpr claimedBalance = 20; // wrong: real balance is 40 + + auto const proof = getForgedConvertBackProof( + mptAlice, bob, + claimedBalance, + spendingBalance, amt, - contextHash, - { - .pedersenCommitment = pedersenCommitment, - .amt = 1, // wrong balance (actual balance is ~40) - .encryptedAmt = encryptedSpendingBalance, - .blindingFactor = pcBlindingFactor, - }); + pedersenCommitment, + encryptedSpendingBalance, + pcBlindingFactor, + contextHash); mptAlice.convertBack({ .account = bob, @@ -5956,7 +5948,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase uint256 const contextHash = getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version); - Buffer const proof = mptAlice.getConvertBackProof( + auto const proof = mptAlice.getConvertBackProof( bob, amt, contextHash, @@ -5966,6 +5958,8 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase .encryptedAmt = encryptedSpendingBalance, .blindingFactor = generateBlindingFactor(), // wrong blinding factor }); + if (!BEAST_EXPECT(proof.has_value())) + return; mptAlice.convertBack({ .account = bob, @@ -5985,7 +5979,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase // makes the proof invalid for this transaction, preventing replay attacks. { uint256 const badContextHash{1}; - Buffer const proof = mptAlice.getConvertBackProof( + auto const proof = mptAlice.getConvertBackProof( bob, amt, badContextHash, // wrong context hash @@ -5995,6 +5989,8 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase .encryptedAmt = encryptedSpendingBalance, .blindingFactor = pcBlindingFactor, }); + if (!BEAST_EXPECT(proof.has_value())) + return; mptAlice.convertBack({ .account = bob, @@ -6014,7 +6010,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase uint256 const contextHash = getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), version); - Buffer const proof = mptAlice.getConvertBackProof( + auto const proof = mptAlice.getConvertBackProof( bob, amt, contextHash, @@ -6024,6 +6020,8 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase .encryptedAmt = encryptedSpendingBalance, .blindingFactor = pcBlindingFactor, }); + if (!BEAST_EXPECT(proof.has_value())) + return; mptAlice.convertBack({ .account = bob, @@ -6073,7 +6071,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase Buffer const bobCiphertext = mptAlice.encryptAmount(bob, amt, blindingFactor); auto const version = mptAlice.getMPTokenVersion(bob); - Buffer const proof = mptAlice.getConvertBackProof( + auto const proof = mptAlice.getConvertBackProof( bob, amt, makeContextHash(env, mptAlice, alice, bob, carol, version), @@ -6084,6 +6082,8 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase encryptedSpendingBalance, "Missing encrypted spending balance"), .blindingFactor = pcBlindingFactor, }); + if (!BEAST_EXPECT(proof.has_value())) + return; mptAlice.convertBack({ .account = bob, @@ -6173,7 +6173,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase uint256 const contextHashA = getConvertBackContextHash(bob, mptAlice.issuanceID(), currentSeq, version); - Buffer const proofA = mptAlice.getConvertBackProof( + auto const proofA = mptAlice.getConvertBackProof( bob, amtA, contextHashA, @@ -6183,6 +6183,8 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase .encryptedAmt = encryptedSpendingBalance, .blindingFactor = pcBlindingFactor, }); + if (!BEAST_EXPECT(proofA.has_value())) + return; // Construct Transaction B with Amount m2 = 20 and attach Proof pi uint64_t const amtB = 20; @@ -6254,7 +6256,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase uint256 const oldContextHash = getConvertBackContextHash(bob, mptAlice.issuanceID(), currentSeq, versionV); - Buffer const oldProof = mptAlice.getConvertBackProof( + auto const oldProof = mptAlice.getConvertBackProof( bob, amt, oldContextHash, @@ -6264,6 +6266,8 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase .encryptedAmt = encryptedSpendingBalanceV, .blindingFactor = pcBlindingFactor, }); + if (!BEAST_EXPECT(oldProof.has_value())) + return; // Submit and verify failure mptAlice.convertBack({ @@ -6326,7 +6330,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase uint256 const contextHash = getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), currentVersion); - Buffer const proof = mptAlice.getConvertBackProof( + auto const proof = mptAlice.getConvertBackProof( bob, amt, contextHash, @@ -6336,6 +6340,8 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase .encryptedAmt = spendingBalEnc, .blindingFactor = pcBf, }); + if (!BEAST_EXPECT(proof.has_value())) + return; // Submit transaction with Divergent Ciphertexts // Holder Ciphertext encrypts 11. Issuer Ciphertext encrypts 10. @@ -6469,7 +6475,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase uint256 const contextHash = getConvertBackContextHash(bob, mptAlice.issuanceID(), env.seq(bob), currentVersion); - Buffer const proof = mptAlice.getConvertBackProof( + auto const proof = mptAlice.getConvertBackProof( bob, 1, contextHash, @@ -6479,6 +6485,8 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase .encryptedAmt = underflowedCt, .blindingFactor = pcBf, }); + if (!BEAST_EXPECT(proof.has_value())) + return; mptAlice.convertBack({ .account = bob, @@ -7759,7 +7767,7 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase uint256 const convertBackCtxHash = getConvertBackContextHash(bob.id(), mptAlice.issuanceID(), env.seq(bob), version); - Buffer const convertBackProof = mptAlice.getConvertBackProof( + auto const convertBackProof = mptAlice.getConvertBackProof( bob, sendAmount, convertBackCtxHash, @@ -7769,14 +7777,18 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase .encryptedAmt = encryptedSpending, .blindingFactor = pcBlindingFactor, }); + if (!BEAST_EXPECT(convertBackProof.has_value())) + return; // Resize the convertBack proof to match the expected send proof // size so it passes preflight's size check and reaches the actual // ZK verification in doApply. auto const expectedSendSize = kEcSendProofLength; Buffer resizedProof(expectedSendSize); - auto const copyLen = std::min(convertBackProof.size(), expectedSendSize); - std::memcpy(resizedProof.data(), convertBackProof.data(), copyLen); + Buffer const& convertBackProofRef = + requireOptionalRef(convertBackProof, "Missing proof"); + auto const copyLen = std::min(convertBackProofRef.size(), expectedSendSize); + std::memcpy(resizedProof.data(), convertBackProofRef.data(), copyLen); // Zero-pad the rest (if convertBack proof is shorter) if (copyLen < expectedSendSize) std::memset(resizedProof.data() + copyLen, 0, expectedSendSize - copyLen); diff --git a/src/test/jtx/ConfidentialTransfer.h b/src/test/jtx/ConfidentialTransfer.h index 02b2e8dccb..5c1b90328b 100644 --- a/src/test/jtx/ConfidentialTransfer.h +++ b/src/test/jtx/ConfidentialTransfer.h @@ -124,6 +124,87 @@ protected: return proof; } + // Forges a ConvertBack proof (compact sigma + single bulletproof) whose + // sigma component claims claimedBalance (which may be wrong) while binding + // to the real pedersen commitment and encrypted spending balance + // ciphertext already on the ledger. The bulletproof component is built + // from realBalance so it stays honest. + // mpt_get_convert_back_proof does not allow to build a proof whose amount + // exceeds the holder's claimed balance. + static Buffer + getForgedConvertBackProof( + test::jtx::MPTTester& mpt, + test::jtx::Account const& holder, + uint64_t claimedBalance, + uint64_t realBalance, + uint64_t amt, + Buffer const& pedersenCommitment, + Buffer const& encryptedSpendingBalance, + Buffer const& pcBlindingFactor, + uint256 const& contextHash) + { + if (pedersenCommitment.size() != kCompressedEcPointLength) + Throw("getForgedConvertBackProof: bad pedersenCommitment length"); + if (encryptedSpendingBalance.size() != kEcGamalEncryptedTotalLength) + { + Throw( + "getForgedConvertBackProof: bad encryptedSpendingBalance length"); + } + if (amt > realBalance) + Throw("getForgedConvertBackProof: amt exceeds realBalance"); + + auto* const ctx = mpt_secp256k1_context(); + auto const holderPubKey = requireOptional(mpt.getPubKey(holder), "Missing holder pubkey"); + auto const holderPrivKey = + requireOptional(mpt.getPrivKey(holder), "Missing holder privkey"); + + secp256k1_pubkey pkHolder; + if (secp256k1_ec_pubkey_parse( + ctx, &pkHolder, holderPubKey.data(), kCompressedEcPointLength) != 1) + Throw("Failed to parse holder's public key"); + + secp256k1_pubkey pcB; + if (secp256k1_ec_pubkey_parse( + ctx, &pcB, pedersenCommitment.data(), kCompressedEcPointLength) != 1) + Throw("Failed to parse pedersen commitment"); + + secp256k1_pubkey b1, b2; + if (secp256k1_ec_pubkey_parse( + ctx, &b1, encryptedSpendingBalance.data(), kCompressedEcPointLength) != 1 || + secp256k1_ec_pubkey_parse( + ctx, + &b2, + encryptedSpendingBalance.data() + kCompressedEcPointLength, + kCompressedEcPointLength) != 1) + Throw("Failed to parse balance ciphertext"); + + Buffer sigmaProof(SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE); + if (secp256k1_compact_convertback_prove( + ctx, + sigmaProof.data(), + claimedBalance, + holderPrivKey.data(), + pcBlindingFactor.data(), + &pkHolder, + &b1, + &b2, + &pcB, + contextHash.data()) != 1) + Throw("Failed to generate convertback sigma proof"); + + auto const forgedBulletproof = + getForgedSingleBulletproof(realBalance - amt, pcBlindingFactor, contextHash); + + Buffer proof(kEcConvertBackProofLength); + std::memcpy(proof.data(), sigmaProof.data(), SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE); + std::memcpy( + proof.data() + SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE, + forgedBulletproof.data(), + kEcSingleBulletproofLength); + + return proof; + } + // Get a bad ciphertext with valid structure but cryptographic invalid for // testing purposes. For preflight test purposes. static Buffer const& @@ -347,6 +428,111 @@ protected: } }; + // Forges a ConfidentialMPTSend proof (compact sigma + double bulletproof) + // for setup.sendAmount against setup's real balance commitment/ciphertext. + // mpt_get_confidential_send_proof does not allow to build a proof whose amount + // exceeds the sender's claimed balance. + static Buffer + getForgedSendProof( + test::jtx::MPTTester& mpt, + test::jtx::Env& env, + test::jtx::Account const& sender, + test::jtx::Account const& dest, + ConfidentialSendSetup const& setup) + { + auto* const ctx = mpt_secp256k1_context(); + + secp256k1_pubkey c1; + std::vector c2Vec(setup.recipients.size()); + std::vector pkVec(setup.recipients.size()); + for (std::size_t i = 0; i < setup.recipients.size(); ++i) + { + auto const& r = setup.recipients[i]; + if (i == 0 && + secp256k1_ec_pubkey_parse( + ctx, &c1, r.encryptedAmount.data(), kCompressedEcPointLength) != 1) + Throw("Failed to parse C1"); + if (secp256k1_ec_pubkey_parse( + ctx, + &c2Vec[i], + r.encryptedAmount.data() + kCompressedEcPointLength, + kCompressedEcPointLength) != 1) + Throw("Failed to parse C2"); + if (secp256k1_ec_pubkey_parse( + ctx, &pkVec[i], r.publicKey.data(), kCompressedEcPointLength) != 1) + Throw("Failed to parse recipient pubkey"); + } + + secp256k1_pubkey pkSender, pcAmount, pcBalance, b1, b2; + if (secp256k1_ec_pubkey_parse( + ctx, &pkSender, setup.senderPubKey.data(), kCompressedEcPointLength) != 1 || + secp256k1_ec_pubkey_parse( + ctx, &pcAmount, setup.amountCommitment.data(), kCompressedEcPointLength) != 1 || + secp256k1_ec_pubkey_parse( + ctx, &pcBalance, setup.balanceCommitment.data(), kCompressedEcPointLength) != 1 || + secp256k1_ec_pubkey_parse( + ctx, &b1, setup.prevEncryptedSpending.data(), kCompressedEcPointLength) != 1 || + secp256k1_ec_pubkey_parse( + ctx, + &b2, + setup.prevEncryptedSpending.data() + kCompressedEcPointLength, + kCompressedEcPointLength) != 1) + Throw("Failed to parse commitments/ciphertext"); + + Buffer const senderPrivKey = + requireOptional(mpt.getPrivKey(sender), "Missing sender privkey"); + auto const ctxHash = getSendContextHash( + sender.id(), mpt.issuanceID(), env.seq(sender), dest.id(), setup.version); + + Buffer sigmaProof(SECP256K1_COMPACT_STANDARD_PROOF_SIZE); + if (secp256k1_compact_standard_prove( + ctx, + sigmaProof.data(), + setup.sendAmount, + setup.prevSpending, + setup.blindingFactor.data(), + senderPrivKey.data(), + setup.balanceBlindingFactor.data(), + setup.recipients.size(), + &c1, + c2Vec.data(), + pkVec.data(), + &pcAmount, + &pkSender, + &pcBalance, + &b1, + &b2, + ctxHash.data()) != 1) + Throw("Failed to generate sigma proof"); + + // Wraps (mod 2^64) for overdrafts, unlike the ledger's own homomorphic + // commitment subtraction (mod the curve order) — that mismatch is + // exactly what makes the forged proof fail verification. + // Computed without a wrapping `uint64` subtract: Clang UBSan treats + // unsigned overflow as fatal (see incrementConfidentialVersion). + std::uint64_t const remaining = setup.sendAmount <= setup.prevSpending + ? setup.prevSpending - setup.sendAmount + : ~setup.sendAmount + setup.prevSpending + 1; + + Buffer negAmountBf(kEcBlindingFactorLength); + Buffer remainingBf(kEcBlindingFactorLength); + secp256k1_mpt_scalar_negate(negAmountBf.data(), setup.amountBlindingFactor.data()); + secp256k1_mpt_scalar_add( + remainingBf.data(), setup.balanceBlindingFactor.data(), negAmountBf.data()); + + auto const forgedBulletproof = getForgedBulletproof( + {setup.sendAmount, remaining}, {setup.amountBlindingFactor, remainingBf}, ctxHash); + + Buffer combinedProof(kEcSendProofLength); + std::memcpy(combinedProof.data(), sigmaProof.data(), SECP256K1_COMPACT_STANDARD_PROOF_SIZE); + std::memcpy( + combinedProof.data() + SECP256K1_COMPACT_STANDARD_PROOF_SIZE, + forgedBulletproof.data(), + kEcDoubleBulletproofLength); + + return combinedProof; + } + // Helper that wraps the boilerplate setup: Env + MPT creation, funding, key // generation, and seeding each holder with a confidential balance. // The caller supplies the issuer and any number of holders. diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index 3bffed918f..385526c588 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -44,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -63,14 +64,23 @@ constexpr std::uint64_t kElGamalDecryptRangeHigh = 3000; * * @param opt The optional to unwrap. * @param what Description used in the thrown exception if opt is empty. + * @param loc The call site to report in the thrown exception, defaulting to + * the immediate caller. * @return A const reference to the contained value. */ template [[nodiscard]] T const& -requireValue(std::optional const& opt, char const* what) +requireValue( + std::optional const& opt, + char const* what, + std::source_location const& loc = std::source_location::current()) { if (!opt) - Throw(what); + { + Throw( + std::string(what) + " must be present (called from " + + std::string(loc.function_name()) + ")"); + } return *opt; } @@ -92,6 +102,50 @@ makePedersenParams(PedersenProofParams const& params) return res; } +/** + * @brief Sets sfAccount on jv to the given account. + * + * @param jv The JSON object to set the field on. + * @param account The account to set. Throws if not present. + * @return The resolved account. + */ +Account const& +setAccountField(json::Value& jv, std::optional const& account) +{ + Account const& act = requireValue(account, "account"); + jv[sfAccount] = act.human(); + return act; +} + +/** + * @brief Sets sfDestination on jv to the given account. + * + * @param jv The JSON object to set the field on. + * @param dest The destination account to set. Throws if not present. + * @return The resolved account. + */ +Account const& +setDestinationField(json::Value& jv, std::optional const& dest) +{ + Account const& act = requireValue(dest, "dest"); + jv[sfDestination] = act.human(); + return act; +} + +/** + * @brief Sets sfZKProof to the given proof if present, otherwise to a + * zero-filled placeholder of the given length. + * + * @param jv The JSON object to set the field on. + * @param proof The real proof to use, if generated. + * @param dummyLen The length of the placeholder buffer to use when proof is not set. + */ +void +setProofOrDummy(json::Value& jv, std::optional const& proof, std::size_t dummyLen) +{ + jv[sfZKProof.jsonName] = strHex(proof ? *proof : gMakeZeroBuffer(dummyLen)); +} + /** * @brief Looks up an account's key at a given key epoch. * @@ -1003,7 +1057,7 @@ MPTTester::getPedersenCommitment(std::uint64_t const amount, Buffer const& peder return buf; } -Buffer +std::optional MPTTester::getConvertBackProof( Account const& holder, std::uint64_t const amount, @@ -1015,13 +1069,13 @@ MPTTester::getConvertBackProof( auto const sleMptoken = env_.le(keylet::mptoken(issuanceID(), holder.id())); if (!sleMptoken || !sleMptoken->isFieldPresent(sfConfidentialBalanceSpending)) - return gMakeZeroBuffer(kExpectedProofLength); + return std::nullopt; auto const holderPubKey = getPubKey(holder); auto const holderPrivKey = getPrivKey(holder); if (!holderPubKey || !holderPrivKey) - return gMakeZeroBuffer(kExpectedProofLength); + return std::nullopt; auto const pedersenParams = makePedersenParams(pcParams); Buffer proof(kExpectedProofLength); @@ -1033,7 +1087,7 @@ MPTTester::getConvertBackProof( amount, &pedersenParams, proof.data()) != 0) - return gMakeZeroBuffer(kExpectedProofLength); + return std::nullopt; return proof; } @@ -1044,33 +1098,32 @@ MPTTester::getEncryptedBalance(Account const& account, EncryptedBalanceType opti if (!id_) Throw("MPT has not been created"); - if (auto const sle = env_.le(keylet::mptoken(*id_, account.id()))) + auto const sle = env_.le(keylet::mptoken(*id_, account.id())); + if (!sle) + return {}; + + SField const* field = nullptr; + switch (option) { - if (option == holderEncryptedInbox && sle->isFieldPresent(sfConfidentialBalanceInbox)) - { - return Buffer( - (*sle)[sfConfidentialBalanceInbox].data(), - (*sle)[sfConfidentialBalanceInbox].size()); - } - if (option == holderEncryptedSpending && sle->isFieldPresent(sfConfidentialBalanceSpending)) - { - return Buffer( - (*sle)[sfConfidentialBalanceSpending].data(), - (*sle)[sfConfidentialBalanceSpending].size()); - } - if (option == issuerEncryptedBalance && sle->isFieldPresent(sfIssuerEncryptedBalance)) - { - return Buffer( - (*sle)[sfIssuerEncryptedBalance].data(), (*sle)[sfIssuerEncryptedBalance].size()); - } - if (option == auditorEncryptedBalance && sle->isFieldPresent(sfAuditorEncryptedBalance)) - { - return Buffer( - (*sle)[sfAuditorEncryptedBalance].data(), (*sle)[sfAuditorEncryptedBalance].size()); - } + case holderEncryptedInbox: + field = &sfConfidentialBalanceInbox; + break; + case holderEncryptedSpending: + field = &sfConfidentialBalanceSpending; + break; + case issuerEncryptedBalance: + field = &sfIssuerEncryptedBalance; + break; + case auditorEncryptedBalance: + field = &sfAuditorEncryptedBalance; + break; } - return {}; + if (field == nullptr || !sle->isFieldPresent(*field)) + return {}; + + auto const blob = sle->getFieldVL(*field); + return Buffer(blob.data(), blob.size()); } std::uint32_t @@ -1087,6 +1140,33 @@ MPTTester::getFlags(std::optional const& holder) const return flags; } +void +MPTTester::setIssuanceIdField(json::Value& jv, std::optional const& id) const +{ + if (id) + { + jv[sfMPTokenIssuanceID] = to_string(*id); + } + else if (id_) + { + jv[sfMPTokenIssuanceID] = to_string(*id_); + } + else + { + Throw("MPT has not been created"); + } +} + +std::uint32_t +MPTTester::ticketOrSeq( + std::optional const& ticketSeq, + std::optional const& account) const +{ + if (ticketSeq) + return *ticketSeq; + return env_.seq(requireValue(account, "account")); +} + MPT MPTTester::operator[](std::string const& name) const { @@ -1104,47 +1184,37 @@ void MPTTester::fillConversionCiphertexts( T const& arg, json::Value& jv, - Buffer& holderCiphertext, - Buffer& issuerCiphertext, - std::optional& auditorCiphertext, - Buffer& blindingFactor) const + Account const& account, + std::uint64_t const amount) const { - blindingFactor = arg.blindingFactor ? *arg.blindingFactor : generateBlindingFactor(); + Buffer const blindingFactor = + arg.blindingFactor ? *arg.blindingFactor : generateBlindingFactor(); + jv[sfBlindingFactor.jsonName] = strHex(blindingFactor); // Handle Holder - if (arg.holderEncryptedAmt) - { - holderCiphertext = *arg.holderEncryptedAmt; - } - else - { - holderCiphertext = encryptAmount( - requireValue(arg.account, "account"), requireValue(arg.amt, "amt"), blindingFactor); - } + Buffer const holderCiphertext = arg.holderEncryptedAmt + ? *arg.holderEncryptedAmt + : encryptAmount(account, amount, blindingFactor); jv[sfHolderEncryptedAmount.jsonName] = strHex(holderCiphertext); // Handle Issuer - if (arg.issuerEncryptedAmt) - { - issuerCiphertext = *arg.issuerEncryptedAmt; - } - else - { - issuerCiphertext = encryptAmount(issuer_, requireValue(arg.amt, "amt"), blindingFactor); - } + Buffer const issuerCiphertext = arg.issuerEncryptedAmt + ? *arg.issuerEncryptedAmt + : encryptAmount(issuer_, amount, blindingFactor); jv[sfIssuerEncryptedAmount.jsonName] = strHex(issuerCiphertext); // Handle Auditor + std::optional auditorCiphertext; if (arg.auditorEncryptedAmt) { auditorCiphertext = *arg.auditorEncryptedAmt; } else if (auditor_.has_value() && arg.fillAuditorEncryptedAmt.value_or(false)) { - auditorCiphertext = encryptAmount( - requireValue(auditor_, "auditor"), requireValue(arg.amt, "amt"), blindingFactor); + auditorCiphertext = + encryptAmount(requireValue(auditor_, "auditor"), amount, blindingFactor); } // Update auditor JSON only if ciphertext exists @@ -1155,73 +1225,17 @@ MPTTester::fillConversionCiphertexts( void MPTTester::convert(MPTConvert const& arg) { - json::Value jv; - if (arg.account) - { - jv[sfAccount] = arg.account->human(); - } - else - { - Throw("Account not specified"); - } + json::Value const jv = convertJV(arg, ticketOrSeq(arg.ticketSeq, arg.account)); - jv[jss::TransactionType] = jss::ConfidentialMPTConvert; - if (arg.id) - { - jv[sfMPTokenIssuanceID] = to_string(*arg.id); - } - else - { - if (!id_) - Throw("MPT has not been created"); - jv[sfMPTokenIssuanceID] = to_string(*id_); - } + Account const& account = requireValue(arg.account, "account"); + auto const amt = requireValue(arg.amt, "amt"); - if (arg.amt) - jv[sfMPTAmount.jsonName] = std::to_string(*arg.amt); - if (arg.holderPubKey) - jv[sfHolderEncryptionKey.jsonName] = strHex(*arg.holderPubKey); - - Buffer holderCiphertext; - Buffer issuerCiphertext; - std::optional auditorCiphertext; - Buffer blindingFactor; - - fillConversionCiphertexts( - arg, jv, holderCiphertext, issuerCiphertext, auditorCiphertext, blindingFactor); - - jv[sfBlindingFactor.jsonName] = strHex(blindingFactor); - if (arg.proof) - { - jv[sfZKProof.jsonName] = *arg.proof; - } - else if (arg.fillSchnorrProof.value_or(arg.holderPubKey.has_value())) - { - // whether to automatically generate and attach a Schnorr proof: - // if fillSchnorrProof is explicitly set, follow its value; - // otherwise, default to generating the proof only if holder pub key is - // present. - auto const seq = arg.ticketSeq.value_or(env_.seq(*arg.account)); - auto const contextHash = - getConvertContextHash(requireValue(arg.account, "account").id(), issuanceID(), seq); - - auto const proof = getSchnorrProof(*arg.account, contextHash); - if (proof) - { - jv[sfZKProof.jsonName] = strHex(*proof); - } - else - { - jv[sfZKProof.jsonName] = strHex(gMakeZeroBuffer(kEcSchnorrProofLength)); - } - } - - auto const holderAmt = getBalance(*arg.account); + auto const holderAmt = getBalance(account); auto const prevConfidentialOutstanding = getIssuanceConfidentialBalance(); - auto const prevInboxBalance = getDecryptedBalance(*arg.account, holderEncryptedInbox); - auto const prevSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending); - auto const prevIssuerBalance = getDecryptedBalance(*arg.account, issuerEncryptedBalance); + auto const prevInboxBalance = getDecryptedBalance(account, holderEncryptedInbox); + auto const prevSpendingBalance = getDecryptedBalance(account, holderEncryptedSpending); + auto const prevIssuerBalance = getDecryptedBalance(account, issuerEncryptedBalance); if (!prevInboxBalance || !prevSpendingBalance || !prevIssuerBalance) Throw("Failed to get Pre-convert balance"); @@ -1234,7 +1248,7 @@ MPTTester::convert(MPTConvert const& arg) std::optional prevAuditorBalance; if (hasAuditorAmt) { - prevAuditorBalance = getDecryptedBalance(*arg.account, auditorEncryptedBalance); + prevAuditorBalance = getDecryptedBalance(account, auditorEncryptedBalance); if (!prevAuditorBalance) Throw("Failed to get Pre-convert balance"); } @@ -1245,59 +1259,57 @@ MPTTester::convert(MPTConvert const& arg) { auto const postConfidentialOutstanding = getIssuanceConfidentialBalance(); auto const postOutstanding = getIssuanceOutstandingBalance(); - env_.require(MptBalance( - *this, requireValue(arg.account, "account"), holderAmt - requireValue(arg.amt, "amt"))); + env_.require(MptBalance(*this, account, holderAmt - amt)); env_.require(RequireAny([&]() -> bool { return prevOutstanding && postOutstanding && *prevOutstanding == *postOutstanding; })); env_.require(RequireAny([&]() -> bool { - return prevConfidentialOutstanding + *arg.amt == postConfidentialOutstanding; + return prevConfidentialOutstanding + amt == postConfidentialOutstanding; })); env_.require(RequireAny([&]() -> bool { - return getEncryptedBalance(*arg.account, holderEncryptedInbox).has_value(); + return getEncryptedBalance(account, holderEncryptedInbox).has_value(); })); env_.require(RequireAny([&]() -> bool { - return getEncryptedBalance(*arg.account, holderEncryptedSpending).has_value(); + return getEncryptedBalance(account, holderEncryptedSpending).has_value(); })); env_.require(RequireAny([&]() -> bool { - return getEncryptedBalance(*arg.account, issuerEncryptedBalance).has_value(); + return getEncryptedBalance(account, issuerEncryptedBalance).has_value(); })); - auto const postInboxBalance = getDecryptedBalance(*arg.account, holderEncryptedInbox); - auto const postIssuerBalance = getDecryptedBalance(*arg.account, issuerEncryptedBalance); - auto const postSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending); + auto const postInboxBalance = getDecryptedBalance(account, holderEncryptedInbox); + auto const postIssuerBalance = getDecryptedBalance(account, issuerEncryptedBalance); + auto const postSpendingBalance = getDecryptedBalance(account, holderEncryptedSpending); if (!postInboxBalance || !postIssuerBalance || !postSpendingBalance) Throw("Failed to get post-convert balance"); if (hasAuditorAmt) { - auto const postAuditorBalance = - getDecryptedBalance(*arg.account, auditorEncryptedBalance); + auto const postAuditorBalance = getDecryptedBalance(account, auditorEncryptedBalance); if (!postAuditorBalance) Throw("Failed to get post-convert auditor balance"); env_.require(RequireAny([&]() -> bool { - return getEncryptedBalance(*arg.account, auditorEncryptedBalance).has_value(); + return getEncryptedBalance(account, auditorEncryptedBalance).has_value(); })); // auditor's encrypted balance is updated correctly env_.require(RequireAny( - [&]() -> bool { return *prevAuditorBalance + *arg.amt == *postAuditorBalance; })); + [&]() -> bool { return *prevAuditorBalance + amt == *postAuditorBalance; })); } // 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; })); + env_.require( + RequireAny([&]() -> bool { return *prevIssuerBalance + amt == *postIssuerBalance; })); // holder's inbox balance is updated correctly - env_.require(RequireAny( - [&]() -> bool { return *prevInboxBalance + *arg.amt == *postInboxBalance; })); + env_.require( + RequireAny([&]() -> bool { return *prevInboxBalance + amt == *postInboxBalance; })); // sum of holder's inbox and spending balance should equal to issuer's // encrypted balance @@ -1312,7 +1324,7 @@ MPTTester::convert(MPTConvert const& arg) [&](SLEP const& sle) -> bool { if (sle) { - auto const holderPubKey = getPubKey(*arg.account); + auto const holderPubKey = getPubKey(account); if (!holderPubKey) { Throw( @@ -1324,7 +1336,7 @@ MPTTester::convert(MPTConvert const& arg) } return false; }, - arg.account); + account); })); } } @@ -1334,41 +1346,17 @@ json::Value MPTTester::convertJV(MPTConvert const& arg, std::uint32_t seq) { json::Value jv; - if (arg.account) - { - jv[sfAccount] = arg.account->human(); - } - else - { - Throw("Account not specified"); - } + Account const& account = setAccountField(jv, arg.account); jv[jss::TransactionType] = jss::ConfidentialMPTConvert; - if (arg.id) - { - jv[sfMPTokenIssuanceID] = to_string(*arg.id); - } - else - { - if (!id_) - Throw("MPT has not been created"); - jv[sfMPTokenIssuanceID] = to_string(*id_); - } + setIssuanceIdField(jv, arg.id); - if (arg.amt) - jv[sfMPTAmount.jsonName] = std::to_string(*arg.amt); + auto const amt = requireValue(arg.amt, "amt"); + jv[sfMPTAmount.jsonName] = std::to_string(amt); if (arg.holderPubKey) jv[sfHolderEncryptionKey.jsonName] = strHex(*arg.holderPubKey); - Buffer holderCiphertext; - Buffer issuerCiphertext; - std::optional auditorCiphertext; - Buffer blindingFactor; - - fillConversionCiphertexts( - arg, jv, holderCiphertext, issuerCiphertext, auditorCiphertext, blindingFactor); - - jv[sfBlindingFactor.jsonName] = strHex(blindingFactor); + fillConversionCiphertexts(arg, jv, account, amt); if (arg.proof) { @@ -1376,17 +1364,8 @@ MPTTester::convertJV(MPTConvert const& arg, std::uint32_t seq) } else if (arg.fillSchnorrProof.value_or(arg.holderPubKey.has_value())) { - auto const contextHash = - getConvertContextHash(requireValue(arg.account, "account").id(), issuanceID(), seq); - auto const proof = getSchnorrProof(*arg.account, contextHash); - if (proof) - { - jv[sfZKProof.jsonName] = strHex(*proof); - } - else - { - jv[sfZKProof.jsonName] = strHex(gMakeZeroBuffer(kEcSchnorrProofLength)); - } + auto const contextHash = getConvertContextHash(account.id(), issuanceID(), seq); + setProofOrDummy(jv, getSchnorrProof(account, contextHash), kEcSchnorrProofLength); } return jv; @@ -1395,253 +1374,48 @@ MPTTester::convertJV(MPTConvert const& arg, std::uint32_t seq) void MPTTester::send(MPTConfidentialSend const& arg) { - json::Value jv; - jv[jss::TransactionType] = jss::ConfidentialMPTSend; + json::Value const jv = sendJV(arg, ticketOrSeq(arg.ticketSeq, arg.account)); - if (arg.account) - { - jv[sfAccount] = arg.account->human(); - } - else - { - Throw("Account not specified"); - } - - if (arg.dest) - { - jv[sfDestination] = arg.dest->human(); - } - else - { - Throw("Destination not specified"); - } - - if (!arg.amt) - Throw("Amount not specified for testing purposes"); - - if (arg.id) - { - jv[sfMPTokenIssuanceID] = to_string(*arg.id); - } - else - { - if (!id_) - Throw("MPT has not been created"); - jv[sfMPTokenIssuanceID] = to_string(*id_); - } - - Buffer const blindingFactor = - arg.blindingFactor ? *arg.blindingFactor : generateBlindingFactor(); - - // fill in the encrypted amounts if not provided - auto const senderAmt = arg.senderEncryptedAmt - ? *arg.senderEncryptedAmt - : encryptAmount(*arg.account, *arg.amt, blindingFactor); - auto const destAmt = arg.destEncryptedAmt ? *arg.destEncryptedAmt - : encryptAmount(*arg.dest, *arg.amt, blindingFactor); - auto const issuerAmt = arg.issuerEncryptedAmt - ? *arg.issuerEncryptedAmt - : encryptAmount(issuer_, *arg.amt, blindingFactor); - - std::optional auditorAmt; - if (arg.auditorEncryptedAmt) - { - auditorAmt = arg.auditorEncryptedAmt; - } - else if (auditor_.has_value() && arg.fillAuditorEncryptedAmt.value_or(false)) - { - auditorAmt = encryptAmount( - requireValue(auditor_, "auditor"), requireValue(arg.amt, "amt"), blindingFactor); - } - - jv[sfSenderEncryptedAmount] = strHex(senderAmt); - jv[sfDestinationEncryptedAmount] = strHex(destAmt); - jv[sfIssuerEncryptedAmount] = strHex(issuerAmt); - if (auditorAmt) - jv[sfAuditorEncryptedAmount] = strHex(*auditorAmt); - - if (arg.credentials) - { - auto& arr(jv[sfCredentialIDs.jsonName] = json::ValueType::Array); - for (auto const& hash : *arg.credentials) - arr.append(hash); - } + Account const& account = requireValue(arg.account, "account"); + Account const& dest = requireValue(arg.dest, "dest"); + auto const amt = requireValue(arg.amt, "amt"); // Version counters before send - auto const prevSenderVersion = getMPTokenVersion(*arg.account); - auto const prevDestVersion = getMPTokenVersion(*arg.dest); + auto const prevSenderVersion = getMPTokenVersion(account); + auto const prevDestVersion = getMPTokenVersion(dest); // Sender's previous confidential state - auto const prevSenderInbox = getDecryptedBalance(*arg.account, holderEncryptedInbox); - auto const prevSenderSpending = getDecryptedBalance(*arg.account, holderEncryptedSpending); - auto const prevSenderIssuer = getDecryptedBalance(*arg.account, issuerEncryptedBalance); - auto const prevSenderInboxEncrypted = getEncryptedBalance(*arg.account, holderEncryptedInbox); - auto const prevSenderSpendingEncrypted = - getEncryptedBalance(*arg.account, holderEncryptedSpending); - auto const prevSenderIssuerEncrypted = - getEncryptedBalance(*arg.account, issuerEncryptedBalance); + auto const prevSenderInbox = getDecryptedBalance(account, holderEncryptedInbox); + auto const prevSenderSpending = getDecryptedBalance(account, holderEncryptedSpending); + auto const prevSenderIssuer = getDecryptedBalance(account, issuerEncryptedBalance); if (!prevSenderInbox || !prevSenderSpending || !prevSenderIssuer) Throw("Failed to get Pre-send balance"); std::optional prevSenderAuditor; - auto const prevSenderAuditorEncrypted = - getEncryptedBalance(*arg.account, auditorEncryptedBalance); if (arg.auditorEncryptedAmt || auditor_) { - prevSenderAuditor = getDecryptedBalance(*arg.account, auditorEncryptedBalance); + prevSenderAuditor = getDecryptedBalance(account, auditorEncryptedBalance); if (!prevSenderAuditor) Throw("Failed to get Pre-send balance"); } // Destination's previous confidential state - auto const prevDestInbox = getDecryptedBalance(*arg.dest, holderEncryptedInbox); - auto const prevDestSpending = getDecryptedBalance(*arg.dest, holderEncryptedSpending); - auto const prevDestIssuer = getDecryptedBalance(*arg.dest, issuerEncryptedBalance); - auto const prevDestInboxEncrypted = getEncryptedBalance(*arg.dest, holderEncryptedInbox); - auto const prevDestSpendingEncrypted = getEncryptedBalance(*arg.dest, holderEncryptedSpending); - auto const prevDestIssuerEncrypted = getEncryptedBalance(*arg.dest, issuerEncryptedBalance); + auto const prevDestInbox = getDecryptedBalance(dest, holderEncryptedInbox); + auto const prevDestSpending = getDecryptedBalance(dest, holderEncryptedSpending); + auto const prevDestIssuer = getDecryptedBalance(dest, issuerEncryptedBalance); if (!prevDestInbox || !prevDestSpending || !prevDestIssuer) Throw("Failed to get Pre-send balance"); std::optional prevDestAuditor; - auto const prevDestAuditorEncrypted = getEncryptedBalance(*arg.dest, auditorEncryptedBalance); if (arg.auditorEncryptedAmt || auditor_) { - prevDestAuditor = getDecryptedBalance(*arg.dest, auditorEncryptedBalance); + prevDestAuditor = getDecryptedBalance(dest, auditorEncryptedBalance); if (!prevDestAuditor) Throw("Failed to get Pre-send balance"); } - // Fill in the commitment if not provided - // The amount commitment must use the same blinding factor as the ElGamal - // encryption. The sigma proof links the two, so using different randomness - // for each would cause proof verification to fail. - Buffer amountCommitment, balanceCommitment; - if (arg.amountCommitment) - { - amountCommitment = *arg.amountCommitment; - } - else - { - amountCommitment = getPedersenCommitment(*arg.amt, blindingFactor); - } - - 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 - { - auto const version = getMPTokenVersion(*arg.account); - auto const seq = arg.ticketSeq.value_or(env_.seq(*arg.account)); - auto const ctxHash = getSendContextHash( - requireValue(arg.account, "account").id(), - issuanceID(), - seq, - requireValue(arg.dest, "dest").id(), - version); - - std::vector recipients; - - auto const senderPubKey = getPubKey(*arg.account); - auto const destPubKey = getPubKey(*arg.dest); - auto const issuerPubKey = getPubKey(issuer_); - - // If a key is missing, we skip adding the recipient. This intentionally - // causes proof generation to fail, triggering the dummy proof fallback. - if (senderPubKey) - { - recipients.push_back({ - .publicKey = Slice(*senderPubKey), - .encryptedAmount = senderAmt, - }); - } - if (destPubKey) - { - recipients.push_back({ - .publicKey = Slice(*destPubKey), - .encryptedAmount = destAmt, - }); - } - if (issuerPubKey) - { - recipients.push_back({ - .publicKey = Slice(*issuerPubKey), - .encryptedAmount = issuerAmt, - }); - } - - std::optional auditorPubKey; - if (auditorAmt) - { - if (!auditor_) - Throw("Auditor not registered"); - - auditorPubKey = getPubKey(*auditor_); - if (auditorPubKey) - { - recipients.push_back({ - .publicKey = Slice(*auditorPubKey), - .encryptedAmount = *auditorAmt, - }); - } - } - - std::optional proof; - - // Skip proof generation if encrypted balance is missing (e.g., - // feature disabled), when the sender and destination are the same - // (malformed case causing pcm to be zero), or when spending balance - // is 0 - if (arg.account != arg.dest && prevSenderSpendingEncrypted && *prevSenderSpending > 0) - { - proof = getConfidentialSendProof( - *arg.account, - *arg.amt, - recipients, - blindingFactor, - ctxHash, - { - .pedersenCommitment = amountCommitment, - .amt = *arg.amt, - .encryptedAmt = senderAmt, - .blindingFactor = blindingFactor, - }, - { - .pedersenCommitment = balanceCommitment, - .amt = *prevSenderSpending, - .encryptedAmt = *prevSenderSpendingEncrypted, - .blindingFactor = balanceBlindingFactor, - }); - } - - if (proof) - { - jv[sfZKProof.jsonName] = strHex(*proof); - } - else - { - jv[sfZKProof.jsonName] = strHex(gMakeZeroBuffer(kEcSendProofLength)); - } - } - - auto const senderPubAmt = getBalance(*arg.account); - auto const destPubAmt = getBalance(*arg.dest); + auto const senderPubAmt = getBalance(account); + auto const destPubAmt = getBalance(dest); auto const prevCOA = getIssuanceConfidentialBalance(); auto const prevOA = getIssuanceOutstandingBalance(); @@ -1651,24 +1425,24 @@ MPTTester::send(MPTConfidentialSend const& arg) auto const postOA = getIssuanceOutstandingBalance(); // Sender's post confidential state - auto const postSenderInbox = getDecryptedBalance(*arg.account, holderEncryptedInbox); - auto const postSenderSpending = getDecryptedBalance(*arg.account, holderEncryptedSpending); - auto const postSenderIssuer = getDecryptedBalance(*arg.account, issuerEncryptedBalance); + auto const postSenderInbox = getDecryptedBalance(account, holderEncryptedInbox); + auto const postSenderSpending = getDecryptedBalance(account, holderEncryptedSpending); + auto const postSenderIssuer = getDecryptedBalance(account, issuerEncryptedBalance); if (!postSenderInbox || !postSenderSpending || !postSenderIssuer) Throw("Failed to get Post-send balance"); // Destination's post confidential state - auto const postDestInbox = getDecryptedBalance(*arg.dest, holderEncryptedInbox); - auto const postDestSpending = getDecryptedBalance(*arg.dest, holderEncryptedSpending); - auto const postDestIssuer = getDecryptedBalance(*arg.dest, issuerEncryptedBalance); + auto const postDestInbox = getDecryptedBalance(dest, holderEncryptedInbox); + auto const postDestSpending = getDecryptedBalance(dest, holderEncryptedSpending); + auto const postDestIssuer = getDecryptedBalance(dest, issuerEncryptedBalance); if (!postDestInbox || !postDestSpending || !postDestIssuer) Throw("Failed to get Post-send balance"); // Public balances unchanged - env_.require(MptBalance(*this, *arg.account, senderPubAmt)); - env_.require(MptBalance(*this, *arg.dest, destPubAmt)); + env_.require(MptBalance(*this, account, senderPubAmt)); + env_.require(MptBalance(*this, dest, destPubAmt)); // OA and COA unchanged env_.require(RequireAny([&]() -> bool { return prevOA && postOA && *prevOA == *postOA; })); @@ -1676,21 +1450,18 @@ MPTTester::send(MPTConfidentialSend const& arg) // Verify sender changes env_.require(RequireAny([&]() -> bool { - return *prevSenderSpending >= *arg.amt && - *postSenderSpending == *prevSenderSpending - *arg.amt; + return *prevSenderSpending >= amt && *postSenderSpending == *prevSenderSpending - amt; })); env_.require(RequireAny([&]() -> bool { return postSenderInbox == prevSenderInbox; })); env_.require(RequireAny([&]() -> bool { - return *prevSenderIssuer >= *arg.amt && - *postSenderIssuer == *prevSenderIssuer - *arg.amt; + return *prevSenderIssuer >= amt && *postSenderIssuer == *prevSenderIssuer - amt; })); // Verify destination changes - env_.require( - RequireAny([&]() -> bool { return *postDestInbox == *prevDestInbox + *arg.amt; })); + env_.require(RequireAny([&]() -> bool { return *postDestInbox == *prevDestInbox + amt; })); env_.require(RequireAny([&]() -> bool { return *postDestSpending == *prevDestSpending; })); env_.require( - RequireAny([&]() -> bool { return *postDestIssuer == *prevDestIssuer + *arg.amt; })); + RequireAny([&]() -> bool { return *postDestIssuer == *prevDestIssuer + amt; })); // Cross checks env_.require(RequireAny( @@ -1700,15 +1471,14 @@ MPTTester::send(MPTConfidentialSend const& arg) // Version: sender increments by 1; receiver version is unchanged by incoming sends env_.require(RequireAny( - [&]() -> bool { return getMPTokenVersion(*arg.account) == prevSenderVersion + 1; })); + [&]() -> bool { return getMPTokenVersion(account) == prevSenderVersion + 1; })); env_.require( - RequireAny([&]() -> bool { return getMPTokenVersion(*arg.dest) == prevDestVersion; })); + RequireAny([&]() -> bool { return getMPTokenVersion(dest) == prevDestVersion; })); if (arg.auditorEncryptedAmt || auditor_) { - auto const postSenderAuditor = - getDecryptedBalance(*arg.account, auditorEncryptedBalance); - auto const postDestAuditor = getDecryptedBalance(*arg.dest, auditorEncryptedBalance); + auto const postSenderAuditor = getDecryptedBalance(account, auditorEncryptedBalance); + auto const postDestAuditor = getDecryptedBalance(dest, auditorEncryptedBalance); if (!postSenderAuditor || !postDestAuditor) Throw("Failed to get Post-send balance"); @@ -1719,13 +1489,12 @@ MPTTester::send(MPTConfidentialSend const& arg) // verify sender env_.require(RequireAny([&]() -> bool { - return prevSenderAuditor >= *arg.amt && - *postSenderAuditor == *prevSenderAuditor - *arg.amt; + return *prevSenderAuditor >= amt && *postSenderAuditor == *prevSenderAuditor - amt; })); // verify dest - env_.require(RequireAny( - [&]() -> bool { return *postDestAuditor == *prevDestAuditor + *arg.amt; })); + env_.require( + RequireAny([&]() -> bool { return *postDestAuditor == *prevDestAuditor + amt; })); } } } @@ -1739,49 +1508,21 @@ MPTTester::sendJV( json::Value jv; jv[jss::TransactionType] = jss::ConfidentialMPTSend; - if (arg.account) - { - jv[sfAccount] = arg.account->human(); - } - else - { - Throw("Account not specified"); - } + Account const& account = setAccountField(jv, arg.account); + Account const& dest = setDestinationField(jv, arg.dest); + auto const amt = requireValue(arg.amt, "amt"); - if (arg.dest) - { - jv[sfDestination] = arg.dest->human(); - } - else - { - Throw("Destination not specified"); - } - - if (!arg.amt) - Throw("Amount not specified for testing purposes"); - - if (arg.id) - { - jv[sfMPTokenIssuanceID] = to_string(*arg.id); - } - else - { - if (!id_) - Throw("MPT has not been created"); - jv[sfMPTokenIssuanceID] = to_string(*id_); - } + setIssuanceIdField(jv, arg.id); Buffer const blindingFactor = arg.blindingFactor ? *arg.blindingFactor : generateBlindingFactor(); - auto const senderAmt = arg.senderEncryptedAmt - ? *arg.senderEncryptedAmt - : encryptAmount(*arg.account, *arg.amt, blindingFactor); - auto const destAmt = arg.destEncryptedAmt ? *arg.destEncryptedAmt - : encryptAmount(*arg.dest, *arg.amt, blindingFactor); - auto const issuerAmt = arg.issuerEncryptedAmt - ? *arg.issuerEncryptedAmt - : encryptAmount(issuer_, *arg.amt, blindingFactor); + auto const senderAmt = arg.senderEncryptedAmt ? *arg.senderEncryptedAmt + : encryptAmount(account, amt, blindingFactor); + auto const destAmt = + arg.destEncryptedAmt ? *arg.destEncryptedAmt : encryptAmount(dest, amt, blindingFactor); + auto const issuerAmt = arg.issuerEncryptedAmt ? *arg.issuerEncryptedAmt + : encryptAmount(issuer_, amt, blindingFactor); std::optional auditorAmt; if (arg.auditorEncryptedAmt) @@ -1790,8 +1531,7 @@ MPTTester::sendJV( } else if (auditor_.has_value() && arg.fillAuditorEncryptedAmt.value_or(false)) { - auditorAmt = encryptAmount( - requireValue(auditor_, "auditor"), requireValue(arg.amt, "amt"), blindingFactor); + auditorAmt = encryptAmount(requireValue(auditor_, "auditor"), amt, blindingFactor); } jv[sfSenderEncryptedAmount] = strHex(senderAmt); @@ -1818,12 +1558,12 @@ MPTTester::sendJV( } else { - auto const ledgerSpending = getDecryptedBalance(*arg.account, holderEncryptedSpending); + auto const ledgerSpending = getDecryptedBalance(account, holderEncryptedSpending); if (!ledgerSpending) Throw("Failed to get sender spending balance"); prevSenderSpending = *ledgerSpending; - prevEncryptedSenderSpending = getEncryptedBalance(*arg.account, holderEncryptedSpending); - version = getMPTokenVersion(*arg.account); + prevEncryptedSenderSpending = getEncryptedBalance(account, holderEncryptedSpending); + version = getMPTokenVersion(account); } // The amount commitment must use the same blinding factor as the tx ElGamal @@ -1835,7 +1575,7 @@ MPTTester::sendJV( } else { - amountCommitment = getPedersenCommitment(*arg.amt, blindingFactor); + amountCommitment = getPedersenCommitment(amt, blindingFactor); } jv[sfAmountCommitment] = strHex(amountCommitment); @@ -1858,17 +1598,13 @@ MPTTester::sendJV( } else { - auto const ctxHash = getSendContextHash( - requireValue(arg.account, "account").id(), - issuanceID(), - seq, - requireValue(arg.dest, "dest").id(), - version); + auto const ctxHash = + getSendContextHash(account.id(), issuanceID(), seq, dest.id(), version); std::vector recipients; - auto const senderPubKey = getPubKey(*arg.account); - auto const destPubKey = getPubKey(*arg.dest); + auto const senderPubKey = getPubKey(account); + auto const destPubKey = getPubKey(dest); auto const issuerPubKey = getPubKey(issuer_); if (senderPubKey) @@ -1911,17 +1647,17 @@ MPTTester::sendJV( std::optional proof; // Skip proof generation when spending balance is 0 - if (arg.account != arg.dest && prevEncryptedSenderSpending && prevSenderSpending > 0) + if (prevEncryptedSenderSpending && prevSenderSpending > 0) { proof = getConfidentialSendProof( - *arg.account, - *arg.amt, + account, + amt, recipients, blindingFactor, ctxHash, { .pedersenCommitment = amountCommitment, - .amt = *arg.amt, + .amt = amt, .encryptedAmt = senderAmt, .blindingFactor = blindingFactor, }, @@ -1933,14 +1669,7 @@ MPTTester::sendJV( }); } - if (proof) - { - jv[sfZKProof.jsonName] = strHex(*proof); - } - else - { - jv[sfZKProof.jsonName] = strHex(gMakeZeroBuffer(kEcSendProofLength)); - } + setProofOrDummy(jv, proof, kEcSendProofLength); } return jv; @@ -2003,31 +1732,14 @@ MPTTester::confidentialClaw(MPTConfidentialClawback const& arg) auto const account = arg.account ? *arg.account : issuer_; jv[sfAccount] = account.human(); - if (arg.holder) - { - jv[sfHolder] = arg.holder->human(); - } - else - { - Throw("Holder not specified"); - } + Account const& holder = requireValue(arg.holder, "holder"); + jv[sfHolder] = holder.human(); jv[jss::TransactionType] = jss::ConfidentialMPTClawback; - if (arg.id) - { - jv[sfMPTokenIssuanceID] = to_string(*arg.id); - } - else if (id_) - { - jv[sfMPTokenIssuanceID] = to_string(*id_); - } - else - { - Throw("MPT has not been created"); - } + setIssuanceIdField(jv, arg.id); - if (arg.amt) - jv[sfMPTAmount] = std::to_string(*arg.amt); + auto const amt = requireValue(arg.amt, "amt"); + jv[sfMPTAmount] = std::to_string(amt); if (arg.proof) { @@ -2036,62 +1748,49 @@ MPTTester::confidentialClaw(MPTConfidentialClawback const& arg) else { auto const seq = arg.ticketSeq ? *arg.ticketSeq : env_.seq(account); - auto const contextHash = getClawbackContextHash( - account.id(), issuanceID(), seq, requireValue(arg.holder, "holder").id()); + auto const contextHash = + getClawbackContextHash(account.id(), issuanceID(), seq, holder.id()); auto const privKey = getPrivKey(account); if (!privKey || privKey->size() != kEcPrivKeyLength) Throw("Failed to get clawback private key"); - auto const proof = getClawbackProof( - requireValue(arg.holder, "holder"), - requireValue(arg.amt, "amt"), - requireValue(privKey, "privKey"), - contextHash); + auto const proof = + getClawbackProof(holder, amt, requireValue(privKey, "privKey"), contextHash); - if (proof) - { - jv[sfZKProof] = strHex(*proof); - } - else - { - jv[sfZKProof] = strHex(gMakeZeroBuffer(kEcClawbackProofLength)); - } + setProofOrDummy(jv, proof, kEcClawbackProofLength); } - auto const holderPubAmt = getBalance(*arg.holder); + auto const holderPubAmt = getBalance(holder); auto const prevCOA = getIssuanceConfidentialBalance(); auto const prevOA = getIssuanceOutstandingBalance(); - auto const prevVersion = getMPTokenVersion(*arg.holder); + auto const prevVersion = getMPTokenVersion(holder); if (submit(arg, jv) == tesSUCCESS) { auto const postCOA = getIssuanceConfidentialBalance(); auto const postOA = getIssuanceOutstandingBalance(); - auto const postVersion = getMPTokenVersion(*arg.holder); + auto const postVersion = getMPTokenVersion(holder); // Verify holder's public balance is unchanged - env_.require(MptBalance(*this, *arg.holder, holderPubAmt)); + env_.require(MptBalance(*this, 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 prevCOA >= amt && postCOA == prevCOA - amt; })); env_.require(RequireAny([&]() -> bool { - return prevOA && postOA && *prevOA >= *arg.amt && *postOA == *prevOA - *arg.amt; + return prevOA && postOA && *prevOA >= amt && *postOA == *prevOA - amt; })); // Verify holder's confidential balances are zeroed out env_.require(RequireAny( - [&]() -> bool { return getDecryptedBalance(*arg.holder, holderEncryptedInbox) == 0; })); - env_.require(RequireAny([&]() -> bool { - return getDecryptedBalance(*arg.holder, holderEncryptedSpending) == 0; - })); - env_.require(RequireAny([&]() -> bool { - return getDecryptedBalance(*arg.holder, issuerEncryptedBalance) == 0; - })); - env_.require(RequireAny([&]() -> bool { - return getDecryptedBalance(*arg.holder, auditorEncryptedBalance) == 0; - })); + [&]() -> bool { return getDecryptedBalance(holder, holderEncryptedInbox) == 0; })); + env_.require(RequireAny( + [&]() -> bool { return getDecryptedBalance(holder, holderEncryptedSpending) == 0; })); + env_.require(RequireAny( + [&]() -> bool { return getDecryptedBalance(holder, issuerEncryptedBalance) == 0; })); + env_.require(RequireAny( + [&]() -> bool { return getDecryptedBalance(holder, auditorEncryptedBalance) == 0; })); // Verify version is incremented env_.require(RequireAny([&]() -> bool { return postVersion == prevVersion + 1; })); @@ -2213,30 +1912,14 @@ MPTTester::getDecryptedBalance(Account const& account, EncryptedBalanceType bala } return decryptAmount(decryptor, *encryptedAmt, epoch); -}; +} json::Value MPTTester::mergeInboxJV(MPTMergeInbox const& arg) const { json::Value jv; - if (arg.account) - { - jv[sfAccount] = arg.account->human(); - } - else - { - Throw("Account not specified"); - } - if (arg.id) - { - jv[sfMPTokenIssuanceID] = to_string(*arg.id); - } - else - { - if (!id_) - Throw("MPT has not been created"); - jv[sfMPTokenIssuanceID] = to_string(*id_); - } + setAccountField(jv, arg.account); + setIssuanceIdField(jv, arg.id); jv[sfTransactionType] = jss::ConfidentialMPTMergeInbox; return jv; } @@ -2244,36 +1927,18 @@ MPTTester::mergeInboxJV(MPTMergeInbox const& arg) const void MPTTester::mergeInbox(MPTMergeInbox const& arg) { - json::Value jv; - if (arg.account) - { - jv[sfAccount] = arg.account->human(); - } - else - { - Throw("Account not specified"); - } - if (arg.id) - { - jv[sfMPTokenIssuanceID] = to_string(*arg.id); - } - else - { - if (!id_) - Throw("MPT has not been created"); - jv[sfMPTokenIssuanceID] = to_string(*id_); - } + json::Value const jv = mergeInboxJV(arg); + Account const& account = requireValue(arg.account, "account"); - jv[sfTransactionType] = jss::ConfidentialMPTMergeInbox; - auto const holderPubAmt = getBalance(*arg.account); + auto const holderPubAmt = getBalance(account); auto const prevCOA = getIssuanceConfidentialBalance(); auto const prevOA = getIssuanceOutstandingBalance(); - auto const prevInboxBalance = getDecryptedBalance(*arg.account, holderEncryptedInbox); - auto const prevSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending); - auto const prevIssuerBalance = getDecryptedBalance(*arg.account, issuerEncryptedBalance); - auto const prevIssuerEncrypted = getEncryptedBalance(*arg.account, issuerEncryptedBalance); - auto const prevAuditorEncrypted = getEncryptedBalance(*arg.account, auditorEncryptedBalance); - auto const prevVersion = getMPTokenVersion(*arg.account); + auto const prevInboxBalance = getDecryptedBalance(account, holderEncryptedInbox); + auto const prevSpendingBalance = getDecryptedBalance(account, holderEncryptedSpending); + auto const prevIssuerBalance = getDecryptedBalance(account, issuerEncryptedBalance); + auto const prevIssuerEncrypted = getEncryptedBalance(account, issuerEncryptedBalance); + auto const prevAuditorEncrypted = getEncryptedBalance(account, auditorEncryptedBalance); + auto const prevVersion = getMPTokenVersion(account); if (!prevInboxBalance || !prevSpendingBalance || !prevIssuerBalance) Throw("Failed to get pre-mergeInbox balances"); @@ -2282,20 +1947,19 @@ MPTTester::mergeInbox(MPTMergeInbox const& arg) { auto const postCOA = getIssuanceConfidentialBalance(); auto const postOA = getIssuanceOutstandingBalance(); - auto const postInboxBalance = getDecryptedBalance(*arg.account, holderEncryptedInbox); - auto const postSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending); - auto const postIssuerBalance = getDecryptedBalance(*arg.account, issuerEncryptedBalance); - auto const postInboxEncrypted = getEncryptedBalance(*arg.account, holderEncryptedInbox); - auto const postIssuerEncrypted = getEncryptedBalance(*arg.account, issuerEncryptedBalance); - auto const postAuditorEncrypted = - getEncryptedBalance(*arg.account, auditorEncryptedBalance); - auto const postVersion = getMPTokenVersion(*arg.account); + auto const postInboxBalance = getDecryptedBalance(account, holderEncryptedInbox); + auto const postSpendingBalance = getDecryptedBalance(account, holderEncryptedSpending); + auto const postIssuerBalance = getDecryptedBalance(account, issuerEncryptedBalance); + auto const postInboxEncrypted = getEncryptedBalance(account, holderEncryptedInbox); + auto const postIssuerEncrypted = getEncryptedBalance(account, issuerEncryptedBalance); + auto const postAuditorEncrypted = getEncryptedBalance(account, auditorEncryptedBalance); + auto const postVersion = getMPTokenVersion(account); if (!postInboxBalance || !postSpendingBalance || !postIssuerBalance || !prevIssuerEncrypted || !postInboxEncrypted || !postIssuerEncrypted) Throw("Failed to get post-mergeInbox balances"); - env_.require(MptBalance(*this, *arg.account, holderPubAmt)); + env_.require(MptBalance(*this, account, holderPubAmt)); env_.require(RequireAny([&]() -> bool { return prevOA && postOA && *prevOA == *postOA; })); env_.require(RequireAny([&]() -> bool { return prevCOA == postCOA; })); @@ -2307,14 +1971,12 @@ MPTTester::mergeInbox(MPTMergeInbox const& arg) env_.require( RequireAny([&]() -> bool { return *prevIssuerBalance == *postIssuerBalance; })); - auto const holderPubKey = getPubKey(*arg.account); + auto const holderPubKey = getPubKey(account); if (!holderPubKey) Throw("Failed to get holder public key"); auto const expectedInbox = encryptCanonicalZeroAmount( - requireValue(holderPubKey, "holderPubKey"), - requireValue(arg.account, "account").id(), - issuanceID()); + requireValue(holderPubKey, "holderPubKey"), account.id(), issuanceID()); if (!expectedInbox) Throw("Failed to get canonical zero encryption"); @@ -2366,158 +2028,74 @@ MPTTester::getMPTokenVersion(Account const account) const void MPTTester::convertBack(MPTConvertBack const& arg) { - json::Value jv; - if (arg.account) - { - jv[sfAccount] = arg.account->human(); - } - else - { - Throw("Account not specified"); - } + json::Value const jv = convertBackJV(arg, ticketOrSeq(arg.ticketSeq, arg.account)); - jv[jss::TransactionType] = jss::ConfidentialMPTConvertBack; - if (arg.id) - { - jv[sfMPTokenIssuanceID] = to_string(*arg.id); - } - else - { - if (!id_) - Throw("MPT has not been created"); - jv[sfMPTokenIssuanceID] = to_string(*id_); - } + Account const& account = requireValue(arg.account, "account"); + auto const amt = requireValue(arg.amt, "amt"); - if (arg.amt) - jv[sfMPTAmount.jsonName] = std::to_string(*arg.amt); - - Buffer holderCiphertext; - Buffer issuerCiphertext; - std::optional auditorCiphertext; - Buffer blindingFactor; - - fillConversionCiphertexts( - arg, jv, holderCiphertext, issuerCiphertext, auditorCiphertext, blindingFactor); - - jv[sfBlindingFactor] = strHex(blindingFactor); - - auto const prevInboxBalance = getDecryptedBalance(*arg.account, holderEncryptedInbox); - auto const prevSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending); - auto const prevIssuerBalance = getDecryptedBalance(*arg.account, issuerEncryptedBalance); + auto const prevInboxBalance = getDecryptedBalance(account, holderEncryptedInbox); + auto const prevSpendingBalance = getDecryptedBalance(account, holderEncryptedSpending); + auto const prevIssuerBalance = getDecryptedBalance(account, issuerEncryptedBalance); if (!prevInboxBalance || !prevSpendingBalance || !prevIssuerBalance) Throw("Failed to get Pre-convertBack balance"); - Buffer pedersenCommitment; - Buffer const pcBlindingFactor = generateBlindingFactor(); - if (arg.pedersenCommitment) - { - pedersenCommitment = *arg.pedersenCommitment; - } - else - { - pedersenCommitment = getPedersenCommitment(*prevSpendingBalance, pcBlindingFactor); - } - - jv[sfBalanceCommitment] = strHex(pedersenCommitment); - - if (arg.proof) - { - jv[sfZKProof.jsonName] = strHex(*arg.proof); - } - else - { - auto const version = getMPTokenVersion(*arg.account); - - // if the caller generated ciphertexts themselves, they should also - // generate the proof themselves from the blinding factor - auto const seq = arg.ticketSeq.value_or(env_.seq(*arg.account)); - auto const contextHash = getConvertBackContextHash( - requireValue(arg.account, "account").id(), issuanceID(), seq, version); - auto const prevEncryptedSpendingBalance = - getEncryptedBalance(*arg.account, holderEncryptedSpending); - - Buffer proof; - // generate a dummy proof if no encrypted amount field, so that other - // preflight/preclaim are checked - if (!prevEncryptedSpendingBalance) - { - proof = gMakeZeroBuffer(kEcConvertBackProofLength); - } - else - { - proof = getConvertBackProof( - *arg.account, - requireValue(arg.amt, "amt"), - contextHash, - { - .pedersenCommitment = pedersenCommitment, - .amt = *prevSpendingBalance, - .encryptedAmt = *prevEncryptedSpendingBalance, - .blindingFactor = pcBlindingFactor, - }); - } - jv[sfZKProof] = strHex(proof); - } - - auto const holderAmt = getBalance(*arg.account); + auto const holderAmt = getBalance(account); auto const prevConfidentialOutstanding = getIssuanceConfidentialBalance(); std::optional prevAuditorBalance; if (arg.auditorEncryptedAmt || auditor_) { - prevAuditorBalance = getDecryptedBalance(*arg.account, auditorEncryptedBalance); + prevAuditorBalance = getDecryptedBalance(account, auditorEncryptedBalance); if (!prevAuditorBalance) Throw("Failed to get Pre-convertBack balance"); } auto const prevOutstanding = getIssuanceOutstandingBalance(); - auto const prevVersion = getMPTokenVersion(*arg.account); + auto const prevVersion = getMPTokenVersion(account); if (submit(arg, jv) == tesSUCCESS) { auto const postConfidentialOutstanding = getIssuanceConfidentialBalance(); auto const postOutstanding = getIssuanceOutstandingBalance(); - auto const postVersion = getMPTokenVersion(*arg.account); - env_.require(MptBalance( - *this, requireValue(arg.account, "account"), holderAmt + requireValue(arg.amt, "amt"))); + auto const postVersion = getMPTokenVersion(account); + env_.require(MptBalance(*this, account, holderAmt + amt)); env_.require(RequireAny([&]() -> bool { return prevOutstanding && postOutstanding && *prevOutstanding == *postOutstanding; })); env_.require(RequireAny([&]() -> bool { - return prevConfidentialOutstanding - *arg.amt == postConfidentialOutstanding; + return prevConfidentialOutstanding - amt == postConfidentialOutstanding; })); - auto const postInboxBalance = getDecryptedBalance(*arg.account, holderEncryptedInbox); - auto const postIssuerBalance = getDecryptedBalance(*arg.account, issuerEncryptedBalance); - auto const postSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending); + auto const postInboxBalance = getDecryptedBalance(account, holderEncryptedInbox); + auto const postIssuerBalance = getDecryptedBalance(account, issuerEncryptedBalance); + auto const postSpendingBalance = getDecryptedBalance(account, holderEncryptedSpending); if (!postInboxBalance || !postIssuerBalance || !postSpendingBalance) Throw("Failed to get post-convertBack balance"); if (arg.auditorEncryptedAmt || auditor_) { - auto const postAuditorBalance = - getDecryptedBalance(*arg.account, auditorEncryptedBalance); + auto const postAuditorBalance = getDecryptedBalance(account, auditorEncryptedBalance); if (!postAuditorBalance) Throw("Failed to get post-convertBack balance"); // auditor's encrypted balance is updated correctly env_.require(RequireAny( - [&]() -> bool { return *prevAuditorBalance - *arg.amt == *postAuditorBalance; })); + [&]() -> bool { return *prevAuditorBalance - amt == *postAuditorBalance; })); } // 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; })); + env_.require( + RequireAny([&]() -> bool { return *prevIssuerBalance - amt == *postIssuerBalance; })); // holder's spending balance is updated correctly env_.require(RequireAny( - [&]() -> bool { return *prevSpendingBalance - *arg.amt == *postSpendingBalance; })); + [&]() -> bool { return *prevSpendingBalance - amt == *postSpendingBalance; })); // holder's confidential balance version is updated correctly env_.require(RequireAny([&]() -> bool { return postVersion == prevVersion + 1; })); @@ -2534,41 +2112,17 @@ json::Value MPTTester::convertBackJV(MPTConvertBack const& arg, std::uint32_t seq) { json::Value jv; - if (arg.account) - { - jv[sfAccount] = arg.account->human(); - } - else - { - Throw("Account not specified"); - } + Account const& account = setAccountField(jv, arg.account); jv[jss::TransactionType] = jss::ConfidentialMPTConvertBack; - if (arg.id) - { - jv[sfMPTokenIssuanceID] = to_string(*arg.id); - } - else - { - if (!id_) - Throw("MPT has not been created"); - jv[sfMPTokenIssuanceID] = to_string(*id_); - } + setIssuanceIdField(jv, arg.id); - if (arg.amt) - jv[sfMPTAmount.jsonName] = std::to_string(*arg.amt); + auto const amt = requireValue(arg.amt, "amt"); + jv[sfMPTAmount.jsonName] = std::to_string(amt); - Buffer holderCiphertext; - Buffer issuerCiphertext; - std::optional auditorCiphertext; - Buffer blindingFactor; + fillConversionCiphertexts(arg, jv, account, amt); - fillConversionCiphertexts( - arg, jv, holderCiphertext, issuerCiphertext, auditorCiphertext, blindingFactor); - - jv[sfBlindingFactor] = strHex(blindingFactor); - - auto const prevSpendingBalance = getDecryptedBalance(*arg.account, holderEncryptedSpending); + auto const prevSpendingBalance = getDecryptedBalance(account, holderEncryptedSpending); if (!prevSpendingBalance) Throw("convertBackJV: failed to read spending balance from ledger"); @@ -2591,21 +2145,17 @@ MPTTester::convertBackJV(MPTConvertBack const& arg, std::uint32_t seq) } else { - auto const version = getMPTokenVersion(*arg.account); - auto const prevEncSpending = getEncryptedBalance(*arg.account, holderEncryptedSpending); - auto const contextHash = getConvertBackContextHash( - requireValue(arg.account, "account").id(), issuanceID(), seq, version); + auto const version = getMPTokenVersion(account); + auto const prevEncSpending = getEncryptedBalance(account, holderEncryptedSpending); + auto const contextHash = + getConvertBackContextHash(account.id(), issuanceID(), seq, version); - Buffer proof; - if (!prevEncSpending) - { - proof = gMakeZeroBuffer(kEcConvertBackProofLength); - } - else + std::optional proof; + if (prevEncSpending) { proof = getConvertBackProof( - *arg.account, - requireValue(arg.amt, "amt"), + account, + amt, contextHash, { .pedersenCommitment = pedersenCommitment, @@ -2615,7 +2165,7 @@ MPTTester::convertBackJV(MPTConvertBack const& arg, std::uint32_t seq) }); } - jv[sfZKProof] = strHex(proof); + setProofOrDummy(jv, proof, kEcConvertBackProofLength); } return jv; diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index 31a809dce6..a737e76320 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -763,7 +763,7 @@ public: PedersenProofParams const& amountParams, PedersenProofParams const& balanceParams) const; - [[nodiscard]] Buffer + [[nodiscard]] std::optional getConvertBackProof( Account const& holder, std::uint64_t const amount, @@ -862,15 +862,28 @@ private: [[nodiscard]] std::uint32_t getFlags(std::optional const& holder) const; + /** + * @brief Sets sfMPTokenIssuanceID on jv, falling back to id_ if arg's id is + * not set. + * + * @param jv The JSON object to set the field on. + * @param id The explicit issuance ID override from the caller, if any. + */ + void + setIssuanceIdField(json::Value& jv, std::optional const& id) const; + + [[nodiscard]] std::uint32_t + ticketOrSeq( + std::optional const& ticketSeq, + std::optional const& account) const; + template void fillConversionCiphertexts( T const& arg, json::Value& jv, - Buffer& holderCiphertext, - Buffer& issuerCiphertext, - std::optional& auditorCiphertext, - Buffer& blindingFactor) const; + Account const& account, + std::uint64_t const amount) const; }; } // namespace xrpl::test::jtx diff --git a/src/test/overlay/cluster_test.cpp b/src/test/overlay/cluster_test.cpp index 0a51f98594..06df4fb73a 100644 --- a/src/test/overlay/cluster_test.cpp +++ b/src/test/overlay/cluster_test.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -96,6 +97,29 @@ public: } } + { + testcase("Membership: isMember agrees with member"); + + // Number of network nodes that also belong to the cluster. + std::size_t const overlapCount = 16; + + // Total size of the cluster once padded with non-network nodes. + std::size_t const clusterSize = 32; + + std::vector cluster(network.begin(), network.begin() + overlapCount); + + while (cluster.size() != clusterSize) + cluster.push_back(randomNode()); + + auto c = create(cluster); + + for (auto const& n : cluster) + BEAST_EXPECT(c->isMember(n)); + + for (auto const& n : network) + BEAST_EXPECT(c->isMember(n) == static_cast(c->member(n))); + } + { testcase("Membership: Non-empty cluster and all present"); diff --git a/src/xrpld/overlay/Cluster.h b/src/xrpld/overlay/Cluster.h index 703e1601aa..bffebca550 100644 --- a/src/xrpld/overlay/Cluster.h +++ b/src/xrpld/overlay/Cluster.h @@ -62,6 +62,19 @@ public: std::optional member(PublicKey const& node) const; + /** + * Determines whether a node belongs in the cluster. + * + * Prefer this to `member` when the comment is not wanted: `member` + * copies the node's name out from under the lock, and most callers + * only test the result for engagement. + * + * @param node The node's public identity. + * @return Whether the node is a cluster member. + */ + bool + isMember(PublicKey const& node) const; + /** * The number of nodes in the cluster list. */ diff --git a/src/xrpld/overlay/detail/Cluster.cpp b/src/xrpld/overlay/detail/Cluster.cpp index 15c8fa9c66..5e2e2053e9 100644 --- a/src/xrpld/overlay/detail/Cluster.cpp +++ b/src/xrpld/overlay/detail/Cluster.cpp @@ -38,6 +38,14 @@ Cluster::member(PublicKey const& identity) const return iter->name(); } +bool +Cluster::isMember(PublicKey const& identity) const +{ + std::scoped_lock const lock(mutex_); + + return nodes_.contains(identity); +} + std::size_t Cluster::size() const { diff --git a/src/xrpld/overlay/detail/OverlayImpl.cpp b/src/xrpld/overlay/detail/OverlayImpl.cpp index 587f4f810c..7f5dfcc30a 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.cpp +++ b/src/xrpld/overlay/detail/OverlayImpl.cpp @@ -306,7 +306,7 @@ OverlayImpl::onHandoff( { // The node gets a reserved slot if it is in our cluster // or if it has a reservation. - bool const reserved = static_cast(app_.getCluster().member(publicKey)) || + bool const reserved = app_.getCluster().isMember(publicKey) || app_.getPeerReservations().contains(publicKey); auto const result = peerFinder_->activate(slot, publicKey, reserved); if (result != peer_finder::Result::Success) diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index c56ea5797f..318ee6bae6 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -413,7 +413,7 @@ PeerImp::crawl() const bool PeerImp::cluster() const { - return static_cast(app_.getCluster().member(publicKey_)); + return app_.getCluster().isMember(publicKey_); } std::string