mirror of
https://github.com/XRPLF/rippled.git
synced 2026-04-29 15:37:57 +00:00
ConfidentialConvert with Equality Proof (#6177)
This commit is contained in:
@@ -34,6 +34,13 @@ getClawbackContextHash(
|
||||
std::uint64_t amount,
|
||||
AccountID const& holder);
|
||||
|
||||
uint256
|
||||
getConvertContextHash(
|
||||
AccountID const& account,
|
||||
std::uint32_t sequence,
|
||||
uint192 const& issuanceID,
|
||||
std::uint64_t amount);
|
||||
|
||||
/**
|
||||
* @brief Generates a new secp256k1 key pair.
|
||||
*/
|
||||
@@ -228,7 +235,8 @@ proveEquality(
|
||||
uint256 const& txHash, // Transaction context data
|
||||
std::uint32_t const spendVersion);
|
||||
|
||||
Buffer
|
||||
// returns ciphertext and the blinding factor used
|
||||
std::pair<Buffer, Buffer>
|
||||
encryptAmount(uint64_t amt, Slice const& pubKeySlice);
|
||||
|
||||
Buffer
|
||||
@@ -258,6 +266,17 @@ verifyEqualityProof(
|
||||
Slice const& ciphertext,
|
||||
uint256 const& contextHash);
|
||||
|
||||
TER
|
||||
verifyClawbackEqualityProof(
|
||||
uint64_t const amount,
|
||||
Slice const& proof,
|
||||
Slice const& pubKeySlice,
|
||||
Slice const& ciphertext,
|
||||
uint256 const& contextHash);
|
||||
|
||||
std::vector<Buffer>
|
||||
getEqualityProofs(Slice const& zkp);
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
|
||||
@@ -399,6 +399,7 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({
|
||||
{sfDomainID, soeOPTIONAL},
|
||||
{sfMutableFlags, soeDEFAULT},
|
||||
{sfIssuerElGamalPublicKey, soeOPTIONAL},
|
||||
{sfAuditorElGamalPublicKey, soeOPTIONAL},
|
||||
{sfConfidentialOutstandingAmount, soeDEFAULT},
|
||||
}))
|
||||
|
||||
|
||||
@@ -309,6 +309,9 @@ TYPED_SFIELD(sfHolderEncryptedAmount, VL, 38)
|
||||
TYPED_SFIELD(sfIssuerEncryptedAmount, VL, 39)
|
||||
TYPED_SFIELD(sfSenderEncryptedAmount, VL, 40)
|
||||
TYPED_SFIELD(sfDestinationEncryptedAmount, VL, 41)
|
||||
TYPED_SFIELD(sfAuditorEncryptedBalance, VL, 42)
|
||||
TYPED_SFIELD(sfAuditorEncryptedAmount, VL, 43)
|
||||
TYPED_SFIELD(sfAuditorElGamalPublicKey, VL, 44)
|
||||
|
||||
// account (common)
|
||||
TYPED_SFIELD(sfAccount, ACCOUNT, 1)
|
||||
|
||||
@@ -39,6 +39,20 @@ getClawbackContextHash(
|
||||
return s.getSHA512Half();
|
||||
}
|
||||
|
||||
uint256
|
||||
getConvertContextHash(
|
||||
AccountID const& account,
|
||||
std::uint32_t sequence,
|
||||
uint192 const& issuanceID,
|
||||
std::uint64_t amount)
|
||||
{
|
||||
Serializer s;
|
||||
addCommonZKPFields(
|
||||
s, ttCONFIDENTIAL_CONVERT, account, sequence, issuanceID, amount);
|
||||
|
||||
return s.getSHA512Half();
|
||||
}
|
||||
|
||||
int
|
||||
secp256k1_elgamal_generate_keypair(
|
||||
secp256k1_context const* ctx,
|
||||
@@ -842,7 +856,7 @@ proveEquality(
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
Buffer
|
||||
std::pair<Buffer, Buffer>
|
||||
encryptAmount(uint64_t amt, Slice const& pubKeySlice)
|
||||
{
|
||||
Buffer buf(ecGamalEncryptedTotalLength);
|
||||
@@ -870,7 +884,7 @@ encryptAmount(uint64_t amt, Slice const& pubKeySlice)
|
||||
Throw<std::runtime_error>(
|
||||
"Failed to serialize into 66 byte compressed format");
|
||||
|
||||
return buf;
|
||||
return std::make_pair(buf, Buffer(blindingFactor, 32));
|
||||
}
|
||||
|
||||
Buffer
|
||||
@@ -981,6 +995,36 @@ verifyEqualityProof(
|
||||
secp256k1_pubkey pubKey;
|
||||
std::memcpy(pubKey.data, pubKeySlice.data(), ecPubKeyLength);
|
||||
|
||||
if (secp256k1_equality_plaintext_verify(
|
||||
secp256k1Context(),
|
||||
proof.data(),
|
||||
&c1,
|
||||
&c2,
|
||||
&pubKey,
|
||||
amount,
|
||||
contextHash.data()) != 1)
|
||||
{
|
||||
return tecBAD_PROOF;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
verifyClawbackEqualityProof(
|
||||
uint64_t const amount,
|
||||
Slice const& proof,
|
||||
Slice const& pubKeySlice,
|
||||
Slice const& ciphertext,
|
||||
uint256 const& contextHash)
|
||||
{
|
||||
secp256k1_pubkey c1, c2;
|
||||
if (!makeEcPair(ciphertext, c1, c2))
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
secp256k1_pubkey pubKey;
|
||||
std::memcpy(pubKey.data, pubKeySlice.data(), ecPubKeyLength);
|
||||
|
||||
if (secp256k1_equality_plaintext_verify(
|
||||
secp256k1Context(),
|
||||
proof.data(),
|
||||
@@ -995,4 +1039,22 @@ verifyEqualityProof(
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
std::vector<Buffer>
|
||||
getEqualityProofs(Slice const& zkp)
|
||||
{
|
||||
if (zkp.size() % ecEqualityProofLength != 0)
|
||||
return {};
|
||||
auto const count = zkp.size() / ecEqualityProofLength;
|
||||
|
||||
std::vector<Buffer> zkps;
|
||||
zkps.reserve(count);
|
||||
|
||||
for (size_t i = 0; i < count; ++i)
|
||||
zkps.emplace_back(
|
||||
zkp.data() + (i * ecEqualityProofLength), ecEqualityProofLength);
|
||||
|
||||
return zkps;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -47,26 +47,22 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert({
|
||||
.account = bob,
|
||||
.amt = 0,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
});
|
||||
|
||||
mptAlice.convert({
|
||||
.account = bob,
|
||||
.amt = 20,
|
||||
.proof = "123",
|
||||
});
|
||||
|
||||
mptAlice.convert({
|
||||
.account = bob,
|
||||
.amt = 40,
|
||||
.proof = "123",
|
||||
});
|
||||
|
||||
mptAlice.convert({
|
||||
.account = bob,
|
||||
.amt = 40,
|
||||
.proof = "123",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -76,104 +72,113 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
testcase("Convert preflight");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, features - featureConfidentialTransfer};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
{
|
||||
Env env{*this, features - featureConfidentialTransfer};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
|
||||
mptAlice.create(
|
||||
{.ownerCount = 1,
|
||||
.holderCount = 0,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanLock});
|
||||
mptAlice.create(
|
||||
{.ownerCount = 1,
|
||||
.holderCount = 0,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanLock});
|
||||
|
||||
mptAlice.authorize({.account = bob});
|
||||
env.close();
|
||||
mptAlice.pay(alice, bob, 100);
|
||||
env.close();
|
||||
mptAlice.authorize({.account = bob});
|
||||
env.close();
|
||||
mptAlice.pay(alice, bob, 100);
|
||||
env.close();
|
||||
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.generateKeyPair(bob);
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.generateKeyPair(bob);
|
||||
|
||||
mptAlice.set(
|
||||
{.account = alice,
|
||||
.pubKey = mptAlice.getPubKey(alice),
|
||||
.err = temDISABLED});
|
||||
mptAlice.set(
|
||||
{.account = alice,
|
||||
.pubKey = mptAlice.getPubKey(alice),
|
||||
.err = temDISABLED});
|
||||
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 10,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.err = temDISABLED});
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 10,
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.err = temDISABLED});
|
||||
}
|
||||
|
||||
env.close();
|
||||
{
|
||||
Env env{*this, features};
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
MPTTester mptAlice(env, alice, {.holders = {bob}});
|
||||
|
||||
env.enableFeature(featureConfidentialTransfer);
|
||||
env.close();
|
||||
mptAlice.create(
|
||||
{.ownerCount = 1,
|
||||
.holderCount = 0,
|
||||
.flags = tfMPTCanTransfer | tfMPTCanLock});
|
||||
|
||||
mptAlice.convert(
|
||||
{.account = alice,
|
||||
.amt = 10,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.err = temMALFORMED});
|
||||
mptAlice.authorize({.account = bob});
|
||||
env.close();
|
||||
mptAlice.pay(alice, bob, 100);
|
||||
env.close();
|
||||
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 10,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.holderEncryptedAmt = Buffer{},
|
||||
.err = temBAD_CIPHERTEXT});
|
||||
mptAlice.generateKeyPair(alice);
|
||||
mptAlice.generateKeyPair(bob);
|
||||
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 10,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.issuerEncryptedAmt = Buffer{},
|
||||
.err = temBAD_CIPHERTEXT});
|
||||
mptAlice.convert(
|
||||
{.account = alice,
|
||||
.amt = 10,
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.err = temMALFORMED});
|
||||
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = maxMPTokenAmount + 1,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.err = temBAD_AMOUNT});
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 10,
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.holderEncryptedAmt = Buffer{},
|
||||
.err = temBAD_CIPHERTEXT});
|
||||
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 1,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.holderEncryptedAmt =
|
||||
Buffer{badCiphertext, ecGamalEncryptedTotalLength},
|
||||
.err = temBAD_CIPHERTEXT});
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 10,
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.issuerEncryptedAmt = Buffer{},
|
||||
.err = temBAD_CIPHERTEXT});
|
||||
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 1,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.issuerEncryptedAmt =
|
||||
Buffer{badCiphertext, ecGamalEncryptedTotalLength},
|
||||
.err = temBAD_CIPHERTEXT});
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = maxMPTokenAmount + 1,
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.err = temBAD_AMOUNT});
|
||||
|
||||
// invalid pub key
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 10,
|
||||
.proof = "123",
|
||||
.holderPubKey = Buffer{},
|
||||
.err = temMALFORMED});
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 1,
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.holderEncryptedAmt =
|
||||
Buffer{badCiphertext, ecGamalEncryptedTotalLength},
|
||||
.err = temBAD_CIPHERTEXT});
|
||||
|
||||
// todo: change to to check proof size
|
||||
// mptAlice.convert(
|
||||
// {.account = bob,
|
||||
// .amt = 10,
|
||||
// .proof = "123",
|
||||
// .holderPubKey = mptAlice.getPubKey(bob),
|
||||
// .err = temMALFORMED});
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 1,
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.issuerEncryptedAmt =
|
||||
Buffer{badCiphertext, ecGamalEncryptedTotalLength},
|
||||
.err = temBAD_CIPHERTEXT});
|
||||
|
||||
// invalid pub key
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 10,
|
||||
.holderPubKey = Buffer{},
|
||||
.err = temMALFORMED});
|
||||
|
||||
// todo: change to to check proof size
|
||||
// mptAlice.convert(
|
||||
// {.account = bob,
|
||||
// .amt = 10,
|
||||
// .proof = "123",
|
||||
// .holderPubKey = mptAlice.getPubKey(bob),
|
||||
// .err = temMALFORMED});
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -288,7 +293,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 10,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.err = tecNO_PERMISSION});
|
||||
}
|
||||
@@ -316,7 +320,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 10,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.err = tecNO_PERMISSION});
|
||||
}
|
||||
@@ -345,7 +348,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 10,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.err = tecOBJECT_NOT_FOUND});
|
||||
}
|
||||
@@ -371,7 +373,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 10,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.err = tecOBJECT_NOT_FOUND});
|
||||
}
|
||||
@@ -403,7 +404,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 200,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.err = tecINSUFFICIENT_FUNDS});
|
||||
}
|
||||
@@ -435,14 +435,12 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 10,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob)});
|
||||
|
||||
// cannot upload pk again
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 10,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.err = tecDUPLICATE});
|
||||
}
|
||||
@@ -476,7 +474,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 10,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.err = tecINSUFFICIENT_FUNDS});
|
||||
|
||||
@@ -486,7 +483,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert({
|
||||
.account = bob,
|
||||
.amt = 10,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
});
|
||||
}
|
||||
@@ -524,7 +520,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 10,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.err = tecINSUFFICIENT_FUNDS});
|
||||
|
||||
@@ -537,7 +532,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert({
|
||||
.account = bob,
|
||||
.amt = 10,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
});
|
||||
}
|
||||
@@ -574,7 +568,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert({
|
||||
.account = bob,
|
||||
.amt = 40,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
});
|
||||
|
||||
@@ -612,7 +605,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert({
|
||||
.account = bob,
|
||||
.amt = 40,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
});
|
||||
|
||||
@@ -757,7 +749,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 60,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.err = tesSUCCESS});
|
||||
|
||||
@@ -780,7 +771,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert(
|
||||
{.account = carol,
|
||||
.amt = 20,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(carol),
|
||||
.err = tesSUCCESS});
|
||||
|
||||
@@ -1032,13 +1022,11 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 60,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.err = tesSUCCESS});
|
||||
mptAlice.convert(
|
||||
{.account = carol,
|
||||
.amt = 20,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(carol),
|
||||
.err = tesSUCCESS});
|
||||
|
||||
@@ -1252,7 +1240,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 60,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.err = tesSUCCESS});
|
||||
|
||||
@@ -1264,7 +1251,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert(
|
||||
{.account = carol,
|
||||
.amt = 20,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(carol),
|
||||
.err = tesSUCCESS});
|
||||
|
||||
@@ -1316,7 +1302,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert({
|
||||
.account = bob,
|
||||
.amt = 100,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
});
|
||||
|
||||
@@ -1356,14 +1341,12 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert({
|
||||
.account = bob,
|
||||
.amt = 100,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
});
|
||||
|
||||
mptAlice.convert({
|
||||
.account = carol,
|
||||
.amt = 0,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(carol),
|
||||
});
|
||||
|
||||
@@ -1400,7 +1383,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert({
|
||||
.account = bob,
|
||||
.amt = 0,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
});
|
||||
|
||||
@@ -1437,7 +1419,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert({
|
||||
.account = bob,
|
||||
.amt = 0,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
});
|
||||
|
||||
@@ -1480,7 +1461,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert({
|
||||
.account = bob,
|
||||
.amt = 40,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
});
|
||||
|
||||
@@ -1559,7 +1539,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert({
|
||||
.account = bob,
|
||||
.amt = 40,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
});
|
||||
|
||||
@@ -1765,7 +1744,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert({
|
||||
.account = bob,
|
||||
.amt = 40,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
});
|
||||
|
||||
@@ -1776,7 +1754,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert({
|
||||
.account = carol,
|
||||
.amt = 40,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(carol),
|
||||
});
|
||||
|
||||
@@ -1816,7 +1793,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert({
|
||||
.account = bob,
|
||||
.amt = 40,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
});
|
||||
mptAlice.mergeInbox({.account = bob});
|
||||
@@ -1892,13 +1868,11 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert(
|
||||
{.account = carol,
|
||||
.amt = 50,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(carol),
|
||||
.err = tesSUCCESS});
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 50,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.err = tesSUCCESS});
|
||||
|
||||
@@ -2021,7 +1995,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 60,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob)});
|
||||
|
||||
// bob merge inbox
|
||||
@@ -2037,7 +2010,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert(
|
||||
{.account = carol,
|
||||
.amt = 120,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(carol)});
|
||||
|
||||
// carol merge inbox
|
||||
@@ -2052,7 +2024,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert(
|
||||
{.account = dave,
|
||||
.amt = 200,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(dave)});
|
||||
|
||||
// setup: carol confidential send 50 to bob.
|
||||
@@ -2215,7 +2186,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert({
|
||||
.account = bob,
|
||||
.amt = 60,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
});
|
||||
mptAlice.mergeInbox({
|
||||
@@ -2338,7 +2308,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 60,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob)});
|
||||
mptAlice.mergeInbox({
|
||||
.account = bob,
|
||||
@@ -2465,7 +2434,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 500,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob)});
|
||||
mptAlice.mergeInbox({
|
||||
.account = bob,
|
||||
@@ -2474,7 +2442,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert(
|
||||
{.account = carol,
|
||||
.amt = 1000,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(carol)});
|
||||
|
||||
// verify proof fails with invalid clawback amount
|
||||
@@ -2506,7 +2473,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 300,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob)});
|
||||
mptAlice.mergeInbox({
|
||||
.account = bob,
|
||||
@@ -2514,7 +2480,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert(
|
||||
{.account = carol,
|
||||
.amt = 400,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(carol)});
|
||||
mptAlice.mergeInbox({
|
||||
.account = carol,
|
||||
@@ -2625,14 +2590,12 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = amt,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.err = expectedResult});
|
||||
else
|
||||
mptAlice.convert({
|
||||
.account = bob,
|
||||
.amt = amt,
|
||||
.proof = "123",
|
||||
.err = expectedResult,
|
||||
});
|
||||
|
||||
@@ -2697,7 +2660,6 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 50,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob)});
|
||||
|
||||
// set or clear lsfMPTCanPrivacy should fail because of
|
||||
@@ -2729,14 +2691,16 @@ class ConfidentialTransfer_test : public beast::unit_test::suite
|
||||
mptAlice.convert(
|
||||
{.account = bob,
|
||||
.amt = 10,
|
||||
.proof = "123",
|
||||
.holderPubKey = mptAlice.getPubKey(bob),
|
||||
.err = tecNO_PERMISSION});
|
||||
|
||||
// can set lsfMPTCanPrivacy again when there's no confidential
|
||||
// outstanding balance
|
||||
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetPrivacy});
|
||||
mptAlice.convert({.account = bob, .amt = 10, .proof = "123"});
|
||||
mptAlice.convert({
|
||||
.account = bob,
|
||||
.amt = 10,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -775,6 +775,98 @@ MPTTester::getClawbackProof(
|
||||
return proof;
|
||||
}
|
||||
|
||||
Buffer
|
||||
MPTTester::getConvertProof(
|
||||
Account const& holder,
|
||||
std::uint64_t amount,
|
||||
uint256 const& ctxHash,
|
||||
std::pair<Buffer, Buffer> holderCiphertext,
|
||||
std::pair<Buffer, Buffer> issuerCiphertext,
|
||||
std::optional<std::pair<Buffer, Buffer>> auditorCiphertext) const
|
||||
{
|
||||
if (!id_)
|
||||
Throw<std::runtime_error>("MPT has not been created");
|
||||
|
||||
auto const sleHolder = env_.le(keylet::mptoken(*id_, holder.id()));
|
||||
auto const sleIssuance = env_.le(keylet::mptIssuance(*id_));
|
||||
|
||||
size_t const zkpSize = auditorCiphertext ? 3 : 2;
|
||||
size_t const zkpByteLength = zkpSize * ecEqualityProofLength;
|
||||
|
||||
if (!sleHolder || !sleIssuance || holderCiphertext.first.size() == 0 ||
|
||||
issuerCiphertext.first.size() == 0)
|
||||
return Buffer(zkpByteLength);
|
||||
|
||||
auto const generateProof = [amount, ctxHash](
|
||||
Slice const& ciphertext,
|
||||
Slice const& pubKey,
|
||||
Slice const& randomness) {
|
||||
secp256k1_pubkey c1, c2;
|
||||
auto const ctx = secp256k1Context();
|
||||
if (!secp256k1_ec_pubkey_parse(
|
||||
ctx, &c1, ciphertext.data(), ecGamalEncryptedLength) ||
|
||||
!secp256k1_ec_pubkey_parse(
|
||||
ctx,
|
||||
&c2,
|
||||
ciphertext.data() + ecGamalEncryptedLength,
|
||||
ecGamalEncryptedLength))
|
||||
{
|
||||
Throw<std::runtime_error>("Invalid Ciphertext");
|
||||
}
|
||||
|
||||
secp256k1_pubkey pk;
|
||||
std::memcpy(pk.data, pubKey.data(), ecPubKeyLength);
|
||||
Buffer proof(ecEqualityProofLength);
|
||||
|
||||
if (secp256k1_equality_plaintext_prove(
|
||||
ctx,
|
||||
proof.data(),
|
||||
&c1,
|
||||
&c2,
|
||||
&pk,
|
||||
amount,
|
||||
randomness.data(),
|
||||
ctxHash.data()) != 1)
|
||||
{
|
||||
Throw<std::runtime_error>("Proof generation failed");
|
||||
}
|
||||
return proof;
|
||||
};
|
||||
|
||||
Buffer zkp(zkpByteLength);
|
||||
|
||||
Buffer holderZkp = generateProof(
|
||||
holderCiphertext.first, getPubKey(holder), holderCiphertext.second);
|
||||
|
||||
Buffer issuerZkp = generateProof(
|
||||
issuerCiphertext.first, getPubKey(issuer_), issuerCiphertext.second);
|
||||
|
||||
// std::optional<Slice> auditorZkp;
|
||||
// if (auditor)
|
||||
// {
|
||||
// Slice auditorPubKey(
|
||||
// sleIssuance->getFieldVL(sfAuditorElGamalPublicKey).data(),
|
||||
// sleIssuance->getFieldVL(sfAuditorElGamalPublicKey).size());
|
||||
// Buffer auditorZkp = txArgs.auditorEncryptedAmt &&
|
||||
// sleIssuance->isFieldPresent(sfIssuerElGamalPublicKey)
|
||||
// ? generateProof(*txArgs.issuerEncryptedAmt, issuerPubKey)
|
||||
// : getDummyProof();
|
||||
// }
|
||||
|
||||
// Pointer arithmetic to copy data into place
|
||||
std::uint8_t* ptr = zkp.data();
|
||||
|
||||
// Copy Holder
|
||||
std::memcpy(ptr, holderZkp.data(), holderZkp.size());
|
||||
ptr += holderZkp.size();
|
||||
|
||||
// Copy Issuer
|
||||
std::memcpy(ptr, issuerZkp.data(), issuerZkp.size());
|
||||
ptr += issuerZkp.size();
|
||||
|
||||
return zkp;
|
||||
}
|
||||
|
||||
std::optional<Buffer>
|
||||
MPTTester::getEncryptedBalance(
|
||||
Account const& account,
|
||||
@@ -855,20 +947,39 @@ MPTTester::convert(MPTConvert const& arg)
|
||||
if (arg.holderPubKey)
|
||||
jv[sfHolderElGamalPublicKey.jsonName] = strHex(*arg.holderPubKey);
|
||||
|
||||
std::pair<Buffer, Buffer> holderCiphertext;
|
||||
if (arg.holderEncryptedAmt)
|
||||
jv[sfHolderEncryptedAmount.jsonName] = strHex(*arg.holderEncryptedAmt);
|
||||
else
|
||||
jv[sfHolderEncryptedAmount.jsonName] =
|
||||
strHex(encryptAmount(*arg.account, *arg.amt));
|
||||
{
|
||||
holderCiphertext = encryptAmount(*arg.account, *arg.amt);
|
||||
jv[sfHolderEncryptedAmount.jsonName] = strHex(holderCiphertext.first);
|
||||
}
|
||||
|
||||
std::pair<Buffer, Buffer> issuerCiphertext;
|
||||
if (arg.issuerEncryptedAmt)
|
||||
jv[sfIssuerEncryptedAmount.jsonName] = strHex(*arg.issuerEncryptedAmt);
|
||||
else
|
||||
jv[sfIssuerEncryptedAmount.jsonName] =
|
||||
strHex(encryptAmount(issuer_, *arg.amt));
|
||||
{
|
||||
issuerCiphertext = encryptAmount(issuer_, *arg.amt);
|
||||
jv[sfIssuerEncryptedAmount.jsonName] = strHex(issuerCiphertext.first);
|
||||
}
|
||||
|
||||
if (arg.proof)
|
||||
jv[sfZKProof.jsonName] = *arg.proof;
|
||||
else
|
||||
{
|
||||
uint256 const ctxHash = getConvertContextHash(
|
||||
arg.account->id(), env_.seq(*arg.account), *id_, *arg.amt);
|
||||
Buffer proof = getConvertProof(
|
||||
*arg.account,
|
||||
*arg.amt,
|
||||
ctxHash,
|
||||
holderCiphertext,
|
||||
issuerCiphertext,
|
||||
{});
|
||||
jv[sfZKProof] = strHex(proof);
|
||||
}
|
||||
|
||||
auto const holderAmt = getBalance(*arg.account);
|
||||
auto const prevConfidentialOutstanding = getIssuanceConfidentialBalance();
|
||||
@@ -965,18 +1076,19 @@ MPTTester::send(MPTConfidentialSend const& arg)
|
||||
jv[sfSenderEncryptedAmount] = strHex(*arg.senderEncryptedAmt);
|
||||
else
|
||||
jv[sfSenderEncryptedAmount] =
|
||||
strHex(encryptAmount(*arg.account, *arg.amt));
|
||||
strHex(encryptAmount(*arg.account, *arg.amt).first);
|
||||
|
||||
if (arg.destEncryptedAmt)
|
||||
jv[sfDestinationEncryptedAmount] = strHex(*arg.destEncryptedAmt);
|
||||
else
|
||||
jv[sfDestinationEncryptedAmount] =
|
||||
strHex(encryptAmount(*arg.dest, *arg.amt));
|
||||
strHex(encryptAmount(*arg.dest, *arg.amt).first);
|
||||
|
||||
if (arg.issuerEncryptedAmt)
|
||||
jv[sfIssuerEncryptedAmount] = strHex(*arg.issuerEncryptedAmt);
|
||||
else
|
||||
jv[sfIssuerEncryptedAmount] = strHex(encryptAmount(issuer_, *arg.amt));
|
||||
jv[sfIssuerEncryptedAmount] =
|
||||
strHex(encryptAmount(issuer_, *arg.amt).first);
|
||||
|
||||
if (arg.proof)
|
||||
jv[sfZKProof] = *arg.proof;
|
||||
@@ -1179,7 +1291,7 @@ MPTTester::getPrivKey(Account const& account) const
|
||||
Throw<std::runtime_error>("Account does not have private key");
|
||||
}
|
||||
|
||||
Buffer
|
||||
std::pair<Buffer, Buffer>
|
||||
MPTTester::encryptAmount(Account const& account, uint64_t amt) const
|
||||
{
|
||||
return ripple::encryptAmount(amt, getPubKey(account));
|
||||
@@ -1308,13 +1420,13 @@ MPTTester::convertBack(MPTConvertBack const& arg)
|
||||
jv[sfHolderEncryptedAmount.jsonName] = strHex(*arg.holderEncryptedAmt);
|
||||
else
|
||||
jv[sfHolderEncryptedAmount.jsonName] =
|
||||
strHex(encryptAmount(*arg.account, *arg.amt));
|
||||
strHex(encryptAmount(*arg.account, *arg.amt).first);
|
||||
|
||||
if (arg.issuerEncryptedAmt)
|
||||
jv[sfIssuerEncryptedAmount.jsonName] = strHex(*arg.issuerEncryptedAmt);
|
||||
else
|
||||
jv[sfIssuerEncryptedAmount.jsonName] =
|
||||
strHex(encryptAmount(issuer_, *arg.amt));
|
||||
strHex(encryptAmount(issuer_, *arg.amt).first);
|
||||
|
||||
if (arg.proof)
|
||||
jv[sfZKProof.jsonName] = *arg.proof;
|
||||
|
||||
@@ -175,6 +175,7 @@ struct MPTConvert
|
||||
std::optional<Buffer> holderPubKey = std::nullopt;
|
||||
std::optional<Buffer> holderEncryptedAmt = std::nullopt;
|
||||
std::optional<Buffer> issuerEncryptedAmt = std::nullopt;
|
||||
std::optional<Buffer> auditorEncryptedAmt = std::nullopt;
|
||||
std::optional<std::uint32_t> ownerCount = std::nullopt;
|
||||
std::optional<std::uint32_t> holderCount = std::nullopt;
|
||||
std::optional<std::uint32_t> flags = std::nullopt;
|
||||
@@ -400,7 +401,7 @@ public:
|
||||
Buffer
|
||||
getPrivKey(Account const& account) const;
|
||||
|
||||
Buffer
|
||||
std::pair<Buffer, Buffer>
|
||||
encryptAmount(Account const& account, uint64_t amt) const;
|
||||
|
||||
uint64_t
|
||||
@@ -421,6 +422,15 @@ public:
|
||||
Buffer const& privateKey,
|
||||
uint256 const& txHash) const;
|
||||
|
||||
Buffer
|
||||
getConvertProof(
|
||||
Account const& holder,
|
||||
std::uint64_t amount,
|
||||
uint256 const& ctxHash,
|
||||
std::pair<Buffer, Buffer> holderCiphertext,
|
||||
std::pair<Buffer, Buffer> issuerCiphertext,
|
||||
std::optional<std::pair<Buffer, Buffer>> auditorCiphertext) const;
|
||||
|
||||
private:
|
||||
using SLEP = SLE::const_pointer;
|
||||
bool
|
||||
|
||||
@@ -89,7 +89,7 @@ ConfidentialClawback::preclaim(PreclaimContext const& ctx)
|
||||
|
||||
auto const contextHash = getClawbackContextHash(
|
||||
account, ctx.tx[sfSequence], mptIssuanceID, amount, holder);
|
||||
return verifyEqualityProof(
|
||||
return verifyClawbackEqualityProof(
|
||||
amount, ctx.tx[sfZKProof], pubKeySlice, ciphertext, contextHash);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,8 +37,10 @@ ConfidentialConvert::preflight(PreflightContext const& ctx)
|
||||
ctx.tx[sfHolderElGamalPublicKey].length() != ecPubKeyLength)
|
||||
return temMALFORMED;
|
||||
|
||||
// if (ctx.tx[sfZKProof].length() != ecEqualityProofLength)
|
||||
// return temMALFORMED;
|
||||
auto const expectedCount =
|
||||
ctx.tx.isFieldPresent(sfAuditorEncryptedAmount) ? 3 : 2;
|
||||
if (ctx.tx[sfZKProof].size() != expectedCount * ecEqualityProofLength)
|
||||
return temMALFORMED;
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
@@ -94,35 +96,56 @@ ConfidentialConvert::preclaim(PreclaimContext const& ctx)
|
||||
ctx.tx.isFieldPresent(sfHolderElGamalPublicKey))
|
||||
return tecDUPLICATE;
|
||||
|
||||
// auto const holderPubKey = ctx.tx.isFieldPresent(sfHolderElGamalPublicKey)
|
||||
// ? ctx.tx[sfHolderElGamalPublicKey]
|
||||
// : (*sleMptoken)[sfHolderElGamalPublicKey];
|
||||
auto const holderPubKey = ctx.tx.isFieldPresent(sfHolderElGamalPublicKey)
|
||||
? ctx.tx[sfHolderElGamalPublicKey]
|
||||
: (*sleMptoken)[sfHolderElGamalPublicKey];
|
||||
|
||||
// auto const contextHash = getContextHash(
|
||||
// ctx.tx[sfMPTokenIssuanceID],
|
||||
// ctx.tx[sfMPTAmount],
|
||||
// ctx.tx[sfAccount],
|
||||
// ctx.tx.getTxnType());
|
||||
auto const contextHash = getConvertContextHash(
|
||||
ctx.tx[sfAccount],
|
||||
ctx.tx[sfSequence],
|
||||
ctx.tx[sfMPTokenIssuanceID],
|
||||
ctx.tx[sfMPTAmount]);
|
||||
|
||||
// // check equality proof
|
||||
// auto checkEqualityProof = [&](auto const& encryptedAmount,
|
||||
// auto const& pubKey) -> TER {
|
||||
// return verifyEqualityProof(
|
||||
// ctx.tx[sfMPTAmount],
|
||||
// ctx.tx[sfZKProof],
|
||||
// pubKey,
|
||||
// encryptedAmount,
|
||||
// contextHash);
|
||||
// };
|
||||
bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount);
|
||||
|
||||
// if (!isTesSuccess(checkEqualityProof(
|
||||
// ctx.tx[sfHolderEncryptedAmount], holderPubKey)) ||
|
||||
// !isTesSuccess(checkEqualityProof(
|
||||
// ctx.tx[sfIssuerEncryptedAmount],
|
||||
// (*sleIssuance)[sfIssuerElGamalPublicKey])))
|
||||
// {
|
||||
// return tecBAD_PROOF;
|
||||
// }
|
||||
std::vector<Buffer> const zkps = getEqualityProofs(ctx.tx[sfZKProof]);
|
||||
|
||||
auto const& amount = ctx.tx[sfMPTAmount];
|
||||
|
||||
// we already checked proof size in preflight, still do sanity check here
|
||||
// since we are going to access individual vector entries
|
||||
auto const expectedCount = ctx.tx[sfZKProof].size() / ecEqualityProofLength;
|
||||
if (zkps.size() != expectedCount)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// check equality proof
|
||||
if (!isTesSuccess(verifyEqualityProof(
|
||||
amount,
|
||||
zkps[0],
|
||||
holderPubKey,
|
||||
ctx.tx[sfHolderEncryptedAmount],
|
||||
contextHash)) ||
|
||||
!isTesSuccess(verifyEqualityProof(
|
||||
amount,
|
||||
zkps[1],
|
||||
(*sleIssuance)[sfIssuerElGamalPublicKey],
|
||||
ctx.tx[sfIssuerEncryptedAmount],
|
||||
contextHash)))
|
||||
{
|
||||
return tecBAD_PROOF;
|
||||
}
|
||||
|
||||
// Verify Auditor proof if present
|
||||
if (hasAuditor &&
|
||||
!isTesSuccess(verifyEqualityProof(
|
||||
amount,
|
||||
zkps[2],
|
||||
(*sleIssuance)[sfAuditorElGamalPublicKey],
|
||||
ctx.tx[sfAuditorEncryptedAmount],
|
||||
contextHash)))
|
||||
{
|
||||
return tecBAD_PROOF;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
@@ -196,19 +219,20 @@ ConfidentialConvert::doApply()
|
||||
{
|
||||
// encrypt sfConfidentialBalanceSpending with zero balance
|
||||
Buffer out;
|
||||
out = encryptAmount(0, (*sleMptoken)[sfHolderElGamalPublicKey]);
|
||||
out =
|
||||
encryptAmount(0, (*sleMptoken)[sfHolderElGamalPublicKey]).first;
|
||||
(*sleMptoken)[sfConfidentialBalanceSpending] = out;
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
return tecINTERNAL;
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// both sfIssuerEncryptedBalance and sfConfidentialBalanceInbox should
|
||||
// exist together
|
||||
return tecINTERNAL;
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
view().update(sleIssuance);
|
||||
|
||||
Reference in New Issue
Block a user