Skip to content

Commit 40549de

Browse files
tests: add replay tests to confidential MPT
1 parent c2f8b91 commit 40549de

File tree

1 file changed

+307
-0
lines changed

1 file changed

+307
-0
lines changed

src/test/app/ConfidentialTransfer_test.cpp

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3778,6 +3778,310 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
37783778
}
37793779
}
37803780

3781+
// This test verifies that proofs are non-replayable by simulating replays
3782+
// with an outdated ledger version or an old sequence number.
3783+
// It confirms that the validator detects the resulting ContextID mismatch
3784+
// and rejects the transaction with tecBAD_PROOF.
3785+
void
3786+
testProofContextBinding(FeatureBitset features)
3787+
{
3788+
testcase("Proof context binding (Sequence and Version)");
3789+
using namespace test::jtx;
3790+
3791+
Env env{*this, features};
3792+
Account const alice("alice");
3793+
Account const bob("bob");
3794+
MPTTester mptAlice(env, alice, {.holders = {bob}});
3795+
3796+
mptAlice.create(
3797+
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanPrivacy});
3798+
3799+
mptAlice.authorize({.account = bob});
3800+
mptAlice.pay(alice, bob, 100);
3801+
3802+
mptAlice.generateKeyPair(alice);
3803+
3804+
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
3805+
3806+
mptAlice.generateKeyPair(bob);
3807+
3808+
mptAlice.convert({
3809+
.account = bob,
3810+
.amt = 40,
3811+
.holderPubKey = mptAlice.getPubKey(bob),
3812+
});
3813+
3814+
mptAlice.mergeInbox({
3815+
.account = bob,
3816+
});
3817+
3818+
uint64_t const amt = 10;
3819+
Buffer const blindingFactor = generateBlindingFactor();
3820+
Buffer const pcBlindingFactor = generateBlindingFactor();
3821+
3822+
auto const spendingBalance = mptAlice.getDecryptedBalance(bob, MPTTester::HOLDER_ENCRYPTED_SPENDING);
3823+
BEAST_EXPECT(spendingBalance.has_value() && *spendingBalance == 40); // because bob encrypted 40
3824+
auto const encryptedSpendingBalance = mptAlice.getEncryptedBalance(bob, MPTTester::HOLDER_ENCRYPTED_SPENDING);
3825+
BEAST_EXPECT(encryptedSpendingBalance.has_value() && !encryptedSpendingBalance->empty());
3826+
3827+
Buffer const pedersenCommitment = mptAlice.getPedersenCommitment(*spendingBalance, pcBlindingFactor);
3828+
Buffer const issuerCiphertext = mptAlice.encryptAmount(alice, amt, blindingFactor);
3829+
Buffer const bobCiphertext = mptAlice.encryptAmount(bob, amt, blindingFactor);
3830+
3831+
auto const currentVersion = mptAlice.getMPTokenVersion(bob);
3832+
3833+
// Invalid Version Binding
3834+
// Simulates replaying a full transaction after the ledger's version
3835+
// has updated. We simulate this by attempting to use a proof built
3836+
// using an older version but with the current valid sequence.
3837+
{
3838+
uint32_t const seqA = env.seq(bob);
3839+
uint32_t const oldVersion = currentVersion - 1;
3840+
uint256 const badContextHash = getConvertBackContextHash(bob, seqA, mptAlice.issuanceID(), amt, oldVersion);
3841+
3842+
Buffer const proof = mptAlice.getConvertBackProof(
3843+
bob,
3844+
amt,
3845+
badContextHash,
3846+
{
3847+
.pedersenCommitment = pedersenCommitment,
3848+
.amt = *spendingBalance,
3849+
.encryptedAmt = *encryptedSpendingBalance,
3850+
.blindingFactor = pcBlindingFactor,
3851+
});
3852+
3853+
mptAlice.convertBack(
3854+
{.account = bob,
3855+
.amt = amt,
3856+
.proof = proof,
3857+
.holderEncryptedAmt = bobCiphertext,
3858+
.issuerEncryptedAmt = issuerCiphertext,
3859+
.blindingFactor = blindingFactor,
3860+
.pedersenCommitment = pedersenCommitment,
3861+
.err = tecBAD_PROOF});
3862+
}
3863+
3864+
// Invalid Sequence Binding
3865+
// Simulates submitting a new transaction (with a new, valid signature
3866+
// and sequence) but reusing a ZKP from a previous sequence number.
3867+
{
3868+
// Fetch updated sequence, as the tecBAD_PROOF above consumed one
3869+
uint32_t const seqB = env.seq(bob);
3870+
uint32_t const oldSeq = seqB - 1;
3871+
uint256 const badContextHash =
3872+
getConvertBackContextHash(bob, oldSeq, mptAlice.issuanceID(), amt, currentVersion);
3873+
3874+
Buffer const proof = mptAlice.getConvertBackProof(
3875+
bob,
3876+
amt,
3877+
badContextHash,
3878+
{
3879+
.pedersenCommitment = pedersenCommitment,
3880+
.amt = *spendingBalance,
3881+
.encryptedAmt = *encryptedSpendingBalance,
3882+
.blindingFactor = pcBlindingFactor,
3883+
});
3884+
3885+
mptAlice.convertBack(
3886+
{.account = bob,
3887+
.amt = amt,
3888+
.proof = proof,
3889+
.holderEncryptedAmt = bobCiphertext,
3890+
.issuerEncryptedAmt = issuerCiphertext,
3891+
.blindingFactor = blindingFactor,
3892+
.pedersenCommitment = pedersenCommitment,
3893+
.err = tecBAD_PROOF});
3894+
}
3895+
3896+
// Verify Correct Proof Passes
3897+
// Ensure the test setup was correct and functions when no replay is attempted.
3898+
{
3899+
// Fetch updated sequence once more
3900+
uint32_t const seqC = env.seq(bob);
3901+
uint256 const goodContextHash =
3902+
getConvertBackContextHash(bob, seqC, mptAlice.issuanceID(), amt, currentVersion);
3903+
3904+
Buffer const proof = mptAlice.getConvertBackProof(
3905+
bob,
3906+
amt,
3907+
goodContextHash,
3908+
{
3909+
.pedersenCommitment = pedersenCommitment,
3910+
.amt = *spendingBalance,
3911+
.encryptedAmt = *encryptedSpendingBalance,
3912+
.blindingFactor = pcBlindingFactor,
3913+
});
3914+
3915+
mptAlice.convertBack({
3916+
.account = bob,
3917+
.amt = amt,
3918+
.proof = proof,
3919+
.holderEncryptedAmt = bobCiphertext,
3920+
.issuerEncryptedAmt = issuerCiphertext,
3921+
.blindingFactor = blindingFactor,
3922+
.pedersenCommitment = pedersenCommitment,
3923+
});
3924+
}
3925+
}
3926+
3927+
// This test simulates a valid proof π extracted from a transaction
3928+
// for amount m1 is reused in a new transaction for a different
3929+
// amount m2 with different ciphertexts. It confirms the context hash
3930+
// recomputation fails due to the ciphertext binding mismatch, resulting
3931+
// in tecBAD_PROOF.
3932+
void
3933+
testProofCiphertextBinding(FeatureBitset features)
3934+
{
3935+
testcase("Proof ciphertext binding");
3936+
using namespace test::jtx;
3937+
3938+
Env env{*this, features};
3939+
Account const alice("alice");
3940+
Account const bob("bob");
3941+
MPTTester mptAlice(env, alice, {.holders = {bob}});
3942+
3943+
mptAlice.create(
3944+
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanPrivacy});
3945+
3946+
mptAlice.authorize({.account = bob});
3947+
mptAlice.pay(alice, bob, 100);
3948+
3949+
mptAlice.generateKeyPair(alice);
3950+
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
3951+
3952+
mptAlice.generateKeyPair(bob);
3953+
mptAlice.convert({
3954+
.account = bob,
3955+
.amt = 50,
3956+
.holderPubKey = mptAlice.getPubKey(bob),
3957+
});
3958+
3959+
mptAlice.mergeInbox({
3960+
.account = bob,
3961+
});
3962+
3963+
auto const spendingBalance = mptAlice.getDecryptedBalance(bob, MPTTester::HOLDER_ENCRYPTED_SPENDING);
3964+
auto const encryptedSpendingBalance = mptAlice.getEncryptedBalance(bob, MPTTester::HOLDER_ENCRYPTED_SPENDING);
3965+
auto const version = mptAlice.getMPTokenVersion(bob);
3966+
Buffer const pcBlindingFactor = generateBlindingFactor();
3967+
Buffer const pedersenCommitment = mptAlice.getPedersenCommitment(*spendingBalance, pcBlindingFactor);
3968+
3969+
// Generate a valid proof pi for Amount m1 = 10
3970+
uint64_t const amtA = 10;
3971+
uint32_t const currentSeq = env.seq(bob);
3972+
uint256 const contextHashA = getConvertBackContextHash(bob, currentSeq, mptAlice.issuanceID(), amtA, version);
3973+
3974+
Buffer const proofA = mptAlice.getConvertBackProof(
3975+
bob,
3976+
amtA,
3977+
contextHashA,
3978+
{
3979+
.pedersenCommitment = pedersenCommitment,
3980+
.amt = *spendingBalance,
3981+
.encryptedAmt = *encryptedSpendingBalance,
3982+
.blindingFactor = pcBlindingFactor,
3983+
});
3984+
3985+
// Construct Transaction B with Amount m2 = 20 and attach Proof pi
3986+
uint64_t const amtB = 20;
3987+
Buffer const blindingFactorB = generateBlindingFactor();
3988+
Buffer const bobCiphertextB = mptAlice.encryptAmount(bob, amtB, blindingFactorB);
3989+
Buffer const issuerCiphertextB = mptAlice.encryptAmount(alice, amtB, blindingFactorB);
3990+
3991+
// We attempt to verify the proof pi (for amt 10) against the new ciphertexts (for amt 20).
3992+
mptAlice.convertBack(
3993+
{.account = bob,
3994+
.amt = amtB,
3995+
.proof = proofA, // Extracted/Reused proof from Transaction A
3996+
.holderEncryptedAmt = bobCiphertextB,
3997+
.issuerEncryptedAmt = issuerCiphertextB,
3998+
.blindingFactor = blindingFactorB,
3999+
.pedersenCommitment = pedersenCommitment,
4000+
.err = tecBAD_PROOF}); // Expected failure
4001+
}
4002+
4003+
// This test simulates a valid proof π and ciphertext are
4004+
// tied to version v, but are reused after an inbox merge has incremented
4005+
// the CBS version to v+1. It confirms the validator rejects the transaction
4006+
// before acceptance due to the ContextID mismatch.
4007+
void
4008+
testProofVersionMismatch(FeatureBitset features)
4009+
{
4010+
testcase("Proof version mismatch");
4011+
using namespace test::jtx;
4012+
4013+
Env env{*this, features};
4014+
Account const alice("alice");
4015+
Account const bob("bob");
4016+
MPTTester mptAlice(env, alice, {.holders = {bob}});
4017+
4018+
mptAlice.create(
4019+
{.ownerCount = 1, .holderCount = 0, .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanPrivacy});
4020+
4021+
mptAlice.authorize({.account = bob});
4022+
mptAlice.pay(alice, bob, 1000);
4023+
4024+
mptAlice.generateKeyPair(alice);
4025+
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
4026+
4027+
mptAlice.generateKeyPair(bob);
4028+
4029+
// Initial state: Version v
4030+
// Convert and merge to establish a spending balance and initial version
4031+
mptAlice.convert({
4032+
.account = bob,
4033+
.amt = 100,
4034+
.holderPubKey = mptAlice.getPubKey(bob),
4035+
});
4036+
mptAlice.mergeInbox({.account = bob});
4037+
4038+
auto const versionV = mptAlice.getMPTokenVersion(bob);
4039+
auto const spendingBalanceV = mptAlice.getDecryptedBalance(bob, MPTTester::HOLDER_ENCRYPTED_SPENDING);
4040+
auto const encryptedSpendingBalanceV = mptAlice.getEncryptedBalance(bob, MPTTester::HOLDER_ENCRYPTED_SPENDING);
4041+
4042+
// Parameters for the intended ConvertBack transaction
4043+
uint64_t const amt = 10;
4044+
Buffer const blindingFactor = generateBlindingFactor();
4045+
Buffer const pcBlindingFactor = generateBlindingFactor();
4046+
Buffer const pedersenCommitment = mptAlice.getPedersenCommitment(*spendingBalanceV, pcBlindingFactor);
4047+
Buffer const issuerCiphertext = mptAlice.encryptAmount(alice, amt, blindingFactor);
4048+
Buffer const bobCiphertext = mptAlice.encryptAmount(bob, amt, blindingFactor);
4049+
4050+
// State Change: Increment version to v+1
4051+
// Converting more funds and merging increments the sfConfidentialBalanceVersion
4052+
mptAlice.convert({.account = bob, .amt = 50});
4053+
mptAlice.mergeInbox({.account = bob});
4054+
4055+
BEAST_EXPECT(mptAlice.getMPTokenVersion(bob) > versionV);
4056+
4057+
// Attack: Attempt to reuse proof tied to Version v at ledger Version v+1
4058+
uint32_t const currentSeq = env.seq(bob);
4059+
// Proof is explicitly generated using the outdated Version v
4060+
uint256 const oldContextHash = getConvertBackContextHash(bob, currentSeq, mptAlice.issuanceID(), amt, versionV);
4061+
4062+
Buffer const oldProof = mptAlice.getConvertBackProof(
4063+
bob,
4064+
amt,
4065+
oldContextHash,
4066+
{
4067+
.pedersenCommitment = pedersenCommitment,
4068+
.amt = *spendingBalanceV,
4069+
.encryptedAmt = *encryptedSpendingBalanceV,
4070+
.blindingFactor = pcBlindingFactor,
4071+
});
4072+
4073+
// Submit and verify failure
4074+
mptAlice.convertBack(
4075+
{.account = bob,
4076+
.amt = amt,
4077+
.proof = oldProof,
4078+
.holderEncryptedAmt = bobCiphertext,
4079+
.issuerEncryptedAmt = issuerCiphertext,
4080+
.blindingFactor = blindingFactor,
4081+
.pedersenCommitment = pedersenCommitment,
4082+
.err = tecBAD_PROOF}); // Fails because TransactionContextID differs
4083+
}
4084+
37814085
void
37824086
testWithFeats(FeatureBitset features)
37834087
{
@@ -3820,6 +4124,9 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
38204124
testConvertBackBulletproof(features);
38214125

38224126
testMutatePrivacy(features);
4127+
testProofContextBinding(features);
4128+
testProofCiphertextBinding(features);
4129+
testProofVersionMismatch(features);
38234130
}
38244131

38254132
public:

0 commit comments

Comments
 (0)