From d13d2c24b6d080e004a7ba1b4cc17b78dbd02663 Mon Sep 17 00:00:00 2001 From: alex v Date: Sun, 18 May 2025 21:00:20 +0200 Subject: [PATCH 01/20] fix CompactBlock and other tests --- src/blockencodings.cpp | 5 +- src/blsct/tokens/rpc.cpp | 2 +- src/rpc/blockchain.cpp | 2 +- test/functional/test_framework/messages.py | 157 ++++++++++++++++++++- 4 files changed, 159 insertions(+), 7 deletions(-) diff --git a/src/blockencodings.cpp b/src/blockencodings.cpp index 18a5fdbed1810..001d1b872139e 100644 --- a/src/blockencodings.cpp +++ b/src/blockencodings.cpp @@ -31,7 +31,10 @@ CBlockHeaderAndShortTxIDs::CBlockHeaderAndShortTxIDs(const CBlock& block) : void CBlockHeaderAndShortTxIDs::FillShortTxIDSelector() const { DataStream stream{}; - stream << header << posProof << nonce; + stream << header; + if (header.IsProofOfStake()) + stream << posProof; + stream << nonce; CSHA256 hasher; hasher.Write((unsigned char*)&(*stream.begin()), stream.end() - stream.begin()); uint256 shorttxidhash; diff --git a/src/blsct/tokens/rpc.cpp b/src/blsct/tokens/rpc.cpp index db2c662538012..6f79ea5f9d482 100644 --- a/src/blsct/tokens/rpc.cpp +++ b/src/blsct/tokens/rpc.cpp @@ -18,7 +18,7 @@ std::vector tokenInfoResult = { RPCResult{RPCResult::Type::OBJ_DYN, "metadata", "the token metadata", {{RPCResult::Type::STR, "xxxx", "value"}}}, RPCResult{RPCResult::Type::NUM, "maxSupply", "the token max supply"}, RPCResult{RPCResult::Type::NUM, "currentSupply", true, "the token current supply"}, - RPCResult{RPCResult::Type::OBJ_DYN, "mintedNft", "the nfts already minted", {{RPCResult::Type::OBJ_DYN, "index", "metadata", {{RPCResult::Type::STR, "xxxx", "value"}}}}}, + RPCResult{RPCResult::Type::OBJ_DYN, "mintedNft", true, "the nfts already minted", {{RPCResult::Type::OBJ_DYN, "index", "metadata", {{RPCResult::Type::STR, "xxxx", "value"}}}}}, }; diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index a266cdc1af666..7275bc04f02bf 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1347,7 +1347,7 @@ RPCHelpMan getdeploymentinfo() RPCResult::Type::OBJ, "", "", { {RPCResult::Type::STR, "hash", "requested block hash (or tip)"}, {RPCResult::Type::NUM, "height", "requested block height (or tip)"}, - {RPCResult::Type::OBJ_DYN, "deployments", "", {{RPCResult::Type::STR, "xxxx", "name of the deployment"}}}, + {RPCResult::Type::OBJ_DYN, "deployments", "", {{RPCResult::Type::OBJ, "xxxx", "name of the deployment", RPCHelpForDeployment}}}, }}, RPCExamples{HelpExampleCli("getdeploymentinfo", "") + HelpExampleRpc("getdeploymentinfo", "")}, [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py index c99c5411293c3..5af7c9a851fba 100755 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -942,6 +942,9 @@ def rehash(self): self.calc_sha256() return self.sha256 + def IsProofOfStake(self): + return self.nVersion & 0x01000000 + def __repr__(self): return "CBlockHeader(nVersion=%i hashPrevBlock=%064x hashMerkleRoot=%064x nTime=%s nBits=%08x nNonce=%08x)" \ % (self.nVersion, self.hashPrevBlock, self.hashMerkleRoot, @@ -951,19 +954,24 @@ def __repr__(self): assert_equal(BLOCK_HEADER_SIZE, 80) class CBlock(CBlockHeader): - __slots__ = ("vtx",) + __slots__ = ("vtx", "posProof") def __init__(self, header=None): super().__init__(header) self.vtx = [] + self.posProof = PosProof() def deserialize(self, f): super().deserialize(f) + if self.IsProofOfStake(): + self.posProof.deserialize(f) self.vtx = deser_vector(f, CTransaction) def serialize(self, with_witness=True): r = b"" r += super().serialize() + if self.IsProofOfStake(): + r += self.posProof.serialize() if with_witness: r += ser_vector(self.vtx, "serialize_with_witness") else: @@ -1061,11 +1069,142 @@ def serialize_with_witness(self): def __repr__(self): return "PrefilledTransaction(index=%d, tx=%s)" % (self.index, repr(self.tx)) +class SetMemProof: + __slots__ = ("phi", "A1", "A2", "S1", "S2", "S3", "T1", "T2", "tau_x", "mu", "z_alpha", "z_tau", "z_beta", "t", "Ls", "Rs", "a", "b", "omega") + + def __init__(self): + self.phi = MclG1Point() + self.A1 = MclG1Point() + self.A2 = MclG1Point() + self.S1 = MclG1Point() + self.S2 = MclG1Point() + self.S3 = MclG1Point() + self.T1 = MclG1Point() + self.T2 = MclG1Point() + self.tau_x = MclScalar() + self.mu = MclScalar() + self.z_alpha = MclScalar() + self.z_tau = MclScalar() + self.z_beta = MclScalar() + self.t = MclScalar() + self.Ls = [] + self.Rs = [] + self.a = MclScalar() + self.b = MclScalar() + self.omega = MclScalar() + + def deserialize(self, f): + self.phi.deserialize(f) + self.A1.deserialize(f) + self.A2.deserialize(f) + self.S1.deserialize(f) + self.S2.deserialize(f) + self.S3.deserialize(f) + self.T1.deserialize(f) + self.T2.deserialize(f) + self.tau_x.deserialize(f) + self.mu.deserialize(f) + self.z_alpha.deserialize(f) + self.z_tau.deserialize(f) + self.z_beta.deserialize(f) + self.t.deserialize(f) + self.Ls = deser_vector(f, MclG1Point) + self.Rs = deser_vector(f, MclG1Point) + self.a.deserialize(f) + self.b.deserialize(f) + self.omega.deserialize(f) + + def serialize(self): + r = b"" + r += self.phi.serialize() + r += self.A1.serialize() + r += self.A2.serialize() + r += self.S1.serialize() + r += self.S2.serialize() + r += self.S3.serialize() + r += self.T1.serialize() + r += self.T2.serialize() + r += self.tau_x.serialize() + r += self.mu.serialize() + r += self.z_alpha.serialize() + r += self.z_tau.serialize() + r += self.z_beta.serialize() + r += self.t.serialize() + r += ser_vector(self.Ls, "serialize") + r += ser_vector(self.Rs, "serialize") + r += self.a.serialize() + r += self.b.serialize() + r += self.omega.serialize() + return r + +class RangeProof: + __slots__ = 'Vs', 'Ls', 'Rs', 'A', 'A_wip', 'B', 'r_prime', 's_prime', 'delta_prime', 'alpha_hat', 'tau_x' + + def __init__(self): + self.Vs = [] + self.Ls = [] + self.Rs = [] + self.A = MclG1Point() + self.A_wip = MclG1Point() + self.B = MclG1Point() + self.r_prime = MclScalar() + self.s_prime = MclScalar() + self.delta_prime = MclScalar() + self.alpha_hat = MclScalar() + self.tau_x = MclScalar() + + def deserialize(self, f): + self.Vs = deser_vector(f, MclG1Point) + self.Ls = deser_vector(f, MclG1Point) + self.Rs = deser_vector(f, MclG1Point) + self.A.deserialize(f) + self.A_wip.deserialize(f) + self.B.deserialize(f) + self.r_prime.deserialize(f) + self.s_prime.deserialize(f) + self.delta_prime.deserialize(f) + self.alpha_hat.deserialize(f) + self.tau_x.deserialize(f) + + def serialize(self): + r = b"" + r += ser_vector(self.Vs, "serialize") + r += ser_vector(self.Ls, "serialize") + r += ser_vector(self.Rs, "serialize") + r += self.A.serialize() + r += self.A_wip.serialize() + r += self.B.serialize() + r += self.r_prime.serialize() + r += self.s_prime.serialize() + r += self.delta_prime.serialize() + r += self.alpha_hat.serialize() + r += self.tau_x.serialize() + return r + + +class PosProof: + __slots__ = ("set_mem_proof", "range_proof") + + def __init__(self, set_mem_proof=SetMemProof(), range_proof=RangeProof()): + self.set_mem_proof = set_mem_proof + self.range_proof = range_proof + + def deserialize(self, f): + self.set_mem_proof = SetMemProof() + self.set_mem_proof.deserialize(f) + self.range_proof = RangeProof() + self.range_proof.deserialize(f) + + def serialize(self): + r = b"" + r += self.set_mem_proof.serialize() + r += self.range_proof.serialize() + return r # This is what we send on the wire, in a cmpctblock message. class P2PHeaderAndShortIDs: __slots__ = ("header", "nonce", "prefilled_txn", "prefilled_txn_length", - "shortids", "shortids_length") + "shortids", "shortids_length", "posProof") def __init__(self): self.header = CBlockHeader() @@ -1074,9 +1213,12 @@ def __init__(self): self.shortids = [] self.prefilled_txn_length = 0 self.prefilled_txn = [] + self.posProof = PosProof() def deserialize(self, f): self.header.deserialize(f) + if self.header.IsProofOfStake(): + self.posProof.deserialize(f) self.nonce = struct.unpack(" Date: Mon, 23 Jun 2025 22:13:59 +0200 Subject: [PATCH 02/20] adds rpc methods for balance proof and raw blsct transactions --- src/Makefile.am | 8 + src/Makefile.test_util.include | 2 + src/blsct/key_io.h | 10 +- src/blsct/wallet/balance_proof.cpp | 6 + src/blsct/wallet/balance_proof.h | 95 ++++ src/blsct/wallet/rpc.cpp | 655 +++++++++++++++++++++- src/blsct/wallet/txfactory_global.h | 36 ++ src/blsct/wallet/unsigned_transaction.cpp | 95 ++++ src/blsct/wallet/unsigned_transaction.h | 55 ++ src/kernel/chainparams.cpp | 10 +- src/rpc/client.cpp | 3 + src/rpc/mempool.cpp | 2 +- src/rpc/rawtransaction.cpp | 6 +- src/rpc/register.h | 3 +- src/wallet/rpc/wallet.cpp | 121 ++-- src/wallet/wallet.cpp | 7 + test/functional/blsct_balance_proof.py | 91 +++ test/functional/blsct_rawtransaction.py | 237 ++++++++ test/functional/test_runner.py | 3 +- 19 files changed, 1370 insertions(+), 75 deletions(-) create mode 100644 src/blsct/wallet/balance_proof.cpp create mode 100644 src/blsct/wallet/balance_proof.h create mode 100644 src/blsct/wallet/unsigned_transaction.cpp create mode 100644 src/blsct/wallet/unsigned_transaction.h create mode 100755 test/functional/blsct_balance_proof.py create mode 100755 test/functional/blsct_rawtransaction.py diff --git a/src/Makefile.am b/src/Makefile.am index 8f7c3fe1fada6..7cba7ceb7d1e1 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -205,6 +205,7 @@ BLSCT_H = \ blsct/tokens/predicate_parser.h \ blsct/tokens/rpc.h \ blsct/wallet/address.h \ + blsct/wallet/balance_proof.h \ blsct/wallet/hdchain.h \ blsct/wallet/helpers.h \ blsct/wallet/import_wallet_type.h \ @@ -214,6 +215,7 @@ BLSCT_H = \ blsct/wallet/txfactory.h \ blsct/wallet/txfactory_base.h \ blsct/wallet/txfactory_global.h \ + blsct/wallet/unsigned_transaction.h \ blsct/wallet/verification.h BLSCT_CPP = \ @@ -268,6 +270,7 @@ BLSCT_CPP = \ blsct/wallet/txfactory.cpp \ blsct/wallet/txfactory_base.cpp \ blsct/wallet/txfactory_global.cpp \ + blsct/wallet/unsigned_transaction.cpp \ blsct/wallet/verification.cpp .PHONY: FORCE check-symbols check-security @@ -582,6 +585,7 @@ libbitcoin_node_a_SOURCES = \ blsct/tokens/info.cpp \ blsct/tokens/rpc.cpp \ blsct/wallet/rpc.cpp \ + blsct/wallet/unsigned_transaction.cpp \ blsct/wallet/verification.cpp \ blsct/signature.cpp \ chain.cpp \ @@ -736,10 +740,12 @@ libbitcoin_wallet_a_SOURCES = \ blsct/set_mem_proof/set_mem_proof_setup.cpp \ blsct/signature.cpp \ blsct/wallet/address.cpp \ + blsct/wallet/balance_proof.cpp \ blsct/wallet/helpers.cpp \ blsct/wallet/keyman.cpp \ blsct/wallet/keyring.cpp \ blsct/wallet/rpc.cpp \ + blsct/wallet/unsigned_transaction.cpp \ blsct/wallet/txfactory.cpp \ blsct/wallet/txfactory_base.cpp \ blsct/wallet/txfactory_global.cpp \ @@ -987,6 +993,7 @@ libbitcoin_common_a_SOURCES = \ blsct/tokens/predicate_parser.cpp \ blsct/wallet/address.cpp \ blsct/wallet/txfactory_global.cpp \ + blsct/wallet/unsigned_transaction.cpp \ chainparams.cpp \ coins.cpp \ common/args.cpp \ @@ -1317,6 +1324,7 @@ libnaviokernel_la_SOURCES = \ blsct/tokens/predicate_exec.cpp \ blsct/tokens/predicate_parser.cpp \ blsct/wallet/txfactory_global.cpp \ + blsct/wallet/unsigned_transaction.cpp \ blsct/wallet/verification.cpp \ chain.cpp \ clientversion.cpp \ diff --git a/src/Makefile.test_util.include b/src/Makefile.test_util.include index e58b8410a1b40..1e28676269e71 100644 --- a/src/Makefile.test_util.include +++ b/src/Makefile.test_util.include @@ -19,6 +19,7 @@ TEST_UTIL_H = \ blsct/public_key.h \ blsct/public_keys.h \ blsct/signature.h \ + blsct/wallet/unsigned_transaction.h \ blsct/wallet/txfactory_global.h \ test/util/blockfilter.h \ test/util/chainstate.h \ @@ -54,6 +55,7 @@ libtest_util_a_SOURCES = \ blsct/public_key.cpp \ blsct/public_keys.cpp \ blsct/signature.cpp \ + blsct/wallet/unsigned_transaction.cpp \ blsct/wallet/rpc.cpp \ blsct/wallet/txfactory_global.cpp \ test/util/blockfilter.cpp \ diff --git a/src/blsct/key_io.h b/src/blsct/key_io.h index 8ae2bd74f9444..35c8b9b484d5d 100644 --- a/src/blsct/key_io.h +++ b/src/blsct/key_io.h @@ -19,13 +19,13 @@ namespace blsct { // - 1-byte separator '1' // - 154-byte data // - 8-byte checksum -constexpr size_t DOUBLE_PUBKEY_ENC_SIZE = 2 + 1 + bech32_mod::DOUBLE_PUBKEY_DATA_ENC_SIZE + 8; +constexpr size_t DOUBLE_PUBKEY_ENC_SIZE = 3 + 1 + bech32_mod::DOUBLE_PUBKEY_DATA_ENC_SIZE + 8; namespace bech32_hrp { - const std::string Main = "nv"; - const std::string TestNet = "tn"; - const std::string SigNet = "tn"; - const std::string RegTest = "nr"; + const std::string Main = "nav"; + const std::string TestNet = "tnv"; + const std::string SigNet = "snv"; + const std::string RegTest = "rnav"; } /** Encode DoublePublicKey to Bech32 or Bech32m string. Encoding must be one of BECH32 or BECH32M. */ diff --git a/src/blsct/wallet/balance_proof.cpp b/src/blsct/wallet/balance_proof.cpp new file mode 100644 index 0000000000000..d41904e10e395 --- /dev/null +++ b/src/blsct/wallet/balance_proof.cpp @@ -0,0 +1,6 @@ +#include +#include + +namespace blsct { +// Implementation is in the header file since it's all inline +} // namespace blsct diff --git a/src/blsct/wallet/balance_proof.h b/src/blsct/wallet/balance_proof.h new file mode 100644 index 0000000000000..6bc44d6269fd1 --- /dev/null +++ b/src/blsct/wallet/balance_proof.h @@ -0,0 +1,95 @@ +// src/blsct/wallet/balance_proof.h +#ifndef BITCOIN_BLSCT_WALLET_BALANCE_PROOF_H +#define BITCOIN_BLSCT_WALLET_BALANCE_PROOF_H + +#include +#include +#include +#include +#include +#include + +namespace blsct { + +class BalanceProof +{ +private: + std::vector m_outpoints; + CAmount m_min_amount; + bulletproofs_plus::RangeProof m_proof; + +public: + BalanceProof() = default; + BalanceProof(const std::vector& outpoints, CAmount min_amount, const bulletproofs_plus::RangeProof& proof) + : m_outpoints(outpoints), m_min_amount(min_amount), m_proof(proof) {} + + BalanceProof(const std::vector& outpoints, CAmount min_amount, const wallet::CWallet& wallet) + { + m_outpoints = outpoints; + m_min_amount = min_amount; + + // Sum up all commitments from the outputs + MclG1Point sum_commitment; + Elements values; + for (const auto& outpoint : outpoints) { + const wallet::CWalletTx* wtx = wallet.GetWalletTx(outpoint.hash); + if (!wtx) { + throw std::runtime_error("Outpoint not found in wallet"); + } + if (outpoint.n >= wtx->tx->vout.size()) { + throw std::runtime_error("Invalid output index"); + } + const CTxOut& txout = wtx->tx->vout[outpoint.n]; + if (!txout.HasBLSCTRangeProof()) { + throw std::runtime_error("Outpoint does not have BLSCT range proof"); + } + sum_commitment = sum_commitment + txout.blsctData.rangeProof.Vs[0]; + auto recoveryData = wtx->GetBLSCTRecoveryData(outpoint.n); + values.Add(MclScalar(recoveryData.amount)); + } + + // Create range proof + bulletproofs_plus::RangeProofLogic prover; + range_proof::GammaSeed nonce(sum_commitment); + std::vector message; + m_proof = prover.Prove(values, nonce, message, TokenId(), MclScalar(min_amount)); + } + + const std::vector& GetOutpoints() const { return m_outpoints; } + CAmount GetMinAmount() const { return m_min_amount; } + const bulletproofs_plus::RangeProof& GetProof() const { return m_proof; } + + bool Verify(const CCoinsViewCache& view) const + { + // Sum up all commitments from the outputs + MclG1Point sum_commitment; + for (const auto& outpoint : m_outpoints) { + Coin coin; + if (!view.GetCoin(outpoint, coin)) { + return false; + } + if (!coin.out.HasBLSCTRangeProof()) { + return false; + } + sum_commitment = sum_commitment + coin.out.blsctData.rangeProof.Vs[0]; + } + + // Create a range proof with seed for verification + bulletproofs_plus::RangeProofWithSeed proof(m_proof, TokenId(), MclScalar(m_min_amount)); + std::vector> proofs; + proofs.push_back(proof); + + // Verify the range proof + bulletproofs_plus::RangeProofLogic prover; + return prover.Verify(proofs); + } + + SERIALIZE_METHODS(BalanceProof, obj) + { + READWRITE(obj.m_outpoints, obj.m_min_amount, obj.m_proof); + } +}; + +} // namespace blsct + +#endif // BITCOIN_BLSCT_WALLET_BALANCE_PROOF_H \ No newline at end of file diff --git a/src/blsct/wallet/rpc.cpp b/src/blsct/wallet/rpc.cpp index 37901096d2d39..eed60064a5c23 100644 --- a/src/blsct/wallet/rpc.cpp +++ b/src/blsct/wallet/rpc.cpp @@ -2,7 +2,9 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include #include +#include #include #include #include @@ -646,6 +648,7 @@ RPCHelpMan sendtokentoblsctaddress() const bool verbose{request.params[4].isNull() ? false : request.params[11].get_bool()}; + blsct::SubAddress subAddress(std::get(destination)); blsct::CreateTransactionData transactionData(address, AmountFromValue(request.params[2]), sMemo, TokenId(token_id), blsct::CreateTransactionType::NORMAL, 0); EnsureWalletIsUnlocked(*pwallet); @@ -723,6 +726,7 @@ RPCHelpMan sendnfttoblsctaddress() const bool verbose{request.params[4].isNull() ? false : request.params[4].get_bool()}; + blsct::SubAddress subAddress(std::get(destination)); blsct::CreateTransactionData transactionData(address, 1, sMemo, TokenId(token_id, nft_id), blsct::CreateTransactionType::NORMAL, 0); EnsureWalletIsUnlocked(*pwallet); @@ -952,7 +956,7 @@ RPCHelpMan listblsctunspent() cctl.m_min_depth = nMinDepth; cctl.m_max_depth = nMaxDepth; LOCK(pwallet->cs_wallet); - vecOutputs = AvailableBlsctCoins(*pwallet, &cctl, filter_coins).All(); + vecOutputs = (pwallet->IsWalletFlagSet(wallet::WALLET_FLAG_BLSCT_OUTPUT_STORAGE) ? AvailableBlsctCoins(*pwallet, &cctl, filter_coins) : AvailableCoins(*pwallet, nullptr, std::nullopt, filter_coins)).All(); } LOCK(pwallet->cs_wallet); @@ -1106,6 +1110,639 @@ RPCHelpMan listblscttransactions() }; }; + +static RPCHelpMan setblsctseed() +{ + return RPCHelpMan{ + "setblsctseed", + "\nSet or generate a new BLSCT wallet seed. Non-BLSCT wallets will not be upgraded to being a BLSCT wallet. Wallets that are already\n" + "BLSCT will have a new BLSCT seed set so that new keys added to the keypool will be derived from this new seed.\n" + "\nNote that you will need to MAKE A NEW BACKUP of your wallet after setting the BLSCT wallet seed." + + wallet::HELP_REQUIRING_PASSPHRASE, + { + {"seed", RPCArg::Type::STR, RPCArg::DefaultHint{"random seed"}, "The WIF private key to use as the new HD seed.\n"}, + }, + RPCResult{RPCResult::Type::NONE, "", ""}, + RPCExamples{ + HelpExampleCli("setblsctseed", "") + HelpExampleCli("setblsctseed", "") + HelpExampleCli("setblsctseed", "\"wifkey\"") + HelpExampleRpc("setblsctseed", "\"wifkey\"")}, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { + std::shared_ptr const pwallet = wallet::GetWalletForJSONRPCRequest(request); + if (!pwallet) return UniValue::VNULL; + + auto blsct_km = pwallet->GetOrCreateBLSCTKeyMan(); + + if (pwallet->IsWalletFlagSet(wallet::WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Cannot set a BLSCT seed to a wallet with private keys disabled"); + } + + LOCK2(pwallet->cs_wallet, blsct_km->cs_KeyStore); + + // Do not do anything to non-HD wallets + if (!pwallet->IsWalletFlagSet(wallet::WALLET_FLAG_BLSCT)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Cannot set a BLSCT seed on a non-BLSCT wallet."); + } + + EnsureWalletIsUnlocked(*pwallet); + + blsct::PrivateKey master_priv_key; + if (request.params[1].isNull()) { + master_priv_key = blsct_km->GenerateNewSeed(); + } else { + CKey key = DecodeSecret(request.params[1].get_str()); + if (!key.IsValid()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key"); + } + + if (blsct_km->HaveKey(key.GetPubKey().GetID())) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Already have this key (either as an BLSCT seed or as a loose private key)"); + } + + master_priv_key = key.GetPrivKey(); + } + + blsct_km->SetHDSeed(master_priv_key); + + return UniValue::VNULL; + }, + }; +} + +RPCHelpMan createblsctbalanceproof() +{ + return RPCHelpMan{ + "createblsctbalanceproof", + "Creates a zero-knowledge proof that the wallet has at least the specified balance\n", + { + {"amount", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "The minimum balance to prove"}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", { + {RPCResult::Type::STR_HEX, "proof", "The serialized balance proof"}, + }}, + RPCExamples{HelpExampleCli("createblsctbalanceproof", "1.0") + HelpExampleRpc("createblsctbalanceproof", "1.0")}, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { + std::vector outpoints; + CAmount target_amount = AmountFromValue(request.params[0]); + + std::shared_ptr const pwallet = wallet::GetWalletForJSONRPCRequest(request); + if (!pwallet) return UniValue::VNULL; + + if (!pwallet->IsWalletFlagSet(wallet::WALLET_FLAG_BLSCT)) { + throw JSONRPCError(RPC_WALLET_ERROR, "BLSCT must be enabled for this wallet"); + } + + LOCK(pwallet->cs_wallet); + + if (target_amount <= 0) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Amount must be positive"); + } + + // Get available BLSCT coins + wallet::CoinFilterParams filter_coins; + filter_coins.only_blsct = true; + filter_coins.skip_locked = false; + filter_coins.include_immature_coinbase = false; + wallet::CoinsResult available_coins = AvailableCoins(*pwallet, nullptr, std::nullopt, filter_coins); + + if (available_coins.GetTotalAmount() < target_amount) { + throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Insufficient funds"); + } + + // Collect outpoints and create balance proof + for (const auto& [type, outputs] : available_coins.coins) { + for (const auto& output : outputs) { + outpoints.push_back(output.outpoint); + } + } + + blsct::BalanceProof proof(outpoints, target_amount, *pwallet); + + // Serialize the proof + DataStream ss{}; + ss << proof; + + UniValue result(UniValue::VOBJ); + result.pushKV("proof", HexStr(ss)); + + return result; + }, + }; +} + +RPCHelpMan verifyblsctbalanceproof() +{ + return RPCHelpMan{ + "verifyblsctbalanceproof", + "Verifies a zero-knowledge balance proof\n", + { + {"proof", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The serialized balance proof"}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", { + {RPCResult::Type::BOOL, "valid", "Whether the proof is valid"}, + {RPCResult::Type::NUM, "min_amount", "The minimum amount proven"}, + }}, + RPCExamples{HelpExampleCli("verifyblsctbalanceproof", "\"\"") + HelpExampleRpc("verifyblsctbalanceproof", "\"\"")}, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { + LOCK(cs_main); + ChainstateManager& chainman = EnsureAnyChainman(request.context); + Chainstate& active_chainstate = chainman.ActiveChainstate(); + + CCoinsViewCache* coins_view; + coins_view = &active_chainstate.CoinsTip(); + + // Deserialize the proof + std::vector proof_data = ParseHex(request.params[0].get_str()); + DataStream ss{proof_data}; + blsct::BalanceProof proof; + try { + ss >> proof; + } catch (const std::exception& e) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid proof format"); + } + + // Verify the proof + bool valid = proof.Verify(coins_view); + + UniValue result(UniValue::VOBJ); + result.pushKV("valid", valid); + result.pushKV("min_amount", ValueFromAmount(proof.GetMinAmount())); + + return result; + }, + }; +} + +RPCHelpMan createblsctrawtransaction() +{ + return RPCHelpMan{ + "createblsctrawtransaction", + "\nCreate a unsigned transaction spending the given inputs and creating new outputs.\n" + "Returns hex-encoded raw unsigned transaction.\n" + "Note that the transaction's inputs are not signed, and\n" + "it is not stored in the wallet or transmitted to the network.\n", + { + { + "inputs", + RPCArg::Type::ARR, + RPCArg::Optional::NO, + "A json array of json objects", + { + { + "", + RPCArg::Type::OBJ, + RPCArg::Optional::OMITTED, + "", + { + {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"}, + {"vout", RPCArg::Type::NUM, RPCArg::Optional::NO, "The output number"}, + {"value", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "The input value in satoshis"}, + {"gamma", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "The gamma value for the input (hex string)"}, + {"private_key", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED, "The private key for signing this input (hex string)"}, + {"is_staked_commitment", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED, "Whether this input is a staked commitment"}, + }, + }, + }, + }, + { + "outputs", + RPCArg::Type::ARR, + RPCArg::Optional::NO, + "A json array with outputs (key-value pairs)", + { + { + "", + RPCArg::Type::OBJ, + RPCArg::Optional::OMITTED, + "", + { + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "The BLSCT address to send to"}, + {"amount", RPCArg::Type::AMOUNT, RPCArg::Optional::NO, "The amount in " + CURRENCY_UNIT}, + {"memo", RPCArg::Type::STR, RPCArg::Default{""}, "A memo used to store in the transaction.\n" + "The recipient will see its value."}, + {"token_id", RPCArg::Type::STR_HEX, RPCArg::Default{""}, "The token id for token transactions"}, + }, + }, + }, + }, + }, + RPCResult{ + RPCResult::Type::STR_HEX, "transaction", "hex string of the transaction"}, + RPCExamples{ + HelpExampleCli("createblsctrawtransaction", "\"[{\\\"txid\\\":\\\"myid\\\",\\\"vout\\\":0,\\\"value\\\":1000000,\\\"gamma\\\":\\\"1234567890abcdef\\\",\\\"private_key\\\":\\\"abcdef1234567890\\\"}]\" \"[{\\\"address\\\":\\\"address\\\",\\\"amount\\\":0.01,\\\"memo\\\":\\\"memo\\\",\\\"token_id\\\":\\\"tokenid\\\"}]\"") + + HelpExampleCli("createblsctrawtransaction", "\"[{\\\"txid\\\":\\\"myid\\\",\\\"vout\\\":0}]\" \"[{\\\"address\\\":\\\"address\\\",\\\"amount\\\":0.01}]\"")}, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { + std::shared_ptr const pwallet = wallet::GetWalletForJSONRPCRequest(request); + if (!pwallet) return UniValue::VNULL; + + // Make sure the results are valid at least up to the most recent block + // the user could have gotten from another RPC command prior to now + pwallet->BlockUntilSyncedToCurrentChain(); + + LOCK(pwallet->cs_wallet); + + auto blsct_km = pwallet->GetOrCreateBLSCTKeyMan(); + + // Parse inputs + const UniValue& inputs = request.params[0].get_array(); + std::vector unsigned_inputs; + for (unsigned int idx = 0; idx < inputs.size(); idx++) { + const UniValue& input = inputs[idx]; + const UniValue& o = input.get_obj(); + + const Txid txid = Txid::FromUint256(ParseHashO(o, "txid")); + const int nOut = o.find_value("vout").getInt(); + if (nOut < 0) + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "vout must be positive"); + + blsct::UnsignedInput unsigned_input; + unsigned_input.in.prevout = COutPoint(txid, nOut); + + // Parse optional value field + if (o.exists("value")) { + CAmount value = o["value"].getInt(); + if (value < 0) + throw JSONRPCError(RPC_INVALID_PARAMETER, "Input value must be positive"); + unsigned_input.value = Scalar(value); + } + + // Parse optional gamma field + if (o.exists("gamma")) { + std::string gamma_hex = o["gamma"].get_str(); + if (!gamma_hex.empty()) { + try { + std::vector gamma_bytes = ParseHex(gamma_hex); + if (gamma_bytes.size() != 32) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Gamma must be 32 bytes (64 hex characters)"); + } + unsigned_input.gamma = Scalar(gamma_bytes); + } catch (const std::exception& e) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid gamma hex string"); + } + } + } + + // Parse optional private key field + if (o.exists("private_key")) { + std::string sk_hex = o["private_key"].get_str(); + if (!sk_hex.empty()) { + try { + std::vector sk_bytes = ParseHex(sk_hex); + if (sk_bytes.size() != 32) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Private key must be 32 bytes (64 hex characters)"); + } + unsigned_input.sk = blsct::PrivateKey(Scalar(sk_bytes)); + } catch (const std::exception& e) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid private key hex string"); + } + } + } + + // Parse optional is_staked_commitment field + if (o.exists("is_staked_commitment")) { + unsigned_input.is_staked_commitment = o["is_staked_commitment"].get_bool(); + } + + // If value or gamma are not provided, try to get them from the wallet + if (!o.exists("value") || !o.exists("gamma")) { + // Get the transaction from the wallet + auto wallet_tx = pwallet->GetWalletTx(txid); + if (!wallet_tx) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Transaction %s not found in wallet", txid.GetHex())); + } + + // Get BLSCT recovery data for this output + auto recovery_data = wallet_tx->GetBLSCTRecoveryData(nOut); + if (recovery_data.amount == 0 && recovery_data.gamma == Scalar(0) && recovery_data.id == 0 && recovery_data.message == "") { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("BLSCT recovery data not available for output %s:%d", txid.GetHex(), nOut)); + } + + // Set value if not provided + if (!o.exists("value")) { + unsigned_input.value = Scalar(recovery_data.amount); + } + + // Set gamma if not provided + if (!o.exists("gamma")) { + unsigned_input.gamma = recovery_data.gamma; + } + } + + // If private key is not provided, try to get it from the wallet + if (!o.exists("private_key")) { + // Get the output to determine the token ID + auto wallet_tx = pwallet->GetWalletTx(txid); + if (!wallet_tx) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Transaction %s not found in wallet", txid.GetHex())); + } + + const CTxOut& txout = wallet_tx->tx->vout[nOut]; + + // Get the spending key for this output + auto spending_key = blsct_km->GetSpendingKeyForOutputWithCache(txout); + if (!spending_key.IsValid()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Spending key not available for output %s:%d", txid.GetHex(), nOut)); + } + + unsigned_input.sk = spending_key; + } + + unsigned_inputs.push_back(unsigned_input); + } + + // Parse type + blsct::CreateTransactionType type = blsct::NORMAL; + if (!request.params[2].isNull()) { + std::string type_str = request.params[2].get_str(); + if (type_str == "create_token") { + type = blsct::TX_CREATE_TOKEN; + } else if (type_str == "mint_token") { + type = blsct::TX_MINT_TOKEN; + } else if (type_str == "mint_nft") { + type = blsct::TX_MINT_TOKEN; + } else if (type_str != "normal") { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid transaction type"); + } + } + + // Parse outputs + const UniValue& outputs = request.params[1].get_array(); + std::vector unsigned_outputs; + for (unsigned int idx = 0; idx < outputs.size(); idx++) { + const UniValue& output = outputs[idx]; + const UniValue& o = output.get_obj(); + + if (!o.exists("address") || !o.exists("amount")) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Each output must have an address and amount"); + } + + std::string address = o["address"].get_str(); + CTxDestination destination = DecodeDestination(address); + if (!IsValidDestination(destination) || destination.index() != 8) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, std::string("Invalid BLSCT address: ") + address); + } + + CAmount nAmount = AmountFromValue(o["amount"]); + if (nAmount < 0) + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid amount, must be positive"); + + std::string memo = o.exists("memo") ? o["memo"].get_str() : ""; + TokenId token_id; + if (o.exists("token_id") && !o["token_id"].get_str().empty()) { + token_id = TokenId(ParseHashV(o["token_id"], "token_id")); + } + + blsct::SubAddress subAddress(std::get(destination)); + blsct::UnsignedOutput unsigned_output = CreateOutput(subAddress.GetKeys(), nAmount, memo, token_id, Scalar::Rand(), type); + unsigned_outputs.push_back(unsigned_output); + } + + // Create unsigned transaction + blsct::UnsignedTransaction unsigned_tx; + + unsigned_tx = blsct::UnsignedTransaction(); + + // Add inputs and outputs + for (const auto& input : unsigned_inputs) { + unsigned_tx.AddInput(input); + } + for (const auto& output : unsigned_outputs) { + unsigned_tx.AddOutput(output); + } + + // Serialize the transaction + return HexStr(unsigned_tx.Serialize()); + }, + }; +} + +RPCHelpMan fundblsctrawtransaction() +{ + return RPCHelpMan{ + "fundblsctrawtransaction", + "\nAdd inputs to a BLSCT transaction until it has enough value to cover outputs and fee.\n", + { + {"hexstring", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The hex string of the raw transaction"}, + {"changeaddress", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "The BLSCT address to receive the change"}, + }, + RPCResult{ + RPCResult::Type::STR_HEX, "transaction", "hex string of the funded transaction"}, + RPCExamples{ + HelpExampleCli("fundblsctrawtransaction", "\"hexstring\"") + + HelpExampleCli("fundblsctrawtransaction", "\"hexstring\" \"changeaddress\"")}, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { + std::shared_ptr const pwallet = wallet::GetWalletForJSONRPCRequest(request); + if (!pwallet) return UniValue::VNULL; + + // Make sure the results are valid at least up to the most recent block + pwallet->BlockUntilSyncedToCurrentChain(); + + LOCK(pwallet->cs_wallet); + + auto blsct_km = pwallet->GetOrCreateBLSCTKeyMan(); + + // Parse the unsigned transaction + std::vector txData = ParseHex(request.params[0].get_str()); + auto unsigned_tx_opt = blsct::UnsignedTransaction::Deserialize(txData); + if (!unsigned_tx_opt) { + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "Transaction deserialization failed"); + } + auto unsigned_tx = unsigned_tx_opt.value(); + + // Calculate total output amount + CAmount output_value = 0; + for (const auto& out : unsigned_tx.GetOutputs()) { + output_value += out.value.GetUint64(); + } + + // Add fixed fee + CAmount required_value = output_value + COIN / 100; // 0.01 fixed fee + unsigned_tx.SetFee(COIN / 100); + + // Get change address + CTxDestination change_dest; + if (!request.params[1].isNull()) { + change_dest = DecodeDestination(request.params[1].get_str()); + if (!IsValidDestination(change_dest) || change_dest.index() != 8) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid BLSCT change address"); + } + } else { + // Get new change address from wallet + change_dest = std::get(blsct_km->GetNewDestination(blsct::CHANGE_ACCOUNT).value()); + } + + // Find unspent outputs to use as inputs + wallet::CoinFilterParams filter_coins; + filter_coins.only_blsct = true; + filter_coins.skip_locked = false; + filter_coins.include_immature_coinbase = false; + wallet::CoinsResult available_outputs = AvailableCoins(*pwallet, nullptr, std::nullopt, filter_coins); + + CAmount input_value = 0; + for (const auto& [type, outputs] : available_outputs.coins) { + for (const auto& output : outputs) { + auto wallet_tx = pwallet->GetWalletTx(output.outpoint.hash); + if (!wallet_tx) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Transaction %s not found in wallet", output.outpoint.hash.GetHex())); + } + + // Get BLSCT recovery data for this output + auto recovery_data = wallet_tx->GetBLSCTRecoveryData(output.outpoint.n); + + if (recovery_data.amount == 0 && recovery_data.gamma == Scalar(0) && recovery_data.id == 0 && recovery_data.message == "") { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("BLSCT recovery data not available for output %s:%d", output.outpoint.hash.GetHex(), output.outpoint.n)); + } + + blsct::UnsignedInput input; + input.in.prevout = output.outpoint; + input.value = Scalar(recovery_data.amount); + input.gamma = recovery_data.gamma; + + // Get the spending key for this output + auto spending_key = blsct_km->GetSpendingKeyForOutputWithCache(output.txout); + if (!spending_key.IsValid()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Spending key not available for output %s:%d", output.outpoint.hash.GetHex(), output.outpoint.n)); + } + + input.sk = spending_key; + + unsigned_tx.AddInput(input); + input_value += recovery_data.amount; + + if (input_value >= required_value) break; + } + + if (input_value < required_value) { + throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, "Insufficient funds"); + } + + // Add change output if needed + if (input_value > required_value) { + CAmount change_value = input_value - required_value; + blsct::SubAddress change_subaddr(std::get(change_dest)); + blsct::UnsignedOutput change_output = CreateOutput(change_subaddr.GetKeys(), change_value, "", TokenId(), Scalar::Rand()); + unsigned_tx.AddOutput(change_output); + } + + return HexStr(unsigned_tx.Serialize()); + } + }, + }; +} + +RPCHelpMan signblsctrawtransaction() +{ + return RPCHelpMan{ + "signblsctrawtransaction", + "\nSigns a BLSCT raw transaction by adding BLSCT signatures.\n", + { + {"hexstring", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction hex string"}, + }, + RPCResult{ + RPCResult::Type::STR_HEX, "hex", "The signed transaction hex"}, + RPCExamples{ + HelpExampleCli("signblsctrawtransaction", "\"hexstring\"") + + HelpExampleRpc("signblsctrawtransaction", "\"hexstring\"")}, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { + std::shared_ptr const pwallet = wallet::GetWalletForJSONRPCRequest(request); + if (!pwallet) return UniValue::VNULL; + + // Make sure the results are valid at least up to the most recent block + // the user could have gotten from another RPC command prior to now + pwallet->BlockUntilSyncedToCurrentChain(); + + LOCK(pwallet->cs_wallet); + + std::vector tx_data = ParseHex(request.params[0].get_str()); + auto unsigned_tx_opt = blsct::UnsignedTransaction::Deserialize(tx_data); + if (!unsigned_tx_opt) { + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "Transaction deserialization failed"); + } + auto unsigned_tx = unsigned_tx_opt.value(); + + // Sign the transaction + auto tx_opt = unsigned_tx.Sign(); + if (!tx_opt) { + throw JSONRPCError(RPC_WALLET_ERROR, "Failed to sign transaction"); + } + auto tx = tx_opt.value(); + + return EncodeHexTx(tx); + }, + }; +} + +RPCHelpMan decodeblsctrawtransaction() +{ + return RPCHelpMan{ + "decodeblsctrawtransaction", + "\nDecode a BLSCT raw transaction and return a JSON object describing the transaction structure.\n", + { + {"hexstring", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction hex string"}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", { + {RPCResult::Type::ARR, "inputs", "Array of transaction inputs", { + {RPCResult::Type::OBJ, "", "", { + {RPCResult::Type::STR_HEX, "txid", "The transaction id"}, + {RPCResult::Type::NUM, "vout", "The output number"}, + {RPCResult::Type::NUM, "value", "The input value in satoshis"}, + {RPCResult::Type::STR_HEX, "gamma", "The gamma value (hex string)"}, + {RPCResult::Type::BOOL, "is_staked_commitment", "Whether this input is a staked commitment"}, + }}, + }}, + {RPCResult::Type::ARR, "outputs", "Array of transaction outputs", { + {RPCResult::Type::OBJ, "", "", { + {RPCResult::Type::STR_AMOUNT, "amount", "The amount in " + CURRENCY_UNIT}, + {RPCResult::Type::STR_HEX, "blinding_key", "The blinding key (hex string)"}, + {RPCResult::Type::STR_HEX, "gamma", "The gamma value (hex string)"}, + }}, + }}, + {RPCResult::Type::STR_AMOUNT, "fee", "The transaction fee in " + CURRENCY_UNIT}, + }}, + RPCExamples{HelpExampleCli("decodeblsctrawtransaction", "\"hexstring\"") + HelpExampleRpc("decodeblsctrawtransaction", "\"hexstring\"")}, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { + std::vector tx_data = ParseHex(request.params[0].get_str()); + auto unsigned_tx_opt = blsct::UnsignedTransaction::Deserialize(tx_data); + if (!unsigned_tx_opt) { + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "Transaction deserialization failed"); + } + auto unsigned_tx = unsigned_tx_opt.value(); + + UniValue result(UniValue::VOBJ); + + // Decode inputs + UniValue inputs(UniValue::VARR); + for (const auto& input : unsigned_tx.GetInputs()) { + UniValue input_obj(UniValue::VOBJ); + input_obj.pushKV("txid", input.in.prevout.hash.GetHex()); + input_obj.pushKV("vout", (int)input.in.prevout.n); + input_obj.pushKV("value", ValueFromAmount(input.value.GetUint64())); + input_obj.pushKV("gamma", HexStr(input.gamma.GetVch())); + input_obj.pushKV("is_staked_commitment", input.is_staked_commitment); + inputs.push_back(input_obj); + } + result.pushKV("inputs", inputs); + + // Decode outputs + UniValue outputs(UniValue::VARR); + for (const auto& output : unsigned_tx.GetOutputs()) { + UniValue output_obj(UniValue::VOBJ); + output_obj.pushKV("amount", ValueFromAmount(output.value.GetUint64())); + output_obj.pushKV("blinding_key", HexStr(output.blindingKey.GetVch())); + output_obj.pushKV("gamma", HexStr(output.gamma.GetVch())); + outputs.push_back(output_obj); + } + result.pushKV("outputs", outputs); + + // Add fee + result.pushKV("fee", ValueFromAmount(unsigned_tx.GetFee())); + + return result; + }, + }; +} + Span GetBLSCTWalletRPCCommands() { static const CRPCCommand commands[]{ @@ -1123,6 +1760,22 @@ Span GetBLSCTWalletRPCCommands() {"blsct", &sendtokentoblsctaddress}, {"blsct", &stakelock}, {"blsct", &stakeunlock}, + {"blsct", &setblsctseed}, + {"blsct", &createblsctbalanceproof}, + {"blsct", &createblsctrawtransaction}, + {"blsct", &fundblsctrawtransaction}, + {"blsct", &signblsctrawtransaction}, + {"blsct", &decodeblsctrawtransaction}, }; return commands; +} + +void RegisterBLSCTUtilsRPCCommands(CRPCTable& t) +{ + static const CRPCCommand commands[]{ + {"blsct", &verifyblsctbalanceproof}, + }; + for (const auto& c : commands) { + t.appendCommand(c.name, &c); + } } \ No newline at end of file diff --git a/src/blsct/wallet/txfactory_global.h b/src/blsct/wallet/txfactory_global.h index 4bb95b5257826..c949da75a3932 100644 --- a/src/blsct/wallet/txfactory_global.h +++ b/src/blsct/wallet/txfactory_global.h @@ -66,12 +66,48 @@ struct UnsignedInput { Scalar gamma; PrivateKey sk; bool is_staked_commitment; + + template + void Serialize(Stream& s) const + { + ::Serialize(s, in); + ::Serialize(s, value); + ::Serialize(s, gamma); + ::Serialize(s, sk); + ::Serialize(s, is_staked_commitment); + } + + template + void Unserialize(Stream& s) + { + ::Unserialize(s, in); + ::Unserialize(s, value); + ::Unserialize(s, gamma); + ::Unserialize(s, sk); + ::Unserialize(s, is_staked_commitment); + } }; struct Amounts { CAmount nFromInputs; CAmount nFromOutputs; CAmount nFromFee; + + template + void Serialize(Stream& s) const + { + ::Serialize(s, nFromInputs); + ::Serialize(s, nFromOutputs); + ::Serialize(s, nFromFee); + } + + template + void Unserialize(Stream& s) + { + ::Unserialize(s, nFromInputs); + ::Unserialize(s, nFromOutputs); + ::Unserialize(s, nFromFee); + } }; CTransactionRef diff --git a/src/blsct/wallet/unsigned_transaction.cpp b/src/blsct/wallet/unsigned_transaction.cpp new file mode 100644 index 0000000000000..af0782ac359e2 --- /dev/null +++ b/src/blsct/wallet/unsigned_transaction.cpp @@ -0,0 +1,95 @@ +// Copyright (c) 2024 The Navio Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +namespace blsct { + +void UnsignedTransaction::AddInput(const UnsignedInput& input) +{ + m_inputs.push_back(input); +} + +void UnsignedTransaction::AddOutput(const UnsignedOutput& output) +{ + m_outputs.push_back(output); +} + +std::vector UnsignedTransaction::Serialize() const +{ + DataStream stream; + stream << *this; + return blsct::Common::DataStreamToVector(stream); +} + +std::optional UnsignedTransaction::Deserialize(const std::vector& data) +{ + try { + DataStream stream(data); + UnsignedTransaction tx; + stream >> tx; + return tx; + } catch (const std::exception& e) { + return std::nullopt; + } +} + +std::optional UnsignedTransaction::Sign() const +{ + try { + // Create a mutable transaction + CMutableTransaction tx; + tx.nVersion |= CTransaction::BLSCT_MARKER; + + // Add inputs + std::vector txSigs; + Scalar gammaAcc; + + // Add outputs first to calculate gamma + for (const auto& out : m_outputs) { + tx.vout.push_back(out.out); + auto outHash = out.out.GetHash(); + + if (out.out.HasBLSCTRangeProof()) { + gammaAcc = gammaAcc - out.gamma; + } + if (out.out.HasBLSCTKeys()) { + txSigs.push_back(PrivateKey(out.blindingKey).Sign(outHash)); + } + + if (out.type == TX_CREATE_TOKEN || out.type == TX_MINT_TOKEN) { + txSigs.push_back(PrivateKey(out.tokenKey).Sign(outHash)); + } + } + + // Add inputs and their signatures + for (const auto& in : m_inputs) { + tx.vin.push_back(in.in); + gammaAcc = gammaAcc + in.gamma; + txSigs.push_back(in.sk.Sign(in.in.GetHash())); + } + + // Calculate fee based on transaction weight + CAmount fee = m_fee; + + // Add fee output + CTxOut fee_out{fee, CScript(OP_RETURN)}; + auto feeKey = blsct::PrivateKey(Scalar::Rand()); + fee_out.predicate = blsct::PayFeePredicate(feeKey.GetPublicKey()).GetVch(); + tx.vout.push_back(fee_out); + + // Add balance and fee signatures + txSigs.push_back(PrivateKey(gammaAcc).SignBalance()); + txSigs.push_back(PrivateKey(feeKey).SignFee()); + + // Aggregate all signatures + tx.txSig = Signature::Aggregate(txSigs); + + return CTransaction(tx); + } catch (const std::exception& e) { + return std::nullopt; + } +} + +} // namespace blsct \ No newline at end of file diff --git a/src/blsct/wallet/unsigned_transaction.h b/src/blsct/wallet/unsigned_transaction.h new file mode 100644 index 0000000000000..8073aa9f30aa5 --- /dev/null +++ b/src/blsct/wallet/unsigned_transaction.h @@ -0,0 +1,55 @@ +// Copyright (c) 2024 The Navio Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BLSCT_UNSIGNED_TRANSACTION_H +#define BLSCT_UNSIGNED_TRANSACTION_H + +#include +#include +#include +#include +#include +#include +#include + +namespace blsct { + +class UnsignedTransaction +{ +private: + // Inputs and outputs + std::vector m_inputs; + std::vector m_outputs; + CAmount m_fee; + +public: + UnsignedTransaction() : m_fee(0){}; + + // Getters + const std::vector& GetInputs() const { return m_inputs; } + const std::vector& GetOutputs() const { return m_outputs; } + CAmount GetFee() const { return m_fee; } + + // Setters + void AddInput(const UnsignedInput& input); + void AddOutput(const UnsignedOutput& output); + void SetFee(CAmount fee) { m_fee = fee; } + + // Serialization + SERIALIZE_METHODS(UnsignedTransaction, obj) + { + READWRITE(obj.m_inputs, obj.m_outputs, obj.m_fee); + } + + // Serialization helpers + std::vector Serialize() const; + static std::optional Deserialize(const std::vector& data); + + // Signing + std::optional Sign() const; +}; + +} // namespace blsct + +#endif // BLSCT_UNSIGNED_TRANSACTION_H \ No newline at end of file diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp index 001cb2acf317b..55782013210d0 100644 --- a/src/kernel/chainparams.cpp +++ b/src/kernel/chainparams.cpp @@ -218,7 +218,7 @@ static CBlock CreateGenesisBlock(uint32_t nTime, uint32_t nNonce, uint32_t nBits base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x88, 0xAD, 0xE4}; bech32_hrp = "bc"; - bech32_mod_hrp = "nv"; + bech32_mod_hrp = "nav"; vFixedSeeds = std::vector(std::begin(chainparams_seed_main), std::end(chainparams_seed_main)); @@ -335,7 +335,7 @@ static CBlock CreateGenesisBlock(uint32_t nTime, uint32_t nNonce, uint32_t nBits base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x35, 0x83, 0x94}; bech32_hrp = "tb"; - bech32_mod_hrp = "tn"; + bech32_mod_hrp = "tnv"; vFixedSeeds = std::vector(std::begin(chainparams_seed_test), std::end(chainparams_seed_test)); @@ -465,7 +465,7 @@ static CBlock CreateGenesisBlock(uint32_t nTime, uint32_t nNonce, uint32_t nBits base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x35, 0x83, 0x94}; bech32_hrp = "tb"; - bech32_mod_hrp = "nv"; + bech32_mod_hrp = "nav"; fDefaultConsistencyChecks = false; m_is_mockable_chain = false; @@ -605,7 +605,7 @@ static CBlock CreateGenesisBlock(uint32_t nTime, uint32_t nNonce, uint32_t nBits base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x35, 0x83, 0x94}; bech32_hrp = "bcrt"; - bech32_mod_hrp = "nr"; + bech32_mod_hrp = "rnv"; } }; @@ -723,7 +723,7 @@ static CBlock CreateGenesisBlock(uint32_t nTime, uint32_t nNonce, uint32_t nBits base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x35, 0x83, 0x94}; bech32_hrp = "bcrt"; - bech32_mod_hrp = "nr"; + bech32_mod_hrp = "rnv"; } }; diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 441ac9d9fa01d..5408db4a5543d 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -29,6 +29,9 @@ class CRPCConvertParam */ static const CRPCConvertParam vRPCConvertParams[] = { + {"createblsctrawtransaction", 0, "inputs"}, + {"createblsctrawtransaction", 1, "outputs"}, + { "createblsctbalanceproof", 0, "amount" }, { "setmocktime", 0, "timestamp" }, { "mockscheduler", 0, "delta_time" }, { "utxoupdatepsbt", 1, "descriptors" }, diff --git a/src/rpc/mempool.cpp b/src/rpc/mempool.cpp index 04d2e68939853..526df1bb0c768 100644 --- a/src/rpc/mempool.cpp +++ b/src/rpc/mempool.cpp @@ -74,7 +74,7 @@ static RPCHelpMan sendrawtransaction() } for (const auto& out : mtx.vout) { - if((out.scriptPubKey.IsUnspendable() || !out.scriptPubKey.HasValidOps()) && out.nValue > max_burn_amount) { + if ((out.scriptPubKey.IsUnspendable() || !out.scriptPubKey.HasValidOps()) && out.nValue > max_burn_amount && !out.IsFee()) { throw JSONRPCTransactionError(TransactionError::MAX_BURN_EXCEEDED); } } diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 3f1b85f0213eb..6d30bc0d3cbd3 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -125,9 +125,9 @@ static std::vector DecodeTxDoc(const std::string& txid_field_doc) {RPCResult::Type::STR_HEX, "ephemeralKey", /*optional=*/true, "hex-encoded ephemeral key"}, {RPCResult::Type::STR_HEX, "spendingKey", /*optional=*/true, "hex-encoded spending key"}, {RPCResult::Type::OBJ, "rangeProof", /*optional=*/true, "output's range proof", { - {RPCResult::Type::ARR, "Vs", "Vs", { - {RPCResult::Type::STR_HEX, "", "hex-encoded point (if any)"}, - }}, + {RPCResult::Type::ARR, "Vs", true, "Vs", { + {RPCResult::Type::STR_HEX, "", "hex-encoded point (if any)"}, + }}, {RPCResult::Type::ARR, "Ls", /*optional=*/true, "Ls", { {RPCResult::Type::STR_HEX, "", "hex-encoded point (if any)"}, }}, diff --git a/src/rpc/register.h b/src/rpc/register.h index 54d9b2780d7ed..f20c19d4282b5 100644 --- a/src/rpc/register.h +++ b/src/rpc/register.h @@ -21,7 +21,7 @@ void RegisterSignMessageRPCCommands(CRPCTable&); void RegisterSignerRPCCommands(CRPCTable &tableRPC); void RegisterTxoutProofRPCCommands(CRPCTable&); void RegisterTokenRPCCommands(CRPCTable&); - +void RegisterBLSCTUtilsRPCCommands(CRPCTable&); static inline void RegisterAllCoreRPCCommands(CRPCTable &t) { RegisterBlockchainRPCCommands(t); @@ -38,6 +38,7 @@ static inline void RegisterAllCoreRPCCommands(CRPCTable &t) #endif // ENABLE_EXTERNAL_SIGNER RegisterTxoutProofRPCCommands(t); RegisterTokenRPCCommands(t); + RegisterBLSCTUtilsRPCCommands(t); } #endif // BITCOIN_RPC_REGISTER_H diff --git a/src/wallet/rpc/wallet.cpp b/src/wallet/rpc/wallet.cpp index 489cb33feb1d2..4fdab6db61276 100644 --- a/src/wallet/rpc/wallet.cpp +++ b/src/wallet/rpc/wallet.cpp @@ -107,12 +107,20 @@ static RPCHelpMan getwalletinfo() } obj.pushKV("keypoolsize", (int64_t)kpExternalSize); - LegacyScriptPubKeyMan* spk_man = pwallet->GetLegacyScriptPubKeyMan(); - if (spk_man) { - CKeyID seed_id = spk_man->GetHDChain().seed_id; + auto blsct_km = pwallet->GetBLSCTKeyMan(); + if (blsct_km) { + CKeyID seed_id = blsct_km->GetHDChain().seed_id; if (!seed_id.IsNull()) { obj.pushKV("hdseedid", seed_id.GetHex()); } + } else { + LegacyScriptPubKeyMan* spk_man = pwallet->GetLegacyScriptPubKeyMan(); + if (spk_man) { + CKeyID seed_id = spk_man->GetHDChain().seed_id; + if (!seed_id.IsNull()) { + obj.pushKV("hdseedid", seed_id.GetHex()); + } + } } if (pwallet->CanSupportFeature(FEATURE_HD_SPLIT)) { @@ -519,72 +527,69 @@ static RPCHelpMan unloadwallet() static RPCHelpMan sethdseed() { - return RPCHelpMan{"sethdseed", - "\nSet or generate a new HD wallet seed. Non-HD wallets will not be upgraded to being a HD wallet. Wallets that are already\n" - "HD will have a new HD seed set so that new keys added to the keypool will be derived from this new seed.\n" - "\nNote that you will need to MAKE A NEW BACKUP of your wallet after setting the HD wallet seed." + HELP_REQUIRING_PASSPHRASE + - "Note: This command is only compatible with legacy wallets.\n", - { - {"newkeypool", RPCArg::Type::BOOL, RPCArg::Default{true}, "Whether to flush old unused addresses, including change addresses, from the keypool and regenerate it.\n" - "If true, the next address from getnewaddress and change address from getrawchangeaddress will be from this new seed.\n" - "If false, addresses (including change addresses if the wallet already had HD Chain Split enabled) from the existing\n" - "keypool will be used until it has been depleted."}, - {"seed", RPCArg::Type::STR, RPCArg::DefaultHint{"random seed"}, "The WIF private key to use as the new HD seed.\n" - "The seed value can be retrieved using the dumpwallet command. It is the private key marked hdseed=1"}, - }, - RPCResult{RPCResult::Type::NONE, "", ""}, - RPCExamples{ - HelpExampleCli("sethdseed", "") - + HelpExampleCli("sethdseed", "false") - + HelpExampleCli("sethdseed", "true \"wifkey\"") - + HelpExampleRpc("sethdseed", "true, \"wifkey\"") - }, - [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue -{ - std::shared_ptr const pwallet = GetWalletForJSONRPCRequest(request); - if (!pwallet) return UniValue::VNULL; + return RPCHelpMan{ + "sethdseed", + "\nSet or generate a new HD wallet seed. Non-HD wallets will not be upgraded to being a HD wallet. Wallets that are already\n" + "HD will have a new HD seed set so that new keys added to the keypool will be derived from this new seed.\n" + "\nNote that you will need to MAKE A NEW BACKUP of your wallet after setting the HD wallet seed." + + HELP_REQUIRING_PASSPHRASE + + "Note: This command is only compatible with legacy wallets.\n", + { + {"newkeypool", RPCArg::Type::BOOL, RPCArg::Default{true}, "Whether to flush old unused addresses, including change addresses, from the keypool and regenerate it.\n" + "If true, the next address from getnewaddress and change address from getrawchangeaddress will be from this new seed.\n" + "If false, addresses (including change addresses if the wallet already had HD Chain Split enabled) from the existing\n" + "keypool will be used until it has been depleted."}, + {"seed", RPCArg::Type::STR, RPCArg::DefaultHint{"random seed"}, "The WIF private key to use as the new HD seed.\n" + "The seed value can be retrieved using the dumpwallet command. It is the private key marked hdseed=1"}, + }, + RPCResult{RPCResult::Type::NONE, "", ""}, + RPCExamples{ + HelpExampleCli("sethdseed", "") + HelpExampleCli("sethdseed", "false") + HelpExampleCli("sethdseed", "true \"wifkey\"") + HelpExampleRpc("sethdseed", "true, \"wifkey\"")}, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { + std::shared_ptr const pwallet = GetWalletForJSONRPCRequest(request); + if (!pwallet) return UniValue::VNULL; - LegacyScriptPubKeyMan& spk_man = EnsureLegacyScriptPubKeyMan(*pwallet, true); + LegacyScriptPubKeyMan& spk_man = EnsureLegacyScriptPubKeyMan(*pwallet, true); - if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { - throw JSONRPCError(RPC_WALLET_ERROR, "Cannot set a HD seed to a wallet with private keys disabled"); - } + if (pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Cannot set a HD seed to a wallet with private keys disabled"); + } - LOCK2(pwallet->cs_wallet, spk_man.cs_KeyStore); + LOCK2(pwallet->cs_wallet, spk_man.cs_KeyStore); - // Do not do anything to non-HD wallets - if (!pwallet->CanSupportFeature(FEATURE_HD)) { - throw JSONRPCError(RPC_WALLET_ERROR, "Cannot set an HD seed on a non-HD wallet. Use the upgradewallet RPC in order to upgrade a non-HD wallet to HD"); - } + // Do not do anything to non-HD wallets + if (!pwallet->CanSupportFeature(FEATURE_HD)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Cannot set an HD seed on a non-HD wallet. Use the upgradewallet RPC in order to upgrade a non-HD wallet to HD"); + } - EnsureWalletIsUnlocked(*pwallet); + EnsureWalletIsUnlocked(*pwallet); - bool flush_key_pool = true; - if (!request.params[0].isNull()) { - flush_key_pool = request.params[0].get_bool(); - } + bool flush_key_pool = true; + if (!request.params[0].isNull()) { + flush_key_pool = request.params[0].get_bool(); + } - CPubKey master_pub_key; - if (request.params[1].isNull()) { - master_pub_key = spk_man.GenerateNewSeed(); - } else { - CKey key = DecodeSecret(request.params[1].get_str()); - if (!key.IsValid()) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key"); - } + CPubKey master_pub_key; + if (request.params[1].isNull()) { + master_pub_key = spk_man.GenerateNewSeed(); + } else { + CKey key = DecodeSecret(request.params[1].get_str()); + if (!key.IsValid()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid private key"); + } - if (HaveKey(spk_man, key)) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Already have this key (either as an HD seed or as a loose private key)"); - } + if (HaveKey(spk_man, key)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Already have this key (either as an HD seed or as a loose private key)"); + } - master_pub_key = spk_man.DeriveNewSeed(key); - } + master_pub_key = spk_man.DeriveNewSeed(key); + } - spk_man.SetHDSeed(master_pub_key); - if (flush_key_pool) spk_man.NewKeyPool(); + spk_man.SetHDSeed(master_pub_key); + if (flush_key_pool) spk_man.NewKeyPool(); - return UniValue::VNULL; -}, + return UniValue::VNULL; + }, }; } diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index f14bf4aa795b3..e1b25335e3107 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -2763,6 +2763,13 @@ util::Result CWallet::GetNewChangeDestination(const OutputType t std::optional CWallet::GetOldestKeyPoolTime() const { LOCK(cs_wallet); + if (IsWalletFlagSet(WALLET_FLAG_BLSCT)) { + auto blsct_km = GetBLSCTKeyMan(); + if (blsct_km) { + return blsct_km->GetOldestSubAddressPoolTime(0); + } + } + if (m_spk_managers.empty()) { return std::nullopt; } diff --git a/test/functional/blsct_balance_proof.py b/test/functional/blsct_balance_proof.py new file mode 100755 index 0000000000000..0e971407fe7fc --- /dev/null +++ b/test/functional/blsct_balance_proof.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# Copyright (c) 2024 The Navio Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, +) + +class NavioBlsctBalanceProofTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser, blsct=True) + + def set_test_params(self): + # Set up two nodes for the test + self.num_nodes = 2 + self.chain = 'blsctregtest' + self.setup_clean_chain = True + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def run_test(self): + self.log.info("Creating wallet1 with BLSCT") + + # Create a new wallet + self.nodes[0].createwallet(wallet_name="wallet1", blsct=True) + self.nodes[1].createwallet(wallet_name="wallet1", blsct=True) + wallet = self.nodes[0].get_wallet_rpc("wallet1") + wallet_2 = self.nodes[1].get_wallet_rpc("wallet1") + + self.log.info("Loading wallet1") + + # Ensure wallet is loaded + wallets = self.nodes[0].listwallets() + assert "wallet1" in wallets, "wallet1 was not loaded successfully" + + self.log.info("Generating BLSCT address") + + # Generate a BLSCT address + blsct_address = wallet.getnewaddress(label="", address_type="blsct") + blsct_address_2 = wallet_2.getnewaddress(label="", address_type="blsct") + + self.log.info(f"BLSCT address NODE 1: {blsct_address}") + self.log.info(f"BLSCT address NODE 2: {blsct_address_2}") + + # Generate blocks and fund the BLSCT address + self.log.info(f"Generating 101 blocks to the BLSCT address {blsct_address}") + block_hashes = self.generatetoblsctaddress(self.nodes[0], 101, blsct_address) + + self.log.info(f"Generated blocks: {len(block_hashes)}") + + # Check the balance of the wallet + balance = wallet.getbalance() + self.log.info(f"Balance in wallet1: {balance}") + + assert_equal(len(block_hashes), 101) + assert balance > 0, "Balance should be greater than zero after mining" + + # Test creating a balance proof + self.log.info("Testing createblsctbalanceproof") + + # Create a valid proof + proof_result = wallet.createblsctbalanceproof(balance / 2) + assert "proof" in proof_result, "Proof not found in result" + proof_hex = proof_result["proof"] + + # Test verifying the proof + self.log.info("Testing verifyblsctbalanceproof") + + # Test with invalid proof format + assert_raises_rpc_error(-8, "Invalid proof format", wallet.verifyblsctbalanceproof, "invalid") + + # Test with modified proof (corrupt the hex string) + if len(proof_hex) > 2: + # Modify the last character of the hex string + modified_proof = proof_hex[::-1] + assert_raises_rpc_error(-8, "Invalid proof format",wallet_2.verifyblsctbalanceproof, modified_proof) + + # Verify the valid proof + verify_result = wallet_2.verifyblsctbalanceproof(proof_hex) + assert "valid" in verify_result, "Valid field not found in result" + assert "min_amount" in verify_result, "Min amount field not found in result" + assert verify_result["valid"], "Proof should be valid" + assert_equal(verify_result["min_amount"], balance / 2) + + +if __name__ == '__main__': + NavioBlsctBalanceProofTest().main() \ No newline at end of file diff --git a/test/functional/blsct_rawtransaction.py b/test/functional/blsct_rawtransaction.py new file mode 100755 index 0000000000000..3455cbebfbb3d --- /dev/null +++ b/test/functional/blsct_rawtransaction.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +# Copyright (c) 2024 The Navio Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +"""Test the BLSCT raw transaction RPC methods.""" + +from decimal import Decimal +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, + assert_greater_than, +) +from test_framework.messages import COIN + + +class BLSCTRawTransactionTest(BitcoinTestFramework): + def add_options(self, parser): + self.add_wallet_options(parser, blsct=True) + + def set_test_params(self): + self.num_nodes = 2 + self.chain = 'blsctregtest' + self.setup_clean_chain = True + + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def setup_network(self): + self.setup_nodes() + self.connect_nodes(0, 1) + + def run_test(self): + self.log.info("Setting up wallets and generating initial blocks") + + # Create BLSCT wallets + self.nodes[0].createwallet(wallet_name="wallet1", blsct=True) + self.nodes[1].createwallet(wallet_name="wallet2", blsct=True) + + wallet1 = self.nodes[0].get_wallet_rpc("wallet1") + wallet2 = self.nodes[1].get_wallet_rpc("wallet2") + + # Generate BLSCT addresses + address1 = wallet1.getnewaddress(label="", address_type="blsct") + address2 = wallet2.getnewaddress(label="", address_type="blsct") + + self.log.info(f"Address 1: {address1}") + self.log.info(f"Address 2: {address2}") + + # Generate blocks to fund the first wallet + self.log.info("Generating 101 blocks to fund wallet1") + block_hashes = self.generatetoblsctaddress(self.nodes[0], 101, address1) + + # Check initial balance + balance1 = wallet1.getbalance() + self.log.info(f"Initial balance in wallet1: {balance1}") + + # Test the three RPC methods + self.test_createblsctrawtransaction(wallet1, wallet2, address1, address2) + self.test_fundblsctrawtransaction(wallet1, wallet2, address1, address2) + self.test_signblsctrawtransaction(wallet1, wallet2, address1, address2) + self.test_decodeblsctrawtransaction(wallet1, wallet2, address1, address2) + self.test_integration_workflow(wallet1, wallet2, address1, address2) + + def test_createblsctrawtransaction(self, wallet1, wallet2, address1, address2): + """Test createblsctrawtransaction RPC method""" + self.log.info("Testing createblsctrawtransaction") + + # Get some unspent outputs + unspent = wallet1.listblsctunspent() + assert_greater_than(len(unspent), 0) + + utxo = unspent[0] + self.log.info(f"Using UTXO: {utxo['txid']}:{utxo['vout']}") + + # Test 1: Create raw transaction with minimal inputs (wallet will fill missing data) + inputs = [{"txid": utxo['txid'], "vout": utxo['vout']}] + outputs = [{"address": address2, "amount": 0.1, "memo": "Test transaction"}] + + raw_tx = wallet1.createblsctrawtransaction(inputs, outputs) + self.log.info(f"Created raw transaction: {raw_tx[:100]}...") + + # Test 2: Create raw transaction with all optional fields provided + inputs_with_data = [{ + "txid": utxo['txid'], + "vout": utxo['vout'], + "value": int(utxo['amount'] * COIN), + "is_staked_commitment": False + }] + + raw_tx_with_data = wallet1.createblsctrawtransaction(inputs_with_data, outputs) + + # Test 3: Create raw transaction with multiple outputs + outputs_multi = [ + {"address": address2, "amount": 0.05, "memo": "First output"}, + {"address": address1, "amount": 0.03, "memo": "Second output"} + ] + + raw_tx_multi = wallet1.createblsctrawtransaction(inputs, outputs_multi) + + # Test 4: Error cases + # Invalid address + outputs_invalid = [{"address": "invalid_address", "amount": 0.1}] + assert_raises_rpc_error(-5, "Invalid BLSCT address", + wallet1.createblsctrawtransaction, inputs, outputs_invalid) + + # Negative amount + outputs_negative = [{"address": address2, "amount": -0.1}] + assert_raises_rpc_error(-3, "Amount out of range", + wallet1.createblsctrawtransaction, inputs, outputs_negative) + + self.log.info("createblsctrawtransaction tests passed") + + def test_fundblsctrawtransaction(self, wallet1, wallet2, address1, address2): + """Test fundblsctrawtransaction RPC method""" + self.log.info("Testing fundblsctrawtransaction") + + # Create a raw transaction with insufficient inputs + inputs = [] # No inputs + outputs = [{"address": address2, "amount": 0.1, "memo": "Test funding"}] + + raw_tx = wallet1.createblsctrawtransaction(inputs, outputs) + + # Fund the transaction + funded_tx = wallet1.fundblsctrawtransaction(raw_tx) + self.log.info(f"Funded transaction: {funded_tx[:100]}...") + + # Verify the funded transaction is different from the original + assert funded_tx != raw_tx, "Funded transaction should be different from original" + + # Test with custom change address + change_address = wallet2.getnewaddress(label="", address_type="blsct") + funded_tx_with_change = wallet1.fundblsctrawtransaction(raw_tx, change_address) + + # Test error cases + # Invalid hex string + assert_raises_rpc_error(-22, "Transaction deserialization faile", + wallet1.fundblsctrawtransaction, "invalid_hex") + + # Invalid change address + assert_raises_rpc_error(-5, "Invalid BLSCT change address", + wallet1.fundblsctrawtransaction, raw_tx, "invalid_address") + + # Test with insufficient funds (create a transaction larger than available balance) + balance = wallet1.getbalance() + large_outputs = [{"address": address2, "amount": balance + 1, "memo": "Too much"}] + large_raw_tx = wallet1.createblsctrawtransaction(inputs, large_outputs) + + assert_raises_rpc_error(-6, "Insufficient funds", + wallet1.fundblsctrawtransaction, large_raw_tx) + + self.log.info("fundblsctrawtransaction tests passed") + + def test_signblsctrawtransaction(self, wallet1, wallet2, address1, address2): + """Test signblsctrawtransaction RPC method""" + self.log.info("Testing signblsctrawtransaction") + + # Create and fund a raw transaction + inputs = [] + outputs = [{"address": address2, "amount": 0.1, "memo": "Test signing"}] + + raw_tx = wallet1.createblsctrawtransaction(inputs, outputs) + funded_tx = wallet1.fundblsctrawtransaction(raw_tx) + + # Sign the transaction + signed_tx = wallet1.signblsctrawtransaction(funded_tx) + self.log.info(f"Signed transaction: {signed_tx[:100]}...") + + # Verify the signed transaction is different from the funded transaction + assert signed_tx != funded_tx, "Signed transaction should be different from funded transaction" + + self.log.info("signblsctrawtransaction tests passed") + + def test_decodeblsctrawtransaction(self, wallet1, wallet2, address1, address2): + """Test decodeblsctrawtransaction RPC method""" + self.log.info("Testing decodeblsctrawtransaction") + + # Get some unspent outputs + unspent = wallet1.listblsctunspent() + assert_greater_than(len(unspent), 0) + + utxo = unspent[0] + self.log.info(f"Using UTXO: {utxo['txid']}:{utxo['vout']}") + + # Test 1: Decode a raw transaction + raw_tx = wallet1.createblsctrawtransaction([{"txid": utxo['txid'], "vout": utxo['vout']}], []) + decoded_tx = wallet1.decodeblsctrawtransaction(raw_tx) + self.log.info(f"Decoded transaction: {decoded_tx}") + + # Test 2: Error cases + # Invalid hex string + assert_raises_rpc_error(-22, "Transaction deserialization faile", + wallet1.decodeblsctrawtransaction, "invalid_hex") + + self.log.info("decodeblsctrawtransaction tests passed") + + def test_integration_workflow(self, wallet1, wallet2, address1, address2): + """Test the complete workflow: create -> fund -> sign -> broadcast""" + self.log.info("Testing complete BLSCT raw transaction workflow") + + # Step 1: Create raw transaction + inputs = [] + outputs = [{"address": address2, "amount": 0.05, "memo": "Integration test"}] + + raw_tx = wallet1.createblsctrawtransaction(inputs, outputs) + self.log.info("Step 1: Created raw transaction") + + # Step 2: Fund the transaction + funded_tx = wallet1.fundblsctrawtransaction(raw_tx) + self.log.info("Step 2: Funded transaction") + + # Step 3: Sign the transaction + signed_tx = wallet1.signblsctrawtransaction(funded_tx) + self.log.info("Step 3: Signed transaction") + + initial_balance2 = wallet2.getbalance() + self.log.info(f"Initial balance in wallet2: {initial_balance2}") + + # Step 4: Broadcast the transaction + txid = self.nodes[0].sendrawtransaction(signed_tx) + self.log.info(f"Step 4: Broadcasted transaction with txid: {txid}") + + # Step 5: Mine a block to confirm the transaction + block_hashes = self.generatetoblsctaddress(self.nodes[0], 1, address1) + self.log.info(f"Step 5: Mined block: {block_hashes[0]}") + + # Step 7: Check that the recipient received the funds + balance2 = wallet2.getbalance() + self.log.info(f"Final balance in wallet2: {balance2}") + assert_greater_than(balance2, initial_balance2) + + self.log.info("Complete BLSCT raw transaction workflow test passed") + + +if __name__ == '__main__': + BLSCTRawTransactionTest().main() \ No newline at end of file diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index bf7ba7a5b4ca6..d9d3b68a486a0 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -397,7 +397,8 @@ 'p2p_dandelionpp_mempool_leak.py', 'p2p_dandelionpp_probing.py', 'blsct_token.py', - 'blsct_nft.py' + 'blsct_nft.py', + 'blsct_balance_proof.py' # Don't append tests at the end to avoid merge conflicts # Put them in a random line within the section that fits their approximate run-time ] From c20f3322ae81e98d046cd4ecc392c9e009975efc Mon Sep 17 00:00:00 2001 From: alex v Date: Tue, 24 Jun 2025 18:18:38 +0200 Subject: [PATCH 03/20] Add BLSCT Raw Transaction and Balance Proof RPC Methods --- src/blsct/wallet/rpc.cpp | 10 +++++----- test/functional/blsct_rawtransaction.py | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/blsct/wallet/rpc.cpp b/src/blsct/wallet/rpc.cpp index eed60064a5c23..d05d3249956b4 100644 --- a/src/blsct/wallet/rpc.cpp +++ b/src/blsct/wallet/rpc.cpp @@ -1672,10 +1672,10 @@ RPCHelpMan signblsctrawtransaction() }; } -RPCHelpMan decodeblsctrawtransaction() +RPCHelpMan decodeblsctrawunsignedtransaction() { return RPCHelpMan{ - "decodeblsctrawtransaction", + "decodeblsctrawunsignedtransaction", "\nDecode a BLSCT raw transaction and return a JSON object describing the transaction structure.\n", { {"hexstring", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction hex string"}, @@ -1686,7 +1686,7 @@ RPCHelpMan decodeblsctrawtransaction() {RPCResult::Type::OBJ, "", "", { {RPCResult::Type::STR_HEX, "txid", "The transaction id"}, {RPCResult::Type::NUM, "vout", "The output number"}, - {RPCResult::Type::NUM, "value", "The input value in satoshis"}, + {RPCResult::Type::NUM, "value", "The input value"}, {RPCResult::Type::STR_HEX, "gamma", "The gamma value (hex string)"}, {RPCResult::Type::BOOL, "is_staked_commitment", "Whether this input is a staked commitment"}, }}, @@ -1700,7 +1700,7 @@ RPCHelpMan decodeblsctrawtransaction() }}, {RPCResult::Type::STR_AMOUNT, "fee", "The transaction fee in " + CURRENCY_UNIT}, }}, - RPCExamples{HelpExampleCli("decodeblsctrawtransaction", "\"hexstring\"") + HelpExampleRpc("decodeblsctrawtransaction", "\"hexstring\"")}, + RPCExamples{HelpExampleCli("decodeblsctrawunsignedtransaction", "\"hexstring\"") + HelpExampleRpc("decodeblsctrawunsignedtransaction", "\"hexstring\"")}, [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { std::vector tx_data = ParseHex(request.params[0].get_str()); auto unsigned_tx_opt = blsct::UnsignedTransaction::Deserialize(tx_data); @@ -1765,7 +1765,7 @@ Span GetBLSCTWalletRPCCommands() {"blsct", &createblsctrawtransaction}, {"blsct", &fundblsctrawtransaction}, {"blsct", &signblsctrawtransaction}, - {"blsct", &decodeblsctrawtransaction}, + {"blsct", &decodeblsctrawunsignedtransaction}, }; return commands; } diff --git a/test/functional/blsct_rawtransaction.py b/test/functional/blsct_rawtransaction.py index 3455cbebfbb3d..825304e972d40 100755 --- a/test/functional/blsct_rawtransaction.py +++ b/test/functional/blsct_rawtransaction.py @@ -60,7 +60,7 @@ def run_test(self): self.test_createblsctrawtransaction(wallet1, wallet2, address1, address2) self.test_fundblsctrawtransaction(wallet1, wallet2, address1, address2) self.test_signblsctrawtransaction(wallet1, wallet2, address1, address2) - self.test_decodeblsctrawtransaction(wallet1, wallet2, address1, address2) + self.test_decodeblsctrawunsignedtransaction(wallet1, wallet2, address1, address2) self.test_integration_workflow(wallet1, wallet2, address1, address2) def test_createblsctrawtransaction(self, wallet1, wallet2, address1, address2): @@ -172,9 +172,9 @@ def test_signblsctrawtransaction(self, wallet1, wallet2, address1, address2): self.log.info("signblsctrawtransaction tests passed") - def test_decodeblsctrawtransaction(self, wallet1, wallet2, address1, address2): - """Test decodeblsctrawtransaction RPC method""" - self.log.info("Testing decodeblsctrawtransaction") + def test_decodeblsctrawunsignedtransaction(self, wallet1, wallet2, address1, address2): + """Test decodeblsctrawunsignedtransaction RPC method""" + self.log.info("Testing decodeblsctrawunsignedtransaction") # Get some unspent outputs unspent = wallet1.listblsctunspent() @@ -185,15 +185,15 @@ def test_decodeblsctrawtransaction(self, wallet1, wallet2, address1, address2): # Test 1: Decode a raw transaction raw_tx = wallet1.createblsctrawtransaction([{"txid": utxo['txid'], "vout": utxo['vout']}], []) - decoded_tx = wallet1.decodeblsctrawtransaction(raw_tx) + decoded_tx = wallet1.decodeblsctrawunsignedtransaction(raw_tx) self.log.info(f"Decoded transaction: {decoded_tx}") # Test 2: Error cases # Invalid hex string assert_raises_rpc_error(-22, "Transaction deserialization faile", - wallet1.decodeblsctrawtransaction, "invalid_hex") + wallet1.decodeblsctrawunsignedtransaction, "invalid_hex") - self.log.info("decodeblsctrawtransaction tests passed") + self.log.info("decodeblsctrawunsignedtransaction tests passed") def test_integration_workflow(self, wallet1, wallet2, address1, address2): """Test the complete workflow: create -> fund -> sign -> broadcast""" From 521f0f5d6fbfff91f4a58e332743a6038a3accda Mon Sep 17 00:00:00 2001 From: alex v Date: Tue, 24 Jun 2025 20:27:49 +0200 Subject: [PATCH 04/20] add getblsctrecoverydata rpc method --- src/blsct/wallet/rpc.cpp | 157 +++++++++++++++++++++++- test/functional/blsct_rawtransaction.py | 107 ++++++++++++++-- 2 files changed, 251 insertions(+), 13 deletions(-) diff --git a/src/blsct/wallet/rpc.cpp b/src/blsct/wallet/rpc.cpp index d05d3249956b4..fd6ff3c956a67 100644 --- a/src/blsct/wallet/rpc.cpp +++ b/src/blsct/wallet/rpc.cpp @@ -1623,9 +1623,8 @@ RPCHelpMan fundblsctrawtransaction() blsct::UnsignedOutput change_output = CreateOutput(change_subaddr.GetKeys(), change_value, "", TokenId(), Scalar::Rand()); unsigned_tx.AddOutput(change_output); } - - return HexStr(unsigned_tx.Serialize()); } + return HexStr(unsigned_tx.Serialize()); }, }; } @@ -1672,10 +1671,10 @@ RPCHelpMan signblsctrawtransaction() }; } -RPCHelpMan decodeblsctrawunsignedtransaction() +RPCHelpMan decodeblsctrawtransaction() { return RPCHelpMan{ - "decodeblsctrawunsignedtransaction", + "decodeblsctrawtransaction", "\nDecode a BLSCT raw transaction and return a JSON object describing the transaction structure.\n", { {"hexstring", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction hex string"}, @@ -1700,7 +1699,7 @@ RPCHelpMan decodeblsctrawunsignedtransaction() }}, {RPCResult::Type::STR_AMOUNT, "fee", "The transaction fee in " + CURRENCY_UNIT}, }}, - RPCExamples{HelpExampleCli("decodeblsctrawunsignedtransaction", "\"hexstring\"") + HelpExampleRpc("decodeblsctrawunsignedtransaction", "\"hexstring\"")}, + RPCExamples{HelpExampleCli("decodeblsctrawtransaction", "\"hexstring\"") + HelpExampleRpc("decodeblsctrawtransaction", "\"hexstring\"")}, [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { std::vector tx_data = ParseHex(request.params[0].get_str()); auto unsigned_tx_opt = blsct::UnsignedTransaction::Deserialize(tx_data); @@ -1728,9 +1727,35 @@ RPCHelpMan decodeblsctrawunsignedtransaction() UniValue outputs(UniValue::VARR); for (const auto& output : unsigned_tx.GetOutputs()) { UniValue output_obj(UniValue::VOBJ); + + // Decode the address from the CTxOut + CTxDestination destination; + if (ExtractDestination(output.out.scriptPubKey, destination)) { + output_obj.pushKV("address", EncodeDestination(destination)); + } else { + output_obj.pushKV("address", ""); + } + output_obj.pushKV("amount", ValueFromAmount(output.value.GetUint64())); + + // Extract memo from script if possible + std::string memo = ""; + if (output.out.scriptPubKey.IsUnspendable()) { + // Try to extract memo from OP_RETURN + std::vector data; + if (output.out.scriptPubKey.size() > 1 && output.out.scriptPubKey[0] == OP_RETURN) { + data.assign(output.out.scriptPubKey.begin() + 1, output.out.scriptPubKey.end()); + memo = std::string(data.begin(), data.end()); + } + } + output_obj.pushKV("memo", memo); + + // Token ID would need to be extracted from the token_key or other fields + output_obj.pushKV("token_id", ""); + output_obj.pushKV("blinding_key", HexStr(output.blindingKey.GetVch())); output_obj.pushKV("gamma", HexStr(output.gamma.GetVch())); + output_obj.pushKV("token_key", HexStr(output.tokenKey.GetVch())); outputs.push_back(output_obj); } result.pushKV("outputs", outputs); @@ -1743,6 +1768,125 @@ RPCHelpMan decodeblsctrawunsignedtransaction() }; } +static RPCHelpMan getblsctrecoverydata() +{ + return RPCHelpMan{ + "getblsctrecoverydata", + "\nGet BLSCT recovery data for transaction output(s)\n", + { + {"txid_or_hex", RPCArg::Type::STR, RPCArg::Optional::NO, "The transaction id or raw transaction hex"}, + {"vout", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "The output index. If omitted, shows data for all outputs."}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", { + {RPCResult::Type::ARR, "outputs", "Array of outputs with recovery data", { + {RPCResult::Type::OBJ, "", "", { + {RPCResult::Type::NUM, "vout", "Output index"}, + {RPCResult::Type::STR_AMOUNT, "amount", "The recovered amount in " + CURRENCY_UNIT}, + {RPCResult::Type::STR_HEX, "gamma", "The gamma value (hex string)"}, + {RPCResult::Type::STR, "message", "The memo/message associated with this output"}, + }}, + }}, + }}, + RPCExamples{HelpExampleCli("getblsctrecoverydata", "\"mytxid\"") + HelpExampleCli("getblsctrecoverydata", "\"mytxid\" 1") + HelpExampleRpc("getblsctrecoverydata", "\"mytxid\", 1")}, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { + std::shared_ptr const wallet = wallet::GetWalletForJSONRPCRequest(request); + if (!wallet) return NullUniValue; + + LOCK(wallet->cs_wallet); + + CMutableTransaction mtx; + uint256 hash; + bool is_hex_input = false; + + // Parse input as either txid or hex + std::string input = request.params[0].get_str(); + if (input.length() == 64) { + hash = uint256S(input); + auto tx = wallet->GetWalletTx(hash); + if (!tx) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Transaction not found in wallet"); + } + mtx = CMutableTransaction(*tx->tx); + } else { + if (!DecodeHexTx(mtx, input)) { + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "Transaction decode failed"); + } + hash = mtx.GetHash(); + is_hex_input = true; + } + + int specific_vout = -1; + if (!request.params[1].isNull()) { + specific_vout = request.params[1].getInt(); + if (specific_vout < 0 || specific_vout >= (int)mtx.vout.size()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "vout index out of range"); + } + } + + UniValue result(UniValue::VOBJ); + UniValue outputs(UniValue::VARR); + + if (is_hex_input) { + // For hex input, use BLSCT key manager to recover outputs + auto blsct_km = wallet->GetOrCreateBLSCTKeyMan(); + + for (size_t i = 0; i < mtx.vout.size(); i++) { + if (specific_vout != -1 && specific_vout != (int)i) { + continue; + } + + const CTxOut& out = mtx.vout[i]; + UniValue output(UniValue::VOBJ); + output.pushKV("vout", (int)i); + + // Use RecoverOutputs for hex input + auto result = blsct_km->RecoverOutputs({out}); + if (!out.HasBLSCTRangeProof()) { + // If recovery failed, show what we can + output.pushKV("amount", ValueFromAmount(out.nValue)); + output.pushKV("gamma", ""); + output.pushKV("message", ""); + outputs.push_back(output); + } else if (result.is_completed && !result.amounts.empty()) { + auto recovery_data = result.amounts[0]; + output.pushKV("amount", ValueFromAmount(recovery_data.amount)); + output.pushKV("gamma", HexStr(recovery_data.gamma.GetVch())); + output.pushKV("message", recovery_data.message); + outputs.push_back(output); + } + } + } else { + // For txid input, use wallet transaction + auto wallet_tx = wallet->GetWalletTx(hash); + if (!wallet_tx) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Transaction not found in wallet"); + } + + for (size_t i = 0; i < mtx.vout.size(); i++) { + if (specific_vout != -1 && specific_vout != (int)i) { + continue; + } + + UniValue output(UniValue::VOBJ); + output.pushKV("vout", (int)i); + + // Get recovery data from wallet transaction + auto recovery_data = wallet_tx->GetBLSCTRecoveryData(i); + output.pushKV("amount", ValueFromAmount(recovery_data.amount)); + output.pushKV("gamma", HexStr(recovery_data.gamma.GetVch())); + output.pushKV("message", recovery_data.message); + + outputs.push_back(output); + } + } + + result.pushKV("outputs", outputs); + return result; + }, + }; +} + Span GetBLSCTWalletRPCCommands() { static const CRPCCommand commands[]{ @@ -1765,7 +1909,8 @@ Span GetBLSCTWalletRPCCommands() {"blsct", &createblsctrawtransaction}, {"blsct", &fundblsctrawtransaction}, {"blsct", &signblsctrawtransaction}, - {"blsct", &decodeblsctrawunsignedtransaction}, + {"blsct", &decodeblsctrawtransaction}, + {"blsct", &getblsctrecoverydata}, }; return commands; } diff --git a/test/functional/blsct_rawtransaction.py b/test/functional/blsct_rawtransaction.py index 825304e972d40..22e8b0f42fa77 100755 --- a/test/functional/blsct_rawtransaction.py +++ b/test/functional/blsct_rawtransaction.py @@ -60,7 +60,8 @@ def run_test(self): self.test_createblsctrawtransaction(wallet1, wallet2, address1, address2) self.test_fundblsctrawtransaction(wallet1, wallet2, address1, address2) self.test_signblsctrawtransaction(wallet1, wallet2, address1, address2) - self.test_decodeblsctrawunsignedtransaction(wallet1, wallet2, address1, address2) + self.test_decodeblsctrawtransaction(wallet1, wallet2, address1, address2) + self.test_getblsctrecoverydata(wallet1, wallet2, address1, address2) self.test_integration_workflow(wallet1, wallet2, address1, address2) def test_createblsctrawtransaction(self, wallet1, wallet2, address1, address2): @@ -172,9 +173,9 @@ def test_signblsctrawtransaction(self, wallet1, wallet2, address1, address2): self.log.info("signblsctrawtransaction tests passed") - def test_decodeblsctrawunsignedtransaction(self, wallet1, wallet2, address1, address2): - """Test decodeblsctrawunsignedtransaction RPC method""" - self.log.info("Testing decodeblsctrawunsignedtransaction") + def test_decodeblsctrawtransaction(self, wallet1, wallet2, address1, address2): + """Test decodeblsctrawtransaction RPC method""" + self.log.info("Testing decodeblsctrawtransaction") # Get some unspent outputs unspent = wallet1.listblsctunspent() @@ -185,15 +186,107 @@ def test_decodeblsctrawunsignedtransaction(self, wallet1, wallet2, address1, add # Test 1: Decode a raw transaction raw_tx = wallet1.createblsctrawtransaction([{"txid": utxo['txid'], "vout": utxo['vout']}], []) - decoded_tx = wallet1.decodeblsctrawunsignedtransaction(raw_tx) + decoded_tx = wallet1.decodeblsctrawtransaction(raw_tx) self.log.info(f"Decoded transaction: {decoded_tx}") # Test 2: Error cases # Invalid hex string assert_raises_rpc_error(-22, "Transaction deserialization faile", - wallet1.decodeblsctrawunsignedtransaction, "invalid_hex") + wallet1.decodeblsctrawtransaction, "invalid_hex") - self.log.info("decodeblsctrawunsignedtransaction tests passed") + self.log.info("decodeblsctrawtransaction tests passed") + + def test_getblsctrecoverydata(self, wallet1, wallet2, address1, address2): + """Test getblsctrecoverydata RPC method""" + self.log.info("Testing getblsctrecoverydata") + + # Get some unspent outputs + unspent = wallet1.listblsctunspent() + assert_greater_than(len(unspent), 0) + + utxo = unspent[0] + self.log.info(f"Using UTXO: {utxo['txid']}:{utxo['vout']}") + + # Test 1: Get recovery data for a raw transaction (hex input) + raw_tx = wallet1.createblsctrawtransaction([{"txid": utxo['txid'], "vout": utxo['vout']}], []) + funded_tx = wallet1.fundblsctrawtransaction(raw_tx) + signed_tx = wallet1.signblsctrawtransaction(funded_tx) + recovery_data = wallet1.getblsctrecoverydata(signed_tx) + self.log.info(f"Recovery data from hex: {recovery_data}") + + assert_equal(len(recovery_data["outputs"]), 2) + # Verify the structure + assert "outputs" in recovery_data + assert isinstance(recovery_data["outputs"], list) + if len(recovery_data["outputs"]) > 0: + output = recovery_data["outputs"][0] + assert "vout" in output + assert "amount" in output + assert "gamma" in output + assert "message" in output + + # Test 2: Get recovery data for a specific vout + if len(recovery_data["outputs"]) > 0: + specific_vout = recovery_data["outputs"][0]["vout"] + recovery_data_specific = wallet1.getblsctrecoverydata(signed_tx, specific_vout) + self.log.info(f"Recovery data for vout {specific_vout}: {recovery_data_specific}") + + # Should have exactly one output + assert_equal(len(recovery_data_specific["outputs"]), 1) + assert_equal(recovery_data_specific["outputs"][0]["vout"], specific_vout) + + # Test 3: Create and broadcast a transaction, then get recovery data by txid + # Create a simple transaction + inputs = [] + outputs = [{"address": address2, "amount": 0.01, "memo": "Test recovery data"}] + + raw_tx = wallet1.createblsctrawtransaction(inputs, outputs) + funded_tx = wallet1.fundblsctrawtransaction(raw_tx) + signed_tx = wallet1.signblsctrawtransaction(funded_tx) + + # Broadcast the transaction + txid = self.nodes[0].sendrawtransaction(signed_tx) + self.log.info(f"Broadcasted transaction: {txid}") + + # Mine a block to confirm + self.generatetoblsctaddress(self.nodes[0], 1, address1) + + # Get the last received transaction to get the actual txid + transactions = wallet1.listtransactions("*", 1, 0) + assert_greater_than(len(transactions), 0) + last_tx = transactions[0] + actual_txid = last_tx["txid"] + self.log.info(f"Last received transaction: {actual_txid}") + + # Get recovery data by txid + recovery_data_txid = wallet1.getblsctrecoverydata(actual_txid) + recovery_data_signed = wallet1.getblsctrecoverydata(actual_txid) + self.log.info(f"Recovery data from txid: {recovery_data_txid}") + self.log.info(f"Recovery data from signed: {recovery_data_signed}") + + assert_equal(recovery_data_txid, recovery_data_signed) + + # Verify we can get recovery data for specific vout + if len(recovery_data_txid["outputs"]) > 0: + specific_vout = recovery_data_txid["outputs"][0]["vout"] + recovery_data_specific_txid = wallet1.getblsctrecoverydata(actual_txid, specific_vout) + assert_equal(len(recovery_data_specific_txid["outputs"]), 1) + + # Test 4: Error cases + # Invalid hex string + assert_raises_rpc_error(-22, "Transaction decode failed", + wallet1.getblsctrecoverydata, "invalid_hex") + + # Invalid vout index + assert_raises_rpc_error(-8, "vout index out of range", + wallet1.getblsctrecoverydata, signed_tx, 999) + + # Transaction not found in wallet (for txid input) + fake_txid = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + assert_raises_rpc_error(-8, "Transaction not found in wallet", + wallet1.getblsctrecoverydata, fake_txid) + + self.log.info("getblsctrecoverydata tests passed") def test_integration_workflow(self, wallet1, wallet2, address1, address2): """Test the complete workflow: create -> fund -> sign -> broadcast""" From 805cf64304605d44f3bf7c1ef82db7ec7f67bb93 Mon Sep 17 00:00:00 2001 From: alex v Date: Thu, 26 Jun 2025 19:59:49 +0200 Subject: [PATCH 05/20] fix tests --- src/bench/bech32_mod.cpp | 2 +- src/blsct/wallet/txfactory_global.cpp | 4 ++-- src/rpc/util.cpp | 2 +- src/test/blsct/wallet/address_tests.cpp | 2 +- src/test/blsct/wallet/keyman_tests.cpp | 20 +++++++++---------- .../blsct/wallet/txfactory_global_tests.cpp | 10 +++++----- src/test/key_io_tests.cpp | 2 +- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/bench/bech32_mod.cpp b/src/bench/bech32_mod.cpp index 3ef6c904c0fb2..3525e647380b6 100644 --- a/src/bench/bech32_mod.cpp +++ b/src/bench/bech32_mod.cpp @@ -25,7 +25,7 @@ static void Bech32ModEncode(benchmark::Bench& bench) static void Bech32ModDecode(benchmark::Bench& bench) { - std::string addr = "nv1d3fqq4j2w384smpjxgm95anexe4rjwzr2pc553t0xduxuc3sd35nvatcxpnn2mjyde45sqrtfapnws3kwfc85sm3v9ekzc2r2ajnquzf09282e62xddykn25x3"; + std::string addr = "nav1d3fqq4j2w384smpjxgm95anexe4rjwzr2pc553t0xduxuc3sd35nvatcxpnn2mjyde45sqrtfapnws3kwfc85sm3v9ekzc2r2ajnquzf09282e62xddykn25x3"; bench.batch(addr.size()).unit("byte").run([&] { bech32_mod::Decode(addr); }); diff --git a/src/blsct/wallet/txfactory_global.cpp b/src/blsct/wallet/txfactory_global.cpp index 5cb2720273700..c6fad8bcea056 100644 --- a/src/blsct/wallet/txfactory_global.cpp +++ b/src/blsct/wallet/txfactory_global.cpp @@ -18,11 +18,11 @@ void UnsignedOutput::GenerateKeys(Scalar blindingKey, DoublePublicKey destKeys) Point vk, sk; - if (!destKeys.GetViewKey(vk)) { + if (!destKeys.GetViewKey(vk) || vk.IsZero()) { throw std::runtime_error(strprintf("%s: could not get view key from destination address\n", __func__)); } - if (!destKeys.GetSpendKey(sk)) { + if (!destKeys.GetSpendKey(sk) || sk.IsZero()) { throw std::runtime_error(strprintf("%s: could not get spend key from destination address\n", __func__)); } diff --git a/src/rpc/util.cpp b/src/rpc/util.cpp index 94d214e310b6b..68eb9fac20daf 100644 --- a/src/rpc/util.cpp +++ b/src/rpc/util.cpp @@ -25,7 +25,7 @@ const std::string UNIX_EPOCH_TIME = "UNIX epoch time"; const std::string EXAMPLE_ADDRESS[2] = {"bc1q09vm5lfy0j5reeulh4x5752q25uqqvz34hufdl", "bc1q02ad21edsxd23d32dfgqqsz4vv4nmtfzuklhy3"}; -const std::string BLSCT_EXAMPLE_ADDRESS[2] = {"nv1d3fqq4j2w384smpjxgm95anexe4rjwzr2pc553t0xduxuc3sd35nvatcxpnn2mjyde45sqrtfapnws3kwfc85sm3v9ekzc2r2ajnquzf09282e62xddykn25x3", "nv1d3fqq4j2w384smpjxgm95anexe4rjwzr2pc553t0xduxuc3sd35nvatcxpnn2mjyde45sqrtfapnws3kwfc85sm3v9ekzc2r2ajnquzf09282e62xddykn25x3"}; +const std::string BLSCT_EXAMPLE_ADDRESS[2] = {"nav1d3fqq4j2w384smpjxgm95anexe4rjwzr2pc553t0xduxuc3sd35nvatcxpnn2mjyde45sqrtfapnws3kwfc85sm3v9ekzc2r2ajnquzf09282e62xddykn25x3", "nav1d3fqq4j2w384smpjxgm95anexe4rjwzr2pc553t0xduxuc3sd35nvatcxpnn2mjyde45sqrtfapnws3kwfc85sm3v9ekzc2r2ajnquzf09282e62xddykn25x3"}; std::string GetAllOutputTypes() { diff --git a/src/test/blsct/wallet/address_tests.cpp b/src/test/blsct/wallet/address_tests.cpp index 7b2f2a09cfafc..c3af33f3406c4 100644 --- a/src/test/blsct/wallet/address_tests.cpp +++ b/src/test/blsct/wallet/address_tests.cpp @@ -40,7 +40,7 @@ BOOST_FIXTURE_TEST_CASE(address_test, BasicTestingSetup) BOOST_CHECK(subAddressDoubleKey.GetID().ToString() == "dc103f3afbfa5fccf51bfcba8a65fb14ddd0c1a7"); - BOOST_CHECK(subAddress.GetString() == "nv1szddvednme8p63xcxhcm3h3k4tlw5vnesudwe66km0w6jrqyymqz969xmknnz0w9unqczu58sp0rhqjc4tdmgt6hmtn9tpavrzckfdfcuwyx4w0s7dgvjce34377psjen6ug4s2xfg9smrw6qx70xtja6s8wrt28dc"); + BOOST_CHECK(subAddress.GetString() == "nav1szddvednme8p63xcxhcm3h3k4tlw5vnesudwe66km0w6jrqyymqz969xmknnz0w9unqczu58sp0rhqjc4tdmgt6hmtn9tpavrzckfdfcuwyx4w0s7dgvjce34377psjen6ug4s2xfg9smrw6qx70xtja6syg55uu8z"); } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/blsct/wallet/keyman_tests.cpp b/src/test/blsct/wallet/keyman_tests.cpp index 08e83e1e948a5..cc103758fbbb8 100644 --- a/src/test/blsct/wallet/keyman_tests.cpp +++ b/src/test/blsct/wallet/keyman_tests.cpp @@ -28,16 +28,16 @@ BOOST_FIXTURE_TEST_CASE(wallet_test, TestingSetup) BOOST_CHECK(blsct_km->NewSubAddressPool(-1)); std::vector expectedAddresses = { - "nv14h85k6mf4l5fu3j4v0nuuswjwrz5entvzcw9jl3s8uknsndu0pfzaze4992n36uq7hpcy8yeuu854p0gmhq4m2u0tf5znazc527cxy4j7c39qxlc89wg4nca8pazkecx0p6wmu3pwrma3ercgrk8s7k475xxa5pl2w", - "nv1kq8zphgry92d02j7sm460c8xv88avuxcqlxrl7unxva9c4uawuvskx3s3pd6g3nychcq0ksy0tlpmgyt35384dnqdtudafa00yrjpcsffef404xur6cegkm98llf5xptkj6napgqk6g9dpa0x24qe4cgaq3v9scy0m", - "nv1s48u8dtxseguw6s7ecewph2szrxwy3fzx47rzdtppgzrnxrp0p0peetjx5h2f6gpwy3ar65tmw4p39z30pzt0t6san07th0694pffc0f6dghnskfujfanzwjdzep8fn0ezdeg7ejmvulj8nymrzkw8wdvqmhvl6gt5", - "nv1k34crux0s5urxtcndupcd37ehkakkz6da8n5ghmx388vfynhqa4k9zmrp8qmyw485ujvpkjwcasqhq5rqpxrkvhm0tg3ap3er8eycgwu5ew5xq5u84vzxsaqgc37ud67g5j9jvynlqacx78zl6l2flw82gvv2w5wzw", - "nv13qq8el3522u4jxd4e8y54du9d5fqlqlcmz8n90k8hc6e72dqky99ajgfarmd3puzx9zz9hazr99zrggharvuh9ulg9ugnu6nf5hfvq9mw03nv2g9xz9v2vnvn6uumrwxcv93ae54kuzjmz49g4mx0u2pzq2dmup8dn", - "nv1kh6n54xfhq0nmsr8rrqsff8xtegr8hvsdsvn2sdtk3w25w9fkescwqeqlnasm9ngcr895ycxx4ave2m5crya7hgyydhsa66ct995lrvywpgseu8cq4yjwcjm7dkh367pg3dhtxnwsfsct7my5tzu0c8jwsnddq2xw3", - "nv15gxjtgw289m82any2fn75gdh09cyte4c6qlzrms7wr4a4vyqdd8epl2qncrhspdflru3kcc4kdpzrrqtcvrq3qzxdjrh3l2lqr9v5jnjw22ut4axj9czcajj8pfyy0mm99n0q8088z99uame7ckrk8k3yvzc6em4d6", - "nv1j08knwnjcuukjl88vyt06c2h7unqjurflvtqaa9ljw08mz6swp2je7zg962u5qke9dc3cnhz3rkfdg0uhyw3zw6jk2akd08krzxqms74lcm9paapjygl3kglru3gaumy682qysl2hy6cgujqs9ugfxvqzcpmrge5pg", - "nv15vn8346nl5ttuu28w7dhwetq5vlu8tv3dgdqdhks769ye9gd9ssaszk5unwtejp6vftw82936k20m93sc4z9z29zz4f2rneexfw770ducywzxt3wp6vc7c3lhgxn2jxxufv74hwppcxd3prcn2yf2qgk6stntwl9lg", - "nv1kag0sqeuzz64stxmc5ztrafqvyx7lv4k09leasauyku5eg6zdsh23nyauzwrszyqysj02ecqmzkdrdym02w7u5y6ed7ptwe5adqyqufnqfj5hqve2et935gw8p8jculfnr66qpk8u86f35zaxs053920gs84wlan8z"}; + "nav14h85k6mf4l5fu3j4v0nuuswjwrz5entvzcw9jl3s8uknsndu0pfzaze4992n36uq7hpcy8yeuu854p0gmhq4m2u0tf5znazc527cxy4j7c39qxlc89wg4nca8pazkecx0p6wmu3pwrma3ercgrk8s7k4759q2thyq5", + "nav1kq8zphgry92d02j7sm460c8xv88avuxcqlxrl7unxva9c4uawuvskx3s3pd6g3nychcq0ksy0tlpmgyt35384dnqdtudafa00yrjpcsffef404xur6cegkm98llf5xptkj6napgqk6g9dpa0x24qe4cgaqj2j0wl9p", + "nav1s48u8dtxseguw6s7ecewph2szrxwy3fzx47rzdtppgzrnxrp0p0peetjx5h2f6gpwy3ar65tmw4p39z30pzt0t6san07th0694pffc0f6dghnskfujfanzwjdzep8fn0ezdeg7ejmvulj8nymrzkw8wdvqc3mqvnpw", + "nav1k34crux0s5urxtcndupcd37ehkakkz6da8n5ghmx388vfynhqa4k9zmrp8qmyw485ujvpkjwcasqhq5rqpxrkvhm0tg3ap3er8eycgwu5ew5xq5u84vzxsaqgc37ud67g5j9jvynlqacx78zl6l2flw82g02a3z4g5", + "nav13qq8el3522u4jxd4e8y54du9d5fqlqlcmz8n90k8hc6e72dqky99ajgfarmd3puzx9zz9hazr99zrggharvuh9ulg9ugnu6nf5hfvq9mw03nv2g9xz9v2vnvn6uumrwxcv93ae54kuzjmz49g4mx0u2pzqftvrhu8f", + "nav1kh6n54xfhq0nmsr8rrqsff8xtegr8hvsdsvn2sdtk3w25w9fkescwqeqlnasm9ngcr895ycxx4ave2m5crya7hgyydhsa66ct995lrvywpgseu8cq4yjwcjm7dkh367pg3dhtxnwsfsct7my5tzu0c8jwsst6luayt", + "nav15gxjtgw289m82any2fn75gdh09cyte4c6qlzrms7wr4a4vyqdd8epl2qncrhspdflru3kcc4kdpzrrqtcvrq3qzxdjrh3l2lqr9v5jnjw22ut4axj9czcajj8pfyy0mm99n0q8088z99uame7ckrk8k3yvp7dxdw8q", + "nav1j08knwnjcuukjl88vyt06c2h7unqjurflvtqaa9ljw08mz6swp2je7zg962u5qke9dc3cnhz3rkfdg0uhyw3zw6jk2akd08krzxqms74lcm9paapjygl3kglru3gaumy682qysl2hy6cgujqs9ugfxvqzcza5h00tj", + "nav15vn8346nl5ttuu28w7dhwetq5vlu8tv3dgdqdhks769ye9gd9ssaszk5unwtejp6vftw82936k20m93sc4z9z29zz4f2rneexfw770ducywzxt3wp6vc7c3lhgxn2jxxufv74hwppcxd3prcn2yf2qgk6sg4u3f74j", + "nav1kag0sqeuzz64stxmc5ztrafqvyx7lv4k09leasauyku5eg6zdsh23nyauzwrszyqysj02ecqmzkdrdym02w7u5y6ed7ptwe5adqyqufnqfj5hqve2et935gw8p8jculfnr66qpk8u86f35zaxs053920gsyneqtgdc"}; for (size_t i = 0; i < 10; i++) { auto recvAddress = blsct::SubAddress(std::get(blsct_km->GetNewDestination(0).value())); diff --git a/src/test/blsct/wallet/txfactory_global_tests.cpp b/src/test/blsct/wallet/txfactory_global_tests.cpp index 272a3168b4d8b..4487db0241e3f 100644 --- a/src/test/blsct/wallet/txfactory_global_tests.cpp +++ b/src/test/blsct/wallet/txfactory_global_tests.cpp @@ -12,16 +12,16 @@ BOOST_AUTO_TEST_SUITE(blsct_txfactory_global_tests) BOOST_FIXTURE_TEST_CASE(create_output_test, TestingSetup) { - std::string destAddr = "nv1szddvednme8p63xcxhcm3h3k4tlw5vnesudwe66km0w6jrqyymqz969xmknnz0w9unqczu58sp0rhqjc4tdmgt6hmtn9tpavrzckfdfcuwyx4w0s7dgvjce34377psjen6ug4s2xfg9smrw6qx70xtja6s8wrt28dc"; + std::string destAddr = "nav14h85k6mf4l5fu3j4v0nuuswjwrz5entvzcw9jl3s8uknsndu0pfzaze4992n36uq7hpcy8yeuu854p0gmhq4m2u0tf5znazc527cxy4j7c39qxlc89wg4nca8pazkecx0p6wmu3pwrma3ercgrk8s7k4759q2thyq5"; MclScalar blindingKey{ParseHex("42c0926471b3bd01ae130d9382c5fca2e2b5000abbf826a93132696ffa5f2c65")}; auto out = blsct::CreateOutput(blsct::SubAddress(destAddr).GetKeys(), 1, "", TokenId(), blindingKey); - BOOST_CHECK(out.out.blsctData.viewTag == 20232); - BOOST_CHECK(HexStr(out.out.blsctData.spendingKey.GetVch()) == "84ee3e9c40fe65f91776033b5ddb3bf280bbd549924028280dad0d6ea464bb728886910f53bd66bcc2b59e37f1b8f55e"); - BOOST_CHECK(HexStr(out.out.blsctData.blindingKey.GetVch()) == "80e845ca79807a66ab93121dd57971ee2acbb3ef20d94c0750eb33eaa64bf58fe0f5cd775dcf73b49117260e806e8708"); + BOOST_CHECK(out.out.blsctData.viewTag == 52098); + BOOST_CHECK(HexStr(out.out.blsctData.spendingKey.GetVch()) == "90a498638b6d13a89b2dd1bbcb1caf419577878bff4c2d6426d602b9d74f3878d4a89feda5c2a59dc862a55c4e25a265"); + BOOST_CHECK(HexStr(out.out.blsctData.blindingKey.GetVch()) == "b96f2eae5089b87e1f9c39f49cccc779c840d861ab0a722d9597273c4c6e9f4075ce5562c82efba02f46146bf17bad72"); BOOST_CHECK(HexStr(out.out.blsctData.ephemeralKey.GetVch()) == "935963399885ba1dd51dd272fb9be541896ac619570315e55f06c1e3a42d28ffb300fe6a3247d484bb491b25ecf7fb8a"); - BOOST_CHECK(out.gamma.GetString() == "77fb7a5ff31f1c28037942b94e23270338fa788e065855dffa6860be93073ee"); + BOOST_CHECK(out.gamma.GetString() == "37763c3ba24138ed73aafc33881e221d942bd81b20acc840033eb0a7bc0be4b5"); BOOST_CHECK(out.blindingKey.GetString() == "42c0926471b3bd01ae130d9382c5fca2e2b5000abbf826a93132696ffa5f2c65"); } diff --git a/src/test/key_io_tests.cpp b/src/test/key_io_tests.cpp index 23b6aa0f068f4..b36d3965a6797 100644 --- a/src/test/key_io_tests.cpp +++ b/src/test/key_io_tests.cpp @@ -163,7 +163,7 @@ BOOST_AUTO_TEST_CASE(key_io_double_public_key_endode_decode) BOOST_AUTO_TEST_CASE(key_io_double_public_key_decode_encode) { // a valid bech32_mod encoded double public key - std::string dpk_bech32_mod = "nv1jlca8fe3jltegf54vwxyl2dvplpk3rz0ja6tjpdpfcar79cm43vxc40g8luh5xh0lva0qzkmytrthftje04fqnt8g6yq3j8t2z552ryhy8dnpyfgqyj58ypdptp43f32u28htwu0r37y9su6332jn0c0fcvan8l53m"; + std::string dpk_bech32_mod = "nav1szddvednme8p63xcxhcm3h3k4tlw5vnesudwe66km0w6jrqyymqz969xmknnz0w9unqczu58sp0rhqjc4tdmgt6hmtn9tpavrzckfdfcuwyx4w0s7dgvjce34377psjen6ug4s2xfg9smrw6qx70xtja6syg55uu8z"; // check if decoding and then encoding it produces // the original bech32_mod encoded double public key From 9730bb4bf93fab823c7171305db3e9354692579a Mon Sep 17 00:00:00 2001 From: alex v Date: Thu, 26 Jun 2025 20:34:30 +0200 Subject: [PATCH 06/20] fix linter --- test/functional/blsct_balance_proof.py | 11 +++--- test/functional/blsct_rawtransaction.py | 52 ++++++++++++------------- 2 files changed, 31 insertions(+), 32 deletions(-) diff --git a/test/functional/blsct_balance_proof.py b/test/functional/blsct_balance_proof.py index 0e971407fe7fc..67cca5cd6b843 100755 --- a/test/functional/blsct_balance_proof.py +++ b/test/functional/blsct_balance_proof.py @@ -61,31 +61,30 @@ def run_test(self): # Test creating a balance proof self.log.info("Testing createblsctbalanceproof") - + # Create a valid proof proof_result = wallet.createblsctbalanceproof(balance / 2) assert "proof" in proof_result, "Proof not found in result" proof_hex = proof_result["proof"] - + # Test verifying the proof self.log.info("Testing verifyblsctbalanceproof") - + # Test with invalid proof format assert_raises_rpc_error(-8, "Invalid proof format", wallet.verifyblsctbalanceproof, "invalid") - + # Test with modified proof (corrupt the hex string) if len(proof_hex) > 2: # Modify the last character of the hex string modified_proof = proof_hex[::-1] assert_raises_rpc_error(-8, "Invalid proof format",wallet_2.verifyblsctbalanceproof, modified_proof) - + # Verify the valid proof verify_result = wallet_2.verifyblsctbalanceproof(proof_hex) assert "valid" in verify_result, "Valid field not found in result" assert "min_amount" in verify_result, "Min amount field not found in result" assert verify_result["valid"], "Proof should be valid" assert_equal(verify_result["min_amount"], balance / 2) - if __name__ == '__main__': NavioBlsctBalanceProofTest().main() \ No newline at end of file diff --git a/test/functional/blsct_rawtransaction.py b/test/functional/blsct_rawtransaction.py index 22e8b0f42fa77..3c587d3d8bd2d 100755 --- a/test/functional/blsct_rawtransaction.py +++ b/test/functional/blsct_rawtransaction.py @@ -51,7 +51,7 @@ def run_test(self): # Generate blocks to fund the first wallet self.log.info("Generating 101 blocks to fund wallet1") block_hashes = self.generatetoblsctaddress(self.nodes[0], 101, address1) - + # Check initial balance balance1 = wallet1.getbalance() self.log.info(f"Initial balance in wallet1: {balance1}") @@ -71,17 +71,17 @@ def test_createblsctrawtransaction(self, wallet1, wallet2, address1, address2): # Get some unspent outputs unspent = wallet1.listblsctunspent() assert_greater_than(len(unspent), 0) - + utxo = unspent[0] self.log.info(f"Using UTXO: {utxo['txid']}:{utxo['vout']}") # Test 1: Create raw transaction with minimal inputs (wallet will fill missing data) inputs = [{"txid": utxo['txid'], "vout": utxo['vout']}] outputs = [{"address": address2, "amount": 0.1, "memo": "Test transaction"}] - + raw_tx = wallet1.createblsctrawtransaction(inputs, outputs) self.log.info(f"Created raw transaction: {raw_tx[:100]}...") - + # Test 2: Create raw transaction with all optional fields provided inputs_with_data = [{ "txid": utxo['txid'], @@ -89,7 +89,7 @@ def test_createblsctrawtransaction(self, wallet1, wallet2, address1, address2): "value": int(utxo['amount'] * COIN), "is_staked_commitment": False }] - + raw_tx_with_data = wallet1.createblsctrawtransaction(inputs_with_data, outputs) # Test 3: Create raw transaction with multiple outputs @@ -97,7 +97,7 @@ def test_createblsctrawtransaction(self, wallet1, wallet2, address1, address2): {"address": address2, "amount": 0.05, "memo": "First output"}, {"address": address1, "amount": 0.03, "memo": "Second output"} ] - + raw_tx_multi = wallet1.createblsctrawtransaction(inputs, outputs_multi) # Test 4: Error cases @@ -120,13 +120,13 @@ def test_fundblsctrawtransaction(self, wallet1, wallet2, address1, address2): # Create a raw transaction with insufficient inputs inputs = [] # No inputs outputs = [{"address": address2, "amount": 0.1, "memo": "Test funding"}] - + raw_tx = wallet1.createblsctrawtransaction(inputs, outputs) - + # Fund the transaction funded_tx = wallet1.fundblsctrawtransaction(raw_tx) self.log.info(f"Funded transaction: {funded_tx[:100]}...") - + # Verify the funded transaction is different from the original assert funded_tx != raw_tx, "Funded transaction should be different from original" @@ -147,7 +147,7 @@ def test_fundblsctrawtransaction(self, wallet1, wallet2, address1, address2): balance = wallet1.getbalance() large_outputs = [{"address": address2, "amount": balance + 1, "memo": "Too much"}] large_raw_tx = wallet1.createblsctrawtransaction(inputs, large_outputs) - + assert_raises_rpc_error(-6, "Insufficient funds", wallet1.fundblsctrawtransaction, large_raw_tx) @@ -160,14 +160,14 @@ def test_signblsctrawtransaction(self, wallet1, wallet2, address1, address2): # Create and fund a raw transaction inputs = [] outputs = [{"address": address2, "amount": 0.1, "memo": "Test signing"}] - + raw_tx = wallet1.createblsctrawtransaction(inputs, outputs) funded_tx = wallet1.fundblsctrawtransaction(raw_tx) - + # Sign the transaction signed_tx = wallet1.signblsctrawtransaction(funded_tx) self.log.info(f"Signed transaction: {signed_tx[:100]}...") - + # Verify the signed transaction is different from the funded transaction assert signed_tx != funded_tx, "Signed transaction should be different from funded transaction" @@ -180,7 +180,7 @@ def test_decodeblsctrawtransaction(self, wallet1, wallet2, address1, address2): # Get some unspent outputs unspent = wallet1.listblsctunspent() assert_greater_than(len(unspent), 0) - + utxo = unspent[0] self.log.info(f"Using UTXO: {utxo['txid']}:{utxo['vout']}") @@ -203,7 +203,7 @@ def test_getblsctrecoverydata(self, wallet1, wallet2, address1, address2): # Get some unspent outputs unspent = wallet1.listblsctunspent() assert_greater_than(len(unspent), 0) - + utxo = unspent[0] self.log.info(f"Using UTXO: {utxo['txid']}:{utxo['vout']}") @@ -213,7 +213,7 @@ def test_getblsctrecoverydata(self, wallet1, wallet2, address1, address2): signed_tx = wallet1.signblsctrawtransaction(funded_tx) recovery_data = wallet1.getblsctrecoverydata(signed_tx) self.log.info(f"Recovery data from hex: {recovery_data}") - + assert_equal(len(recovery_data["outputs"]), 2) # Verify the structure assert "outputs" in recovery_data @@ -230,7 +230,7 @@ def test_getblsctrecoverydata(self, wallet1, wallet2, address1, address2): specific_vout = recovery_data["outputs"][0]["vout"] recovery_data_specific = wallet1.getblsctrecoverydata(signed_tx, specific_vout) self.log.info(f"Recovery data for vout {specific_vout}: {recovery_data_specific}") - + # Should have exactly one output assert_equal(len(recovery_data_specific["outputs"]), 1) assert_equal(recovery_data_specific["outputs"][0]["vout"], specific_vout) @@ -239,25 +239,25 @@ def test_getblsctrecoverydata(self, wallet1, wallet2, address1, address2): # Create a simple transaction inputs = [] outputs = [{"address": address2, "amount": 0.01, "memo": "Test recovery data"}] - + raw_tx = wallet1.createblsctrawtransaction(inputs, outputs) funded_tx = wallet1.fundblsctrawtransaction(raw_tx) signed_tx = wallet1.signblsctrawtransaction(funded_tx) - + # Broadcast the transaction txid = self.nodes[0].sendrawtransaction(signed_tx) self.log.info(f"Broadcasted transaction: {txid}") - + # Mine a block to confirm self.generatetoblsctaddress(self.nodes[0], 1, address1) - + # Get the last received transaction to get the actual txid transactions = wallet1.listtransactions("*", 1, 0) assert_greater_than(len(transactions), 0) last_tx = transactions[0] actual_txid = last_tx["txid"] self.log.info(f"Last received transaction: {actual_txid}") - + # Get recovery data by txid recovery_data_txid = wallet1.getblsctrecoverydata(actual_txid) recovery_data_signed = wallet1.getblsctrecoverydata(actual_txid) @@ -265,7 +265,7 @@ def test_getblsctrecoverydata(self, wallet1, wallet2, address1, address2): self.log.info(f"Recovery data from signed: {recovery_data_signed}") assert_equal(recovery_data_txid, recovery_data_signed) - + # Verify we can get recovery data for specific vout if len(recovery_data_txid["outputs"]) > 0: specific_vout = recovery_data_txid["outputs"][0]["vout"] @@ -276,11 +276,11 @@ def test_getblsctrecoverydata(self, wallet1, wallet2, address1, address2): # Invalid hex string assert_raises_rpc_error(-22, "Transaction decode failed", wallet1.getblsctrecoverydata, "invalid_hex") - + # Invalid vout index assert_raises_rpc_error(-8, "vout index out of range", wallet1.getblsctrecoverydata, signed_tx, 999) - + # Transaction not found in wallet (for txid input) fake_txid = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" assert_raises_rpc_error(-8, "Transaction not found in wallet", @@ -295,7 +295,7 @@ def test_integration_workflow(self, wallet1, wallet2, address1, address2): # Step 1: Create raw transaction inputs = [] outputs = [{"address": address2, "amount": 0.05, "memo": "Integration test"}] - + raw_tx = wallet1.createblsctrawtransaction(inputs, outputs) self.log.info("Step 1: Created raw transaction") From 434231e68c1161cf0744fc2f5e1067d19c0fb8b8 Mon Sep 17 00:00:00 2001 From: alex v Date: Thu, 26 Jun 2025 20:45:32 +0200 Subject: [PATCH 07/20] fix linter --- test/functional/blsct_rawtransaction.py | 30 ++++++++++------------ test/functional/test_framework/messages.py | 6 ++--- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/test/functional/blsct_rawtransaction.py b/test/functional/blsct_rawtransaction.py index 3c587d3d8bd2d..00a3e2c20f6f8 100755 --- a/test/functional/blsct_rawtransaction.py +++ b/test/functional/blsct_rawtransaction.py @@ -5,7 +5,6 @@ """Test the BLSCT raw transaction RPC methods.""" -from decimal import Decimal from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, @@ -33,11 +32,11 @@ def setup_network(self): def run_test(self): self.log.info("Setting up wallets and generating initial blocks") - + # Create BLSCT wallets self.nodes[0].createwallet(wallet_name="wallet1", blsct=True) self.nodes[1].createwallet(wallet_name="wallet2", blsct=True) - + wallet1 = self.nodes[0].get_wallet_rpc("wallet1") wallet2 = self.nodes[1].get_wallet_rpc("wallet2") @@ -50,7 +49,6 @@ def run_test(self): # Generate blocks to fund the first wallet self.log.info("Generating 101 blocks to fund wallet1") - block_hashes = self.generatetoblsctaddress(self.nodes[0], 101, address1) # Check initial balance balance1 = wallet1.getbalance() @@ -84,14 +82,14 @@ def test_createblsctrawtransaction(self, wallet1, wallet2, address1, address2): # Test 2: Create raw transaction with all optional fields provided inputs_with_data = [{ - "txid": utxo['txid'], + "txid": utxo['txid'], "vout": utxo['vout'], "value": int(utxo['amount'] * COIN), "is_staked_commitment": False }] raw_tx_with_data = wallet1.createblsctrawtransaction(inputs_with_data, outputs) - + self.log.info(f"Created raw transaction with data: {raw_tx_with_data[:100]}...") # Test 3: Create raw transaction with multiple outputs outputs_multi = [ {"address": address2, "amount": 0.05, "memo": "First output"}, @@ -99,16 +97,16 @@ def test_createblsctrawtransaction(self, wallet1, wallet2, address1, address2): ] raw_tx_multi = wallet1.createblsctrawtransaction(inputs, outputs_multi) - + self.log.info(f"Created raw transaction with multiple outputs: {raw_tx_multi[:100]}...") # Test 4: Error cases # Invalid address outputs_invalid = [{"address": "invalid_address", "amount": 0.1}] - assert_raises_rpc_error(-5, "Invalid BLSCT address", + assert_raises_rpc_error(-5, "Invalid BLSCT address", wallet1.createblsctrawtransaction, inputs, outputs_invalid) # Negative amount outputs_negative = [{"address": address2, "amount": -0.1}] - assert_raises_rpc_error(-3, "Amount out of range", + assert_raises_rpc_error(-3, "Amount out of range", wallet1.createblsctrawtransaction, inputs, outputs_negative) self.log.info("createblsctrawtransaction tests passed") @@ -133,14 +131,14 @@ def test_fundblsctrawtransaction(self, wallet1, wallet2, address1, address2): # Test with custom change address change_address = wallet2.getnewaddress(label="", address_type="blsct") funded_tx_with_change = wallet1.fundblsctrawtransaction(raw_tx, change_address) - + self.log.info(f"Funded transaction with change: {funded_tx_with_change[:100]}...") # Test error cases # Invalid hex string - assert_raises_rpc_error(-22, "Transaction deserialization faile", + assert_raises_rpc_error(-22, "Transaction deserialization faile", wallet1.fundblsctrawtransaction, "invalid_hex") # Invalid change address - assert_raises_rpc_error(-5, "Invalid BLSCT change address", + assert_raises_rpc_error(-5, "Invalid BLSCT change address", wallet1.fundblsctrawtransaction, raw_tx, "invalid_address") # Test with insufficient funds (create a transaction larger than available balance) @@ -148,7 +146,7 @@ def test_fundblsctrawtransaction(self, wallet1, wallet2, address1, address2): large_outputs = [{"address": address2, "amount": balance + 1, "memo": "Too much"}] large_raw_tx = wallet1.createblsctrawtransaction(inputs, large_outputs) - assert_raises_rpc_error(-6, "Insufficient funds", + assert_raises_rpc_error(-6, "Insufficient funds", wallet1.fundblsctrawtransaction, large_raw_tx) self.log.info("fundblsctrawtransaction tests passed") @@ -191,7 +189,7 @@ def test_decodeblsctrawtransaction(self, wallet1, wallet2, address1, address2): # Test 2: Error cases # Invalid hex string - assert_raises_rpc_error(-22, "Transaction deserialization faile", + assert_raises_rpc_error(-22, "Transaction deserialization faile", wallet1.decodeblsctrawtransaction, "invalid_hex") self.log.info("decodeblsctrawtransaction tests passed") @@ -283,7 +281,7 @@ def test_getblsctrecoverydata(self, wallet1, wallet2, address1, address2): # Transaction not found in wallet (for txid input) fake_txid = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" - assert_raises_rpc_error(-8, "Transaction not found in wallet", + assert_raises_rpc_error(-8, "Transaction not found in wallet", wallet1.getblsctrecoverydata, fake_txid) self.log.info("getblsctrecoverydata tests passed") @@ -327,4 +325,4 @@ def test_integration_workflow(self, wallet1, wallet2, address1, address2): if __name__ == '__main__': - BLSCTRawTransactionTest().main() \ No newline at end of file + BLSCTRawTransactionTest().main() diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py index 5af7c9a851fba..caf51546b1926 100755 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -1136,7 +1136,7 @@ def serialize(self): r += self.b.serialize() r += self.omega.serialize() return r - + class RangeProof: __slots__ = 'Vs', 'Ls', 'Rs', 'A', 'A_wip', 'B', 'r_prime', 's_prime', 'delta_prime', 'alpha_hat', 'tau_x' @@ -1180,8 +1180,8 @@ def serialize(self): r += self.alpha_hat.serialize() r += self.tau_x.serialize() return r - - + + class PosProof: __slots__ = ("set_mem_proof", "range_proof") From cd18ba17d93287bfa33a88d21e023c72d62969fb Mon Sep 17 00:00:00 2001 From: alex v Date: Thu, 26 Jun 2025 20:54:24 +0200 Subject: [PATCH 08/20] update makefile --- src/Makefile.am | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Makefile.am b/src/Makefile.am index 7cba7ceb7d1e1..55ea1e43ae277 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -584,7 +584,6 @@ libbitcoin_node_a_SOURCES = \ blsct/set_mem_proof/set_mem_proof_prover.cpp \ blsct/tokens/info.cpp \ blsct/tokens/rpc.cpp \ - blsct/wallet/rpc.cpp \ blsct/wallet/unsigned_transaction.cpp \ blsct/wallet/verification.cpp \ blsct/signature.cpp \ @@ -744,7 +743,6 @@ libbitcoin_wallet_a_SOURCES = \ blsct/wallet/helpers.cpp \ blsct/wallet/keyman.cpp \ blsct/wallet/keyring.cpp \ - blsct/wallet/rpc.cpp \ blsct/wallet/unsigned_transaction.cpp \ blsct/wallet/txfactory.cpp \ blsct/wallet/txfactory_base.cpp \ From a6619ccf704a0ceabd05da89bfc99855e0b91f11 Mon Sep 17 00:00:00 2001 From: alex v Date: Thu, 26 Jun 2025 21:23:45 +0200 Subject: [PATCH 09/20] fix linter --- test/functional/blsct_balance_proof.py | 2 +- test/functional/blsct_rawtransaction.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/functional/blsct_balance_proof.py b/test/functional/blsct_balance_proof.py index 67cca5cd6b843..60dffcc92b6bb 100755 --- a/test/functional/blsct_balance_proof.py +++ b/test/functional/blsct_balance_proof.py @@ -87,4 +87,4 @@ def run_test(self): assert_equal(verify_result["min_amount"], balance / 2) if __name__ == '__main__': - NavioBlsctBalanceProofTest().main() \ No newline at end of file + NavioBlsctBalanceProofTest().main() diff --git a/test/functional/blsct_rawtransaction.py b/test/functional/blsct_rawtransaction.py index 00a3e2c20f6f8..2ea0b832a48ff 100755 --- a/test/functional/blsct_rawtransaction.py +++ b/test/functional/blsct_rawtransaction.py @@ -272,11 +272,11 @@ def test_getblsctrecoverydata(self, wallet1, wallet2, address1, address2): # Test 4: Error cases # Invalid hex string - assert_raises_rpc_error(-22, "Transaction decode failed", + assert_raises_rpc_error(-22, "Transaction decode failed", wallet1.getblsctrecoverydata, "invalid_hex") # Invalid vout index - assert_raises_rpc_error(-8, "vout index out of range", + assert_raises_rpc_error(-8, "vout index out of range", wallet1.getblsctrecoverydata, signed_tx, 999) # Transaction not found in wallet (for txid input) From ce1df6d488c5ccca995aeeeb9ca620b9e270cca5 Mon Sep 17 00:00:00 2001 From: alex v Date: Thu, 26 Jun 2025 22:09:18 +0200 Subject: [PATCH 10/20] update makefile --- src/Makefile.am | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Makefile.am b/src/Makefile.am index 55ea1e43ae277..b7ee7e147c026 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -744,6 +744,7 @@ libbitcoin_wallet_a_SOURCES = \ blsct/wallet/keyman.cpp \ blsct/wallet/keyring.cpp \ blsct/wallet/unsigned_transaction.cpp \ + blsct/wallet/rpc.cpp \ blsct/wallet/txfactory.cpp \ blsct/wallet/txfactory_base.cpp \ blsct/wallet/txfactory_global.cpp \ From 6eea01e99ddb4f8ac363f020f695c5fffcb2273f Mon Sep 17 00:00:00 2001 From: alex v Date: Thu, 26 Jun 2025 23:04:19 +0200 Subject: [PATCH 11/20] fix duplicated declaration --- test/functional/test_framework/messages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/test_framework/messages.py b/test/functional/test_framework/messages.py index caf51546b1926..9eb1171ddf3ce 100755 --- a/test/functional/test_framework/messages.py +++ b/test/functional/test_framework/messages.py @@ -519,7 +519,7 @@ def serialize(self): r += ser_bytes(self.sig) return r -class RangeProof: +class RangeProof_old: __slots__ = ("Vs", "A", "T1", "T2", "mu", "tau_x", "Ls", "Rs", "a", "b", "t_hat") def __init__(self, proof=None): From 4232d1fe61d610a0bae920674809d12363f53d90 Mon Sep 17 00:00:00 2001 From: alex v Date: Fri, 4 Jul 2025 15:49:24 +0200 Subject: [PATCH 12/20] OP_BLSCHECKSIG --- src/Makefile.am | 1 + src/Makefile.test.include | 1 + src/blsct/range_proof/rpc.cpp | 68 +++++ src/blsct/wallet/keyman.cpp | 86 ++++++ src/blsct/wallet/keyman.h | 42 ++- src/blsct/wallet/rpc.cpp | 112 +++++--- src/blsct/wallet/rpc.h | 5 +- src/blsct/wallet/txfactory_global.cpp | 67 ++++- src/blsct/wallet/txfactory_global.h | 1 + src/blsct/wallet/verification.cpp | 25 +- src/rpc/client.cpp | 5 + src/script/interpreter.cpp | 41 ++- src/script/interpreter.h | 17 ++ src/script/script.cpp | 2 +- src/script/script.h | 11 +- src/script/script_error.h | 4 +- src/test/blsct/pos/pos_chain_tests.cpp | 3 - src/test/blsct/wallet/validation_tests.cpp | 2 +- src/test/blsct_signature_checker_tests.cpp | 258 ++++++++++++++++++ src/test/data/script_tests.json | 25 +- src/test/script_parse_tests.cpp | 2 +- src/test/util/setup_common.h | 7 +- src/wallet/rpc/backup.cpp | 89 +++++- src/wallet/rpc/wallet.cpp | 3 + src/wallet/wallet.cpp | 23 ++ src/wallet/wallet.h | 3 +- src/wallet/walletdb.cpp | 48 +++- src/wallet/walletdb.h | 4 + test/functional/blsct_import_scriptpubkeys.py | 213 +++++++++++++++ test/functional/blsct_rawtransaction.py | 7 +- test/functional/test_framework/messages.py | 89 +++--- test/functional/test_runner.py | 4 +- 32 files changed, 1150 insertions(+), 118 deletions(-) create mode 100644 src/blsct/range_proof/rpc.cpp create mode 100644 src/test/blsct_signature_checker_tests.cpp create mode 100755 test/functional/blsct_import_scriptpubkeys.py diff --git a/src/Makefile.am b/src/Makefile.am index b7ee7e147c026..46a105255a30c 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -912,6 +912,7 @@ libbitcoin_consensus_a_SOURCES = \ blsct/set_mem_proof/set_mem_proof_prover.cpp \ blsct/pos/helpers.cpp \ blsct/pos/proof.cpp \ + blsct/public_key.cpp \ blsct/signature.cpp \ consensus/amount.h \ consensus/merkle.cpp \ diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 2244cb30f9be7..4c2b9098992bf 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -109,6 +109,7 @@ BITCOIN_TESTS =\ test/blsct/set_mem_proof/set_mem_proof_tests.cpp \ test/blsct/signature_tests.cpp \ test/blsct/sign_verify_tests.cpp \ + test/blsct_signature_checker_tests.cpp \ test/bswap_tests.cpp \ test/checkqueue_tests.cpp \ test/coins_tests.cpp \ diff --git a/src/blsct/range_proof/rpc.cpp b/src/blsct/range_proof/rpc.cpp new file mode 100644 index 0000000000000..8a1dd576b47d3 --- /dev/null +++ b/src/blsct/range_proof/rpc.cpp @@ -0,0 +1,68 @@ +// Copyright (c) 2024 The Navio Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +RPCHelpMan verifyblsctbalanceproof() +{ + return RPCHelpMan{ + "verifyblsctbalanceproof", + "Verifies a zero-knowledge balance proof\n", + { + {"proof", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The serialized balance proof"}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", { + {RPCResult::Type::BOOL, "valid", "Whether the proof is valid"}, + {RPCResult::Type::NUM, "min_amount", "The minimum amount proven"}, + }}, + RPCExamples{HelpExampleCli("verifyblsctbalanceproof", "\"\"") + HelpExampleRpc("verifyblsctbalanceproof", "\"\"")}, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { + LOCK(cs_main); + ChainstateManager& chainman = EnsureAnyChainman(request.context); + Chainstate& active_chainstate = chainman.ActiveChainstate(); + + CCoinsViewCache* coins_view; + coins_view = &active_chainstate.CoinsTip(); + + // Deserialize the proof + std::vector proof_data = ParseHex(request.params[0].get_str()); + DataStream ss{proof_data}; + blsct::BalanceProof proof; + try { + ss >> proof; + } catch (const std::exception& e) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid proof format"); + } + + // Verify the proof + bool valid = proof.Verify(coins_view); + + UniValue result(UniValue::VOBJ); + result.pushKV("valid", valid); + result.pushKV("min_amount", ValueFromAmount(proof.GetMinAmount())); + + return result; + }, + }; +} + +void RegisterBLSCTUtilsRPCCommands(CRPCTable& t) +{ + static const CRPCCommand commands[]{ + {"blsct", &verifyblsctbalanceproof}, + }; + for (const auto& c : commands) { + t.appendCommand(c.name, &c); + } +} \ No newline at end of file diff --git a/src/blsct/wallet/keyman.cpp b/src/blsct/wallet/keyman.cpp index 19866868e954e..d17cc07dbca47 100644 --- a/src/blsct/wallet/keyman.cpp +++ b/src/blsct/wallet/keyman.cpp @@ -4,6 +4,7 @@ #include #include +#include