From 7154f7f159b79dd70e161836bbf74151042464e4 Mon Sep 17 00:00:00 2001 From: levonpetrosyan93 <45027856+levonpetrosyan93@users.noreply.github.com> Date: Tue, 21 Nov 2023 12:49:19 +0400 Subject: [PATCH] Lelantus JoinSplit ownership Proof creation/Verification (#1347) * Lelantus tx ownership proof * Better error messages added * Typo fix * Mac build fix --- src/rpc/misc.cpp | 38 ++++++++++++++++++++++ src/validation.cpp | 69 ++++++++++++++++++++++++++++++++++++++++ src/validation.h | 4 +++ src/wallet/rpcwallet.cpp | 47 +++++++++++++++++++++++++++ src/wallet/wallet.cpp | 61 +++++++++++++++++++++++++++++++++++ src/wallet/wallet.h | 2 ++ 6 files changed, 221 insertions(+) diff --git a/src/rpc/misc.cpp b/src/rpc/misc.cpp index 823ad7fdda..dac3f3556b 100644 --- a/src/rpc/misc.cpp +++ b/src/rpc/misc.cpp @@ -488,6 +488,42 @@ UniValue signmessagewithprivkey(const JSONRPCRequest& request) return EncodeBase64(&vchSig[0], vchSig.size()); } +UniValue verifyprivatetxown(const JSONRPCRequest& request) +{ + if (request.fHelp || request.params.size() != 3) + throw std::runtime_error( + "verifyprivatetxown \"txid\" \"signature\" \"message\"\n" + "\nVerify a lelantus tx ownership\n" + "\nArguments:\n" + "1. \"txid\" (string, required) Txid, in which we spend lelantus coins.\n" + "2. \"proof\" (string, required) The signatures of the message encoded in base 64\n" + "3. \"message\" (string, required) The message that was signed.\n" + "\nResult:\n" + "true|false (boolean) If the signature is verified or not.\n" + "\nExamples:\n" + "\nVerify the signature\n" + + HelpExampleCli("verifyprivatetxown", "\"34df0ec7bcc8a2bda2c0df41ac560172d974c56ffc9adc0e2377d0fc54b4e8f9\" \"signature\" \"my message\"") + + "\nAs json rpc\n" + + HelpExampleRpc("verifyprivatetxown", "\"34df0ec7bcc8a2bda2c0df41ac560172d974c56ffc9adc0e2377d0fc54b4e8f9\", \"signature\", \"my message\"") + ); + + LOCK(cs_main); + + std::string strTxId = request.params[0].get_str(); + std::string strProof = request.params[1].get_str(); + std::string strMessage = request.params[2].get_str(); + + uint256 txid = uint256S(strTxId); + bool fInvalid = false; + std::vector vchSig = DecodeBase64(strProof.c_str(), &fInvalid); + + if (fInvalid) + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Malformed base64 encoding"); + + return VerifyPrivateTxOwn(txid, vchSig, strMessage); +} + + UniValue setmocktime(const JSONRPCRequest& request) { if (request.fHelp || request.params.size() != 1) @@ -1630,6 +1666,8 @@ static const CRPCCommand commands[] = { "firo", "znsync", &mnsync, true, {} }, { "firo", "evoznsync", &mnsync, true, {} }, + { "firo", "verifyprivatetxown", &verifyprivatetxown, true, {} }, + /* Not shown in help */ { "hidden", "getinfoex", &getinfoex, false }, { "addressindex", "gettotalsupply", &gettotalsupply, false }, diff --git a/src/validation.cpp b/src/validation.cpp index aad1ea69f1..d8e684f25b 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -141,6 +141,7 @@ static void CheckBlockIndex(const Consensus::Params& consensusParams); CScript COINBASE_FLAGS; const std::string strMessageMagic = "Zcoin Signed Message:\n"; +const std::string strLelantusMessageMagic = "Lelantus signed Message:\n"; // Internal stuff namespace { @@ -333,6 +334,74 @@ bool CheckFinalTx(const CTransaction &tx, int flags) return IsFinalTx(tx, nBlockHeight, nBlockTime); } +bool VerifyPrivateTxOwn(const uint256& txid, const std::vector& vchSig, const std::string& message) +{ + CTransactionRef tx; + uint256 hashBlock; + if(!GetTransaction(txid, tx, Params().GetConsensus(), hashBlock, true)) + return false; + + if (tx->IsLelantusJoinSplit()) { + CHashWriter ss(SER_GETHASH, 0); + ss << strLelantusMessageMagic; + ss << message; + + std::unique_ptr joinsplit; + try { + joinsplit = lelantus::ParseLelantusJoinSplit(*tx); + } catch (const std::exception&) { + return false; + } + const auto& pubKeys = joinsplit->GetEcdsaPubkeys(); + + if((pubKeys.size() *64) != vchSig.size()) { + LogPrintf("Verification to serialNumbers and ecdsaSignatures/ecdsaPubkeys number mismatch."); + return false; + } + + uint32_t count = 0; + + for (const auto& pub : pubKeys) { + ss << count; + uint256 metahash = ss.GetHash(); + + // Check sizes + if (pub.size() != 33 ) { + LogPrintf("Verification failed due to incorrect size of ecdsaSignature."); + return false; + } + + // Verify signature + secp256k1_pubkey pubkey; + secp256k1_ecdsa_signature signature; + + if (!secp256k1_ec_pubkey_parse(OpenSSLContext::get_context(), &pubkey, pub.data(), 33)) { + LogPrintf("Verification failed due to unable to parse ecdsaPubkey."); + return false; + } + + if (1 != secp256k1_ecdsa_signature_parse_compact(OpenSSLContext::get_context(), &signature, &vchSig[count * 64]) ) { + LogPrintf("Verification failed due to signature cannot be parsed."); + return false; + } + + if (!secp256k1_ecdsa_verify( + OpenSSLContext::get_context(), &signature, metahash.begin(), &pubkey)) { + LogPrintf("Verification failed due to signature cannot be verified."); + return false; + } + + count++; + } + } else if (tx->IsCoinBase()) { + throw std::runtime_error("This is a coinbase transaction and not a private transaction"); + } else { + throw std::runtime_error("Currently this is allowed only for Lelantus transactions"); + } + + return true; +} + /** * Calculates the block height and previous block's median time past at * which the transaction will be considered final in the context of BIP 68. diff --git a/src/validation.h b/src/validation.h index 33f0eefb95..36051d4a93 100644 --- a/src/validation.h +++ b/src/validation.h @@ -189,6 +189,7 @@ extern uint64_t nLastBlockTx; extern uint64_t nLastBlockSize; extern uint64_t nLastBlockWeight; extern const std::string strMessageMagic; +extern const std::string strLelantusMessageMagic; extern CWaitableCriticalSection csBestBlock; extern CConditionVariable cvBlockChange; extern std::atomic_bool fImporting; @@ -456,6 +457,9 @@ bool IsFinalTx(const CTransaction &tx, int nBlockHeight, int64_t nBlockTime); */ bool CheckFinalTx(const CTransaction &tx, int flags = -1); + +bool VerifyPrivateTxOwn(const uint256& txid, const std::vector& vchSig, const std::string& message); + /** * Test whether the LockPoints height and time are still valid on the current chain */ diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 97947e3b7f..94a3f0e1c4 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -680,6 +680,52 @@ UniValue signmessage(const JSONRPCRequest& request) return EncodeBase64(&vchSig[0], vchSig.size()); } +UniValue proveprivatetxown(const JSONRPCRequest& request) +{ + CWallet * const pwallet = GetWalletForJSONRPCRequest(request); + if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) { + return NullUniValue; + } + + if (request.fHelp || request.params.size() != 2) + throw std::runtime_error( + "proveprivatetxown \"txid\" \"message\"\n" + "\nCreated a proof by signing the message with private key of each spent coin." + + HelpRequiringPassphrase(pwallet) + "\n" + "\nArguments:\n" + "1. \"strTxId\" (string, required) Txid, in which we spend lelantus coins.\n" + "2. \"message\" (string, required) The message to create a signature of.\n" + "\nResult:\n" + "\"proof\" (string) The signatures of the message encoded in base 64\n" + "\nExamples:\n" + "\nUnlock the wallet for 30 seconds\n" + + HelpExampleCli("walletpassphrase", "\"mypassphrase\" 30") + + "\nCreate the signature\n" + + HelpExampleCli("proveprivatetxown", "\"34df0ec7bcc8a2bda2c0df41ac560172d974c56ffc9adc0e2377d0fc54b4e8f9 \" \"my message\"") + + "\nVerify the signature\n" + + HelpExampleCli("verifyprivatetxown", "\"34df0ec7bcc8a2bda2c0df41ac560172d974c56ffc9adc0e2377d0fc54b4e8f9 \" \"proof\" \"my message\"") + + "\nAs json rpc\n" + + HelpExampleRpc("proveprivatetxown", "\"34df0ec7bcc8a2bda2c0df41ac560172d974c56ffc9adc0e2377d0fc54b4e8f9 \", \"my message\"") + ); + + EnsureLelantusWalletIsAvailable(); + + LOCK2(cs_main, pwallet->cs_wallet); + EnsureWalletIsUnlocked(pwallet); + + std::string strTxId = request.params[0].get_str(); + std::string strMessage = request.params[1].get_str(); + + uint256 txid = uint256S(strTxId); + std::vector vchSig = pwallet->ProvePrivateTxOwn(txid, strMessage); + + if (vchSig.empty()) + throw JSONRPCError(RPC_INVALID_PARAMETER, "Something went wrong, may be you are not the owner of provided tx"); + + return EncodeBase64(&vchSig[0], vchSig.size()); +} + + UniValue getreceivedbyaddress(const JSONRPCRequest& request) { CWallet * const pwallet = GetWalletForJSONRPCRequest(request); @@ -5519,6 +5565,7 @@ static const CRPCCommand commands[] = { "wallet", "setaccount", &setaccount, true, {"address","account"} }, { "wallet", "settxfee", &settxfee, true, {"amount"} }, { "wallet", "signmessage", &signmessage, true, {"address","message"} }, + { "wallet", "proveprivatetxown", &proveprivatetxown, true, {"txid","message"} }, { "wallet", "walletlock", &walletlock, true, {} }, { "wallet", "walletpassphrasechange", &walletpassphrasechange, true, {"oldpassphrase","newpassphrase"} }, { "wallet", "walletpassphrase", &walletpassphrase, true, {"passphrase","timeout"} }, diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index a86adce8bb..2894616f5e 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -3580,6 +3580,67 @@ bool CWallet::GetCoinsToJoinSplit( return true; } +std::vector CWallet::ProvePrivateTxOwn(const uint256& txid, const std::string& message) const { + std::vector result; + if (!mapWallet.count(txid)) + return result; + + const CWalletTx& wtx = mapWallet.at(txid); + + if (wtx.tx->IsLelantusJoinSplit()) { + CHashWriter ss(SER_GETHASH, 0); + ss << strLelantusMessageMagic; + ss << message; + + std::unique_ptr joinsplit; + try { + joinsplit = lelantus::ParseLelantusJoinSplit(*wtx.tx); + } catch (const std::exception&) { + return result; + } + const auto& serials = joinsplit->getCoinSerialNumbers(); + uint32_t count = 0; + result.resize(serials.size() * 64); + + for (const auto& serial : serials) { + CLelantusEntry mint; + uint256 hashSerial = primitives::GetSerialHash(serial); + std::vector ecdsaSecretKey; + if (!GetMint(hashSerial, mint, false)) { + CSigmaEntry sigmaMint; + if (!GetMint(hashSerial, mint, false)) { + return std::vector(); + } + ecdsaSecretKey = sigmaMint.ecdsaSecretKey; + } else { + ecdsaSecretKey = mint.ecdsaSecretKey; + + } + + ss << count; + uint256 metahash = ss.GetHash(); + secp256k1_ecdsa_signature sig; + if (1 != secp256k1_ecdsa_sign( + OpenSSLContext::get_context(), &sig, + metahash.begin(), &ecdsaSecretKey[0], NULL, NULL)) { + return std::vector(); + } + if (1 != secp256k1_ecdsa_signature_serialize_compact( + OpenSSLContext::get_context(), &result[count * 64], &sig)) { + return std::vector(); + } + + count++; + } + } else if (wtx.tx->IsCoinBase()) { + throw std::runtime_error("This is a coinbase transaction and not a private transaction"); + } else { + throw std::runtime_error("Currently this operation is allowed only for Lelantus transactions"); + } + + return result; +} + CAmount CWallet::GetUnconfirmedBalance() const { CAmount nTotal = 0; { diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 150d0675cc..d1ab5eea0f 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -1006,6 +1006,8 @@ class CWallet : public CCryptoKeyStore, public CValidationInterface const CAmount amountToSpendLimit = MAX_MONEY, const CCoinControl *coinControl = NULL) const; + std::vector ProvePrivateTxOwn(const uint256& txid, const std::string& message) const; + /** * Insert additional inputs into the transaction by * calling CreateTransaction();