#include #include #include #include #include namespace xrpl { void addCommonZKPFields( Serializer& s, std::uint16_t txType, AccountID const& account, uint192 const& issuanceID, std::uint32_t sequence) { // TxCommonHash = hash(TxType || Account || IssuanceID || SequenceOrTicket) s.add16(txType); s.addBitString(account); s.addBitString(issuanceID); s.add32(sequence); } uint256 getSendContextHash( AccountID const& account, uint192 const& issuanceID, std::uint32_t sequence, AccountID const& destination, std::uint32_t version) { Serializer s; addCommonZKPFields(s, ttCONFIDENTIAL_MPT_SEND, account, issuanceID, sequence); // TxSpecific = identity || freshness s.addBitString(destination); s.addInteger(version); return s.getSHA512Half(); } uint256 getClawbackContextHash( AccountID const& account, uint192 const& issuanceID, std::uint32_t sequence, AccountID const& holder) { Serializer s; addCommonZKPFields(s, ttCONFIDENTIAL_MPT_CLAWBACK, account, issuanceID, sequence); // TxSpecific = identity || freshness s.addBitString(holder); s.addInteger(0); return s.getSHA512Half(); } uint256 getConvertContextHash(AccountID const& account, uint192 const& issuanceID, std::uint32_t sequence) { Serializer s; addCommonZKPFields(s, ttCONFIDENTIAL_MPT_CONVERT, account, issuanceID, sequence); // TxSpecific = identity || freshness s.addBitString(account); s.addInteger(0); return s.getSHA512Half(); } uint256 getConvertBackContextHash( AccountID const& account, uint192 const& issuanceID, std::uint32_t sequence, std::uint32_t version) { Serializer s; addCommonZKPFields(s, ttCONFIDENTIAL_MPT_CONVERT_BACK, account, issuanceID, sequence); // TxSpecific = identity || freshness s.addBitString(account); s.addInteger(version); return s.getSHA512Half(); } bool makeEcPair(Slice const& buffer, secp256k1_pubkey& out1, secp256k1_pubkey& out2) { auto parsePubKey = [](Slice const& slice, secp256k1_pubkey& out) { return secp256k1_ec_pubkey_parse( secp256k1Context(), &out, reinterpret_cast(slice.data()), slice.length()); }; Slice s1{buffer.data(), ecGamalEncryptedLength}; Slice s2{buffer.data() + ecGamalEncryptedLength, ecGamalEncryptedLength}; int const ret1 = parsePubKey(s1, out1); int const ret2 = parsePubKey(s2, out2); return ret1 == 1 && ret2 == 1; } bool serializeEcPair(secp256k1_pubkey const& in1, secp256k1_pubkey const& in2, Buffer& buffer) { auto serializePubKey = [](secp256k1_pubkey const& pub, unsigned char* out) { size_t outLen = ecGamalEncryptedLength; // 33 bytes int const ret = secp256k1_ec_pubkey_serialize(secp256k1Context(), out, &outLen, &pub, SECP256K1_EC_COMPRESSED); return ret == 1 && outLen == ecGamalEncryptedLength; }; unsigned char* ptr = buffer.data(); bool const res1 = serializePubKey(in1, ptr); bool const res2 = serializePubKey(in2, ptr + ecGamalEncryptedLength); return res1 && res2; } bool isValidCiphertext(Slice const& buffer) { secp256k1_pubkey key1; secp256k1_pubkey key2; return makeEcPair(buffer, key1, key2); } bool isValidCompressedECPoint(Slice const& buffer) { if (buffer.size() != compressedECPointLength) return false; // Compressed EC points must start with 0x02 or 0x03 if (buffer[0] != 0x02 && buffer[0] != 0x03) return false; secp256k1_pubkey point; return secp256k1_ec_pubkey_parse(secp256k1Context(), &point, buffer.data(), buffer.size()) == 1; } TER homomorphicAdd(Slice const& a, Slice const& b, Buffer& out) { if (a.length() != ecGamalEncryptedTotalLength || b.length() != ecGamalEncryptedTotalLength) return tecINTERNAL; secp256k1_pubkey aC1; secp256k1_pubkey aC2; secp256k1_pubkey bC1; secp256k1_pubkey bC2; if (!makeEcPair(a, aC1, aC2) || !makeEcPair(b, bC1, bC2)) return tecINTERNAL; secp256k1_pubkey sumC1; secp256k1_pubkey sumC2; if (secp256k1_elgamal_add(secp256k1Context(), &sumC1, &sumC2, &aC1, &aC2, &bC1, &bC2) != 1) return tecINTERNAL; if (!serializeEcPair(sumC1, sumC2, out)) return tecINTERNAL; return tesSUCCESS; } TER homomorphicSubtract(Slice const& a, Slice const& b, Buffer& out) { if (a.length() != ecGamalEncryptedTotalLength || b.length() != ecGamalEncryptedTotalLength) return tecINTERNAL; secp256k1_pubkey aC1; secp256k1_pubkey aC2; secp256k1_pubkey bC1; secp256k1_pubkey bC2; if (!makeEcPair(a, aC1, aC2) || !makeEcPair(b, bC1, bC2)) return tecINTERNAL; secp256k1_pubkey diffC1; secp256k1_pubkey diffC2; if (secp256k1_elgamal_subtract(secp256k1Context(), &diffC1, &diffC2, &aC1, &aC2, &bC1, &bC2) != 1) return tecINTERNAL; if (!serializeEcPair(diffC1, diffC2, out)) return tecINTERNAL; return tesSUCCESS; } Buffer generateBlindingFactor() { unsigned char blindingFactor[ecBlindingFactorLength]; // todo: might need to be updated using another RNG if (RAND_bytes(blindingFactor, ecBlindingFactorLength) != 1) Throw("Failed to generate random number"); return Buffer(blindingFactor, ecBlindingFactorLength); } std::optional encryptAmount(uint64_t const amt, Slice const& pubKeySlice, Slice const& blindingFactor) { if (blindingFactor.size() != ecBlindingFactorLength) return std::nullopt; if (pubKeySlice.size() != ecPubKeyLength) return std::nullopt; secp256k1_pubkey c1, c2, pubKey; if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength) != 1) return std::nullopt; if (!secp256k1_elgamal_encrypt(secp256k1Context(), &c1, &c2, &pubKey, amt, blindingFactor.data())) return std::nullopt; Buffer buf(ecGamalEncryptedTotalLength); if (!serializeEcPair(c1, c2, buf)) return std::nullopt; return buf; } std::optional encryptCanonicalZeroAmount(Slice const& pubKeySlice, AccountID const& account, MPTID const& mptId) { if (pubKeySlice.size() != ecPubKeyLength) return std::nullopt; // LCOV_EXCL_LINE secp256k1_pubkey c1, c2, pubKey; if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength) != 1) return std::nullopt; // LCOV_EXCL_LINE if (!generate_canonical_encrypted_zero(secp256k1Context(), &c1, &c2, &pubKey, account.data(), mptId.data())) return std::nullopt; // LCOV_EXCL_LINE Buffer buf(ecGamalEncryptedTotalLength); if (!serializeEcPair(c1, c2, buf)) return std::nullopt; // LCOV_EXCL_LINE return buf; } TER verifySchnorrProof(Slice const& pubKeySlice, Slice const& proofSlice, uint256 const& contextHash) { if (proofSlice.size() != ecSchnorrProofLength) return tecINTERNAL; // LCOV_EXCL_LINE if (pubKeySlice.size() != ecPubKeyLength) return tecINTERNAL; // LCOV_EXCL_LINE secp256k1_pubkey pubKey; if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength) != 1) return tecINTERNAL; // LCOV_EXCL_LINE if (secp256k1_mpt_pok_sk_verify(secp256k1Context(), proofSlice.data(), &pubKey, contextHash.data()) != 1) return tecBAD_PROOF; return tesSUCCESS; } TER verifyElGamalEncryption( std::uint64_t const amount, Slice const& blindingFactor, Slice const& pubKeySlice, Slice const& ciphertext) { if (blindingFactor.size() != ecBlindingFactorLength) return tecINTERNAL; // LCOV_EXCL_LINE if (pubKeySlice.size() != ecPubKeyLength) return tecINTERNAL; // LCOV_EXCL_LINE secp256k1_pubkey pubKey; if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength) != 1) return tecINTERNAL; // LCOV_EXCL_LINE secp256k1_pubkey c1, c2; if (!makeEcPair(ciphertext, c1, c2)) return tecINTERNAL; // LCOV_EXCL_LINE if (secp256k1_elgamal_verify_encryption(secp256k1Context(), &c1, &c2, &pubKey, amount, blindingFactor.data()) != 1) return tecBAD_PROOF; return tesSUCCESS; } TER verifyRevealedAmount( std::uint64_t const amount, Slice const& blindingFactor, ConfidentialRecipient const& holder, ConfidentialRecipient const& issuer, std::optional const& auditor) { if (auto const res = verifyElGamalEncryption(amount, blindingFactor, holder.publicKey, holder.encryptedAmount); !isTesSuccess(res)) { return res; } if (auto const res = verifyElGamalEncryption(amount, blindingFactor, issuer.publicKey, issuer.encryptedAmount); !isTesSuccess(res)) { return res; } if (auditor) { if (auto const res = verifyElGamalEncryption(amount, blindingFactor, auditor->publicKey, auditor->encryptedAmount); !isTesSuccess(res)) { return res; } } return tesSUCCESS; } TER verifyMultiCiphertextEqualityProof( Slice const& proof, std::vector const& recipients, std::size_t const nRecipients, uint256 const& contextHash) { if (recipients.size() != nRecipients) return tecINTERNAL; // LCOV_EXCL_LINE if (proof.size() != secp256k1_mpt_proof_equality_shared_r_size(nRecipients)) return tecINTERNAL; // LCOV_EXCL_LINE auto const ctx = secp256k1Context(); secp256k1_pubkey c1; std::vector c2_vec(nRecipients); std::vector pk_vec(nRecipients); for (size_t i = 0; i < nRecipients; ++i) { auto const& recipient = recipients[i]; if (recipient.encryptedAmount.size() != ecGamalEncryptedTotalLength) return tecINTERNAL; // LCOV_EXCL_LINE if (recipient.publicKey.size() != ecPubKeyLength) return tecINTERNAL; // LCOV_EXCL_LINE // Parse Shared C1 from the first recipient only if (i == 0) { if (!secp256k1_ec_pubkey_parse(ctx, &c1, recipient.encryptedAmount.data(), ecGamalEncryptedLength)) return tecINTERNAL; // LCOV_EXCL_LINE } else { // All C1 bytes must be the same if (std::memcmp( recipient.encryptedAmount.data(), recipients[0].encryptedAmount.data(), ecGamalEncryptedLength) != 0) { return tecBAD_PROOF; } } if (secp256k1_ec_pubkey_parse( ctx, &c2_vec[i], recipient.encryptedAmount.data() + ecGamalEncryptedLength, ecGamalEncryptedLength) != 1) { return tecINTERNAL; // LCOV_EXCL_LINE } if (secp256k1_ec_pubkey_parse(ctx, &pk_vec[i], recipient.publicKey.data(), ecPubKeyLength) != 1) return tecINTERNAL; // LCOV_EXCL_LINE } int const result = secp256k1_mpt_verify_equality_shared_r( ctx, proof.data(), nRecipients, &c1, c2_vec.data(), pk_vec.data(), contextHash.data()); if (result != 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 if (pubKeySlice.size() != ecPubKeyLength) return tecINTERNAL; // LCOV_EXCL_LINE secp256k1_pubkey pubKey; if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength) != 1) return tecINTERNAL; // LCOV_EXCL_LINE // Note: c2, c1 order - the proof is generated with c2 first (the encrypted // message component) because the equality proof structure expects the // message-containing term before the blinding term. if (secp256k1_equality_plaintext_verify( secp256k1Context(), proof.data(), &pubKey, &c2, &c1, amount, contextHash.data()) != 1) { return tecBAD_PROOF; } return tesSUCCESS; } NotTEC checkEncryptedAmountFormat(STObject const& object) { // Current usage of this function is only for ConfidentialMPTConvert and // ConfidentialMPTConvertBack transactions, which already enforce that these fields // are present. if (!object.isFieldPresent(sfHolderEncryptedAmount) || !object.isFieldPresent(sfIssuerEncryptedAmount)) return temMALFORMED; // LCOV_EXCL_LINE if (object[sfHolderEncryptedAmount].length() != ecGamalEncryptedTotalLength || object[sfIssuerEncryptedAmount].length() != ecGamalEncryptedTotalLength) return temBAD_CIPHERTEXT; bool const hasAuditor = object.isFieldPresent(sfAuditorEncryptedAmount); if (hasAuditor && object[sfAuditorEncryptedAmount].length() != ecGamalEncryptedTotalLength) return temBAD_CIPHERTEXT; if (!isValidCiphertext(object[sfHolderEncryptedAmount]) || !isValidCiphertext(object[sfIssuerEncryptedAmount])) return temBAD_CIPHERTEXT; if (hasAuditor && !isValidCiphertext(object[sfAuditorEncryptedAmount])) return temBAD_CIPHERTEXT; return tesSUCCESS; } TER verifyAmountPcmLinkage( Slice const& proof, Slice const& encAmt, Slice const& pubKeySlice, Slice const& pcmSlice, uint256 const& contextHash) { if (proof.length() != ecPedersenProofLength) return tecINTERNAL; secp256k1_pubkey c1, c2; if (!makeEcPair(encAmt, c1, c2)) return tecINTERNAL; // LCOV_EXCL_LINE if (pubKeySlice.size() != ecPubKeyLength) return tecINTERNAL; // LCOV_EXCL_LINE if (pcmSlice.size() != ecPedersenCommitmentLength) return tecINTERNAL; // LCOV_EXCL_LINE secp256k1_pubkey pubKey; if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength) != 1) return tecINTERNAL; // LCOV_EXCL_LINE secp256k1_pubkey pcm; if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pcm, pcmSlice.data(), ecPedersenCommitmentLength) != 1) return tecINTERNAL; // LCOV_EXCL_LINE if (secp256k1_elgamal_pedersen_link_verify( secp256k1Context(), proof.data(), &c1, &c2, &pubKey, &pcm, contextHash.data()) != 1) { return tecBAD_PROOF; } return tesSUCCESS; } TER verifyBalancePcmLinkage( Slice const& proof, Slice const& encAmt, Slice const& pubKeySlice, Slice const& pcmSlice, uint256 const& contextHash) { if (proof.length() != ecPedersenProofLength) return tecINTERNAL; secp256k1_pubkey c1; secp256k1_pubkey c2; if (!makeEcPair(encAmt, c1, c2)) return tecINTERNAL; // LCOV_EXCL_LINE if (pubKeySlice.size() != ecPubKeyLength) return tecINTERNAL; // LCOV_EXCL_LINE if (pcmSlice.size() != ecPedersenCommitmentLength) return tecINTERNAL; // LCOV_EXCL_LINE secp256k1_pubkey pubKey; if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pubKey, pubKeySlice.data(), ecPubKeyLength) != 1) return tecINTERNAL; // LCOV_EXCL_LINE secp256k1_pubkey pcm; if (secp256k1_ec_pubkey_parse(secp256k1Context(), &pcm, pcmSlice.data(), ecPedersenCommitmentLength) != 1) return tecINTERNAL; // LCOV_EXCL_LINE // Note: c2, c1 order - the linkage proof expects the message-containing // component (c2 = m*G + r*Pk) before the blinding component (c1 = r*G). if (secp256k1_elgamal_pedersen_link_verify( secp256k1Context(), proof.data(), &pubKey, &c2, &c1, &pcm, contextHash.data()) != 1) { return tecBAD_PROOF; } return tesSUCCESS; } TER verifyAggregatedBulletproof( Slice const& proof, std::vector const& compressedCommitments, uint256 const& contextHash) { // 1. Validate Aggregation Factor (m), m to be a power of 2 std::size_t const m = compressedCommitments.size(); if (m == 0 || (m & (m - 1)) != 0) return tecINTERNAL; // LCOV_EXCL_LINE // 2. Prepare Pedersen Commitments, parse from compressed format auto const ctx = secp256k1Context(); std::vector commitments(m); for (size_t i = 0; i < m; ++i) { // Sanity check length if (compressedCommitments[i].size() != ecPedersenCommitmentLength) return tecINTERNAL; // LCOV_EXCL_LINE if (secp256k1_ec_pubkey_parse( ctx, &commitments[i], compressedCommitments[i].data(), ecPedersenCommitmentLength) != 1) return tecINTERNAL; // LCOV_EXCL_LINE } // 3. Prepare Generator Vectors (G_vec, H_vec) // The range proof requires vectors of size 64 * m std::size_t const n = 64 * m; std::vector G_vec(n); std::vector H_vec(n); // Retrieve deterministic generators "G" and "H" if (secp256k1_mpt_get_generator_vector(ctx, G_vec.data(), n, (unsigned char const*)"G", 1) != 1) { return tecINTERNAL; // LCOV_EXCL_LINE } if (secp256k1_mpt_get_generator_vector(ctx, H_vec.data(), n, (unsigned char const*)"H", 1) != 1) { return tecINTERNAL; // LCOV_EXCL_LINE } // 4. Prepare Base Generator (pk_base / H) secp256k1_pubkey pk_base; if (secp256k1_mpt_get_h_generator(ctx, &pk_base) != 1) { return tecINTERNAL; // LCOV_EXCL_LINE } // 5. Verify the Proof int const result = secp256k1_bulletproof_verify_agg( ctx, G_vec.data(), H_vec.data(), reinterpret_cast(proof.data()), proof.size(), commitments.data(), m, &pk_base, contextHash.data()); if (result != 1) return tecBAD_PROOF; return tesSUCCESS; } TER computeSendRemainder(Slice const& balanceCommitment, Slice const& amountCommitment, Buffer& out) { if (balanceCommitment.size() != ecPedersenCommitmentLength || amountCommitment.size() != ecPedersenCommitmentLength) return tecINTERNAL; auto const ctx = secp256k1Context(); secp256k1_pubkey pcBalance; if (secp256k1_ec_pubkey_parse(ctx, &pcBalance, balanceCommitment.data(), ecPedersenCommitmentLength) != 1) return tecINTERNAL; secp256k1_pubkey pcAmount; if (secp256k1_ec_pubkey_parse(ctx, &pcAmount, amountCommitment.data(), ecPedersenCommitmentLength) != 1) return tecINTERNAL; // Negate PC_amount point to get -PC_amount if (!secp256k1_ec_pubkey_negate(ctx, &pcAmount)) return tecINTERNAL; // Compute pcRem = pcBalance + (-pcAmount) secp256k1_pubkey const* summands[2] = {&pcBalance, &pcAmount}; secp256k1_pubkey pcRem; if (!secp256k1_ec_pubkey_combine(ctx, &pcRem, summands, 2)) return tecINTERNAL; // Serialize result to compressed format out.alloc(ecPedersenCommitmentLength); size_t outLen = ecPedersenCommitmentLength; if (secp256k1_ec_pubkey_serialize(ctx, out.data(), &outLen, &pcRem, SECP256K1_EC_COMPRESSED) != 1) return tecINTERNAL; return tesSUCCESS; } TER computeConvertBackRemainder(Slice const& commitment, std::uint64_t amount, Buffer& out) { if (commitment.size() != ecPedersenCommitmentLength || amount == 0) return tecINTERNAL; // LCOV_EXCL_LINE auto const ctx = secp256k1Context(); // Parse commitment from compressed format secp256k1_pubkey pcBalance; if (secp256k1_ec_pubkey_parse(ctx, &pcBalance, commitment.data(), ecPedersenCommitmentLength) != 1) return tecINTERNAL; // LCOV_EXCL_LINE // Convert amount to 32-byte big-endian scalar unsigned char mScalar[32] = {0}; std::uint64_t amountBigEndian = boost::endian::native_to_big(amount); std::memcpy(&mScalar[24], &amountBigEndian, sizeof(amountBigEndian)); // Compute mG = amount * G secp256k1_pubkey mG; if (!secp256k1_ec_pubkey_create(ctx, &mG, mScalar)) return tecINTERNAL; // LCOV_EXCL_LINE // Negate mG to get -mG if (!secp256k1_ec_pubkey_negate(ctx, &mG)) return tecINTERNAL; // LCOV_EXCL_LINE // Compute pcRem = pcBalance + (-mG) secp256k1_pubkey const* summands[2] = {&pcBalance, &mG}; secp256k1_pubkey pcRem; if (!secp256k1_ec_pubkey_combine(ctx, &pcRem, summands, 2)) return tecINTERNAL; // LCOV_EXCL_LINE // Serialize result to compressed format out.alloc(ecPedersenCommitmentLength); size_t outLen = ecPedersenCommitmentLength; if (secp256k1_ec_pubkey_serialize(ctx, out.data(), &outLen, &pcRem, SECP256K1_EC_COMPRESSED) != 1 || outLen != ecPedersenCommitmentLength) return tecINTERNAL; // LCOV_EXCL_LINE return tesSUCCESS; } } // namespace xrpl