Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Public Transaction Creation #1348

Open
wants to merge 25 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0aca3d7
Refactor transaction creation.
Nov 11, 2023
d8e6161
Allow spending unconfirmed funds from ourself again.
Nov 18, 2023
855a7ca
Check that locked coins are not spent.
Nov 18, 2023
3dc4003
Use configured tx fee.
Nov 18, 2023
0190c0d
Add logic to reject long chains to CreateTransaction.
Nov 20, 2023
56cbcba
Respect strFromAccount in CreateTransaction.
Nov 22, 2023
4dd7e2c
Fix using coinbase inputs with exactly COINBASE_MATURITY confirmations.
Nov 25, 2023
47e3638
Respect -spendzeroconfchange in CreateTransaction.
Nov 27, 2023
957a021
Make txn_clone.py test work with CreateTransaction.
Nov 27, 2023
e9ca351
Fix createtransaction_tests.
Dec 4, 2023
d5e10cd
Fix patrial rebase
psolstice Jan 29, 2024
ce3bf31
Exchange address support in CTransparentTxout
psolstice Feb 1, 2024
08f2642
Moved large template function to .cpp file
psolstice Feb 27, 2024
ea6326d
Changed visibility of CCoinControl members
psolstice Mar 1, 2024
beb1597
Changed signature of CreateTransaction() function
psolstice Mar 1, 2024
bfda395
Removed CTxOut::nRounds field
psolstice Jul 20, 2024
6904736
Changed visibility of critical section back to private
psolstice Jul 20, 2024
fbea637
Apply new transaction limits
psolstice Jul 20, 2024
f603de6
Fixed coincontrol logic (thus fixing many tests behaviour)
psolstice Aug 18, 2024
3423301
Changed TransparentTxout::IsFromMe() implementation to fix bugs in tests
psolstice Aug 18, 2024
11e8a40
Fixed crash/undefined behaviour due to mssing lock
psolstice Aug 19, 2024
988f1c6
Fixed undefined behaviour in dip3-deterministicmn.py test
psolstice Aug 20, 2024
ae24627
Fix undefined behaviour in txn_doublespend.py test
psolstice Aug 20, 2024
f65c3e8
Get rid of tests for removed behaviour
psolstice Aug 21, 2024
bd31b27
Merged with master
psolstice Aug 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions qa/rpc-tests/dip3-deterministicmns.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def run_test(self):
self.log.info("creating collateral for mn-before-dip3")
before_dip3_mn = prepare_mn(self.nodes[0], 1, 'mn-before-dip3')
create_mn_collateral(self.nodes[0], before_dip3_mn)
lock_mn_collateral(self.nodes[0], before_dip3_mn)
mns.append(before_dip3_mn)

# block 550 starts enforcing DIP3 MN payments
Expand Down Expand Up @@ -96,6 +97,7 @@ def run_test(self):
else:
self.log.info("create_collateral %s" % mn.alias)
create_mn_collateral(self.nodes[0], mn)
lock_mn_collateral(self.nodes[0], mn)
self.log.info("register %s" % mn.alias)
register_mn(self.nodes[0], mn)

Expand Down Expand Up @@ -191,6 +193,7 @@ def run_test(self):
# self.test_instantsend(10, 3, timeout=20)

def spend_mn_collateral(self, mn, with_dummy_input_output=False):
unlock_mn_collateral(self.nodes[0], mn)
return self.spend_input(mn.collateral_txid, mn.collateral_vout, 1000, with_dummy_input_output)

def update_mn_payee(self, mn, payee):
Expand Down
6 changes: 6 additions & 0 deletions qa/rpc-tests/test_framework/mn_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ def create_mn_collateral(node, mn):
break
assert(mn.collateral_vout != -1)

def lock_mn_collateral(node, mn):
node.lockunspent(False, [{'txid': mn.collateral_txid, 'vout': mn.collateral_vout}])

def unlock_mn_collateral(node, mn):
node.lockunspent(True, [{'txid': mn.collateral_txid, 'vout': mn.collateral_vout}])

# register a protx MN and also fund it (using collateral inside ProRegTx)
def register_fund_mn(node, mn):
node.sendtoaddress(mn.fundsAddr, 1000.001)
Expand Down
19 changes: 3 additions & 16 deletions qa/rpc-tests/txn_clone.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,7 @@ def run_test(self):
# Coins are sent to node1_address
node1_address = self.nodes[1].getnewaddress("from0")

# Send tx1, and another transaction tx2 that won't be cloned
txid1 = self.nodes[0].sendfrom("foo", node1_address, 40, 0)
txid2 = self.nodes[0].sendfrom("bar", node1_address, 20, 0)

# Construct a clone of tx1, to be malleated
rawtx1 = self.nodes[0].getrawtransaction(txid1,1)
Expand Down Expand Up @@ -84,51 +82,43 @@ def run_test(self):
sync_blocks(self.nodes[0:2])

tx1 = self.nodes[0].gettransaction(txid1)
tx2 = self.nodes[0].gettransaction(txid2)

# Node0's balance should be starting balance, plus 40BTC for another
# matured block, minus tx1 and tx2 amounts, and minus transaction fees:
expected = starting_balance + fund_foo_tx["fee"] + fund_bar_tx["fee"]
if self.options.mine_block: expected += 40
expected += tx1["amount"] + tx1["fee"]
expected += tx2["amount"] + tx2["fee"]
assert_equal(self.nodes[0].getbalance(), expected)

# foo and bar accounts should be debited:
assert_equal(self.nodes[0].getbalance("foo", 0), 969 + tx1["amount"] + tx1["fee"])
assert_equal(self.nodes[0].getbalance("bar", 0), 29 + tx2["amount"] + tx2["fee"])

if self.options.mine_block:
assert_equal(tx1["confirmations"], 1)
assert_equal(tx2["confirmations"], 1)
# Node1's "from0" balance should be both transaction amounts:
assert_equal(self.nodes[1].getbalance("from0"), -(tx1["amount"] + tx2["amount"]))
assert_equal(self.nodes[1].getbalance("from0"), -(tx1["amount"]))
else:
assert_equal(tx1["confirmations"], 0)
assert_equal(tx2["confirmations"], 0)

# Send clone and its parent to miner
self.nodes[2].sendrawtransaction(fund_foo_tx["hex"])
self.nodes[2].sendrawtransaction(fund_bar_tx["hex"])
txid1_clone = self.nodes[2].sendrawtransaction(tx1_clone["hex"])
# ... mine a block...
self.nodes[2].generate(1)

# Reconnect the split network, and sync chain:
connect_nodes_bi(self.nodes, 1, 2)
self.nodes[2].sendrawtransaction(fund_bar_tx["hex"])
self.nodes[2].sendrawtransaction(tx2["hex"])
self.nodes[2].generate(1) # Mine another block to make sure we sync
sync_blocks(self.nodes)

# Re-fetch transaction info:
tx1 = self.nodes[0].gettransaction(txid1)
tx1_clone = self.nodes[0].gettransaction(txid1_clone)
tx2 = self.nodes[0].gettransaction(txid2)

# Verify expected confirmations
assert_equal(tx1["confirmations"], -2)
assert_equal(tx1_clone["confirmations"], 2)
assert_equal(tx2["confirmations"], 1)

# Check node0's total balance; should be same as before the clone, + 100 BTC for 2 matured,
# less possible orphaned matured subsidy
Expand All @@ -141,8 +131,6 @@ def run_test(self):
# Check node0's individual account balances.
# "foo" should have been debited by the equivalent clone of tx1
assert_equal(self.nodes[0].getbalance("foo"), 969 + tx1["amount"] + tx1["fee"])
# "bar" should have been debited by (possibly unconfirmed) tx2
assert_equal(self.nodes[0].getbalance("bar", 0), 29 + tx2["amount"] + tx2["fee"])
# "" should have starting balance, less funding txes, plus subsidies
assert_equal(self.nodes[0].getbalance("", 0), starting_balance
- 969
Expand All @@ -152,8 +140,7 @@ def run_test(self):
+ 80)

# Node1's "from0" account balance
assert_equal(self.nodes[1].getbalance("from0", 0), -(tx1["amount"] + tx2["amount"]))
assert_equal(self.nodes[1].getbalance("from0", 0), -(tx1["amount"]))

if __name__ == '__main__':
TxnMallTest().main()

2 changes: 2 additions & 0 deletions qa/rpc-tests/txn_doublespend.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@ def run_test(self):
node0_address_foo = self.nodes[0].getnewaddress("foo")
fund_foo_txid = self.nodes[0].sendfrom("", node0_address_foo, 969)
fund_foo_tx = self.nodes[0].gettransaction(fund_foo_txid)
self.nodes[0].lockunspent(False, [{"txid": fund_foo_txid, "vout": find_output(self.nodes[0], fund_foo_txid, 969)}])

node0_address_bar = self.nodes[0].getnewaddress("bar")
fund_bar_txid = self.nodes[0].sendfrom("", node0_address_bar, 29)
fund_bar_tx = self.nodes[0].gettransaction(fund_bar_txid)
self.nodes[0].lockunspent(True, [{"txid": fund_foo_txid, "vout": find_output(self.nodes[0], fund_foo_txid, 969)}])

assert_equal(self.nodes[0].getbalance(""),
starting_balance - 969 - 29 + fund_foo_tx["fee"] + fund_bar_tx["fee"])
Expand Down
3 changes: 2 additions & 1 deletion src/Makefile.test.include
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,8 @@ BITCOIN_TESTS += \
wallet/test/spark_tests.cpp \
wallet/test/sigma_tests.cpp \
wallet/test/mnemonic_tests.cpp \
wallet/test/txbuilder_tests.cpp
wallet/test/sigmatxbuilder_tests.cpp \
wallet/test/createtransaction_tests.cpp
endif

test_test_bitcoin_LDADD = $(LIBBITCOIN_SERVER) -ltor
Expand Down
6 changes: 3 additions & 3 deletions src/llmq/quorums_instantsend.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,6 @@ class CInstantSendDb
class CInstantSendManager : public CRecoveredSigsListener
{
private:
CCriticalSection cs;
CInstantSendDb db;

std::thread workThread;
CThreadInterrupt workInterrupt;

Expand Down Expand Up @@ -112,6 +109,9 @@ class CInstantSendManager : public CRecoveredSigsListener
std::unordered_set<uint256, StaticSaltedHasher> pendingRetryTxs;
std::atomic_bool isNewInstantSendEnabled{false};

CCriticalSection cs;
CInstantSendDb db;
Comment on lines +112 to +113
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving CCriticalSection cs and CInstantSendDb db from private to public sections in CInstantSendManager introduces potential risks related to encapsulation and data integrity. Public members can be modified from outside the class, which could lead to race conditions or inconsistent states. Consider using getter methods or friend classes to maintain encapsulation and protect the integrity of these members.


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the only way to implement the test? Wallet changes can certainly be done with an extra member function.

public:
CInstantSendManager(CDBWrapper& _llmqDb);
~CInstantSendManager();
Expand Down
4 changes: 0 additions & 4 deletions src/primitives/transaction.h
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,6 @@ class CTxOut
public:
CAmount nValue;
CScript scriptPubKey;
int nRounds;

CTxOut()
{
Expand All @@ -186,15 +185,12 @@ class CTxOut
inline void SerializationOp(Stream& s, Operation ser_action) {
READWRITE(nValue);
READWRITE(*(CScriptBase*)(&scriptPubKey));
if (ser_action.ForRead())
nRounds = -10;
}

void SetNull()
{
nValue = -1;
scriptPubKey.clear();
nRounds = -10; // an initial value, should be no way to get this by calculations
}

bool IsNull() const
Expand Down
10 changes: 10 additions & 0 deletions src/script/script.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,16 @@ unsigned int CScript::GetSigOpCount(const CScript& scriptSig) const
return subscript.GetSigOpCount(true);
}

bool CScript::IsPayToPublicKey() const {
if (size() != 35) return false;

opcodetype opcode;
const_iterator pc = begin();
GetOp(pc, opcode);
GetOp(pc, opcode);
return opcode == OP_CHECKSIG;
}
Comment on lines +220 to +228
Copy link

@coderabbitai coderabbitai bot Jul 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness and Logic: Verify initial opcode.

The method correctly identifies a subset of pay-to-public-key scripts but does not account for the initial opcodes that push the public key onto the stack. A more robust check would involve verifying that the script starts with an appropriate push operation followed by OP_CHECKSIG.

bool CScript::IsPayToPublicKey() const {
    if (size() != 35) return false;

    opcodetype opcode;
    const_iterator pc = begin();
+   // Expect the script to start with a push operation of the public key (33 bytes) followed by OP_CHECKSIG
+   if (!GetOp(pc, opcode) || opcode != OP_PUSHDATA1) return false;
    GetOp(pc, opcode);
    return opcode == OP_CHECKSIG;
}
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
bool CScript::IsPayToPublicKey() const {
if (size() != 35) return false;
opcodetype opcode;
const_iterator pc = begin();
GetOp(pc, opcode);
GetOp(pc, opcode);
return opcode == OP_CHECKSIG;
}
bool CScript::IsPayToPublicKey() const {
if (size() != 35) return false;
opcodetype opcode;
const_iterator pc = begin();
// Expect the script to start with a push operation of the public key (33 bytes) followed by OP_CHECKSIG
if (!GetOp(pc, opcode) || opcode != OP_PUSHDATA1) return false;
GetOp(pc, opcode);
return opcode == OP_CHECKSIG;
}

Performance: Directly access relevant opcodes.

The method iterates through the script opcodes without checking the first opcode directly. Since the script format for a pay-to-public-key is relatively fixed, directly accessing the relevant opcodes could improve readability and performance.

bool CScript::IsPayToPublicKey() const {
    if (size() != 35) return false;

    opcodetype opcode;
    const_iterator pc = begin();
    if (!GetOp(pc, opcode) || opcode != OP_PUSHDATA1) return false;
    return GetOp(pc, opcode) && opcode == OP_CHECKSIG;
}
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
bool CScript::IsPayToPublicKey() const {
if (size() != 35) return false;
opcodetype opcode;
const_iterator pc = begin();
GetOp(pc, opcode);
GetOp(pc, opcode);
return opcode == OP_CHECKSIG;
}
bool CScript::IsPayToPublicKey() const {
if (size() != 35) return false;
opcodetype opcode;
const_iterator pc = begin();
if (!GetOp(pc, opcode) || opcode != OP_PUSHDATA1) return false;
return GetOp(pc, opcode) && opcode == OP_CHECKSIG;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a valid suggestion:

+   // Expect the script to start with a push operation of the public key (33 bytes) followed by OP_CHECKSIG
+   if (!GetOp(pc, opcode) || opcode != OP_PUSHDATA1) return false;

Second suggestion about optimization is not that valid. Any compiler will optimize this.

@psolstice I can change the code an submit or we can keep what we have. . But depends on your final decision.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


bool CScript::IsNormalPaymentScript() const
{
if(this->size() != 25) return false;
Expand Down
1 change: 1 addition & 0 deletions src/script/script.h
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,7 @@ class CScript : public CScriptBase
unsigned int GetSigOpCount(const CScript& scriptSig) const;
bool IsNormalPaymentScript() const;

bool IsPayToPublicKey() const;
bool IsPayToPublicKeyHash() const;
bool IsPayToExchangeAddress() const;

Expand Down
6 changes: 3 additions & 3 deletions src/sync.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
#include "utilstrencodings.h"

#include <stdio.h>

#include <set>
#include <boost/foreach.hpp>
#include <boost/thread.hpp>

Expand Down Expand Up @@ -67,7 +67,7 @@ struct CLockLocation {

typedef std::vector<std::pair<void*, CLockLocation> > LockStack;
typedef std::map<std::pair<void*, void*>, LockStack> LockOrders;
typedef std::set<std::pair<void*, void*> > InvLockOrders;
typedef std::set<std::pair<void*, void*>> InvLockOrders;

struct LockData {
// Very ugly hack: as the global constructs and destructors run single
Expand Down Expand Up @@ -174,7 +174,7 @@ void AssertLockNotHeldInternal(const char* pszName, const char* pszFile, int nLi
if (i.first == cs) {
fprintf(stderr, "Assertion failed: lock %s held in %s:%i; locks held:\n%s", pszName, pszFile, nLine, LocksHeld().c_str());
abort();
}
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/test/test_bitcoin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ TestingSetup::TestingSetup(const std::string& chainName, std::string suf) : Basi

// Init HD mint

// Create new keyUser and set as default key
// generate a new master key
pwalletMain->GenerateNewMnemonic();

CPubKey masterPubKey = pwalletMain->GenerateNewHDMasterKey();
pwalletMain->SetHDMasterKey(masterPubKey);
CPubKey newDefaultKey;
Expand Down
1 change: 1 addition & 0 deletions src/validation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1196,6 +1196,7 @@ bool AcceptToMemoryPoolWorker(CTxMemPool& pool, CValidationState& state, const C
if (pfMissingInputs) {
*pfMissingInputs = true;
}
LogPrintf("%s(): Couldn't find input %s-%d for tx %s\n", __func__, txin.prevout.hash.GetHex(), txin.prevout.n, tx.GetHash().GetHex());
aleflm marked this conversation as resolved.
Show resolved Hide resolved
return false; // fMissingInputs and !state.IsInvalid() is used to detect this condition, don't set state.Invalid()
}
}
Expand Down
18 changes: 17 additions & 1 deletion src/wallet/coincontrol.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#define BITCOIN_WALLET_COINCONTROL_H

#include "primitives/transaction.h"
#include "script/standard.h"
#include <optional>

enum class CoinType
{
Expand All @@ -16,7 +18,8 @@ enum class CoinType
ONLY_1000 = 5, // find znode outputs including locked ones (use with caution)
ONLY_PRIVATESEND_COLLATERAL = 6,
ONLY_MINTS = 7,
WITH_MINTS = 8
WITH_MINTS = 8,
WITH_1000 = 9
};

/** Coin Control Features. */
Expand All @@ -36,12 +39,18 @@ class CCoinControl
CAmount nMinimumTotalFee;
//! Override estimated feerate
bool fOverrideFeeRate;
// Allow inputs from ourself that haven't been confirmed yet.
std::optional<bool> fAllowUnconfirmed;
//! Feerate to use if overrideFeeRate is true
CFeeRate nFeeRate;
//! Override the default confirmation target, 0 = use default
int nConfirmTarget;
//! Controls which types of coins are allowed to be used (default: ALL_COINS)
CoinType nCoinType;
//! No more than this number of inputs may be used.
size_t nMaxInputs;
// The generated transaction may not be over this size.
size_t nMaxSize;

CCoinControl()
{
Expand All @@ -61,6 +70,8 @@ class CCoinControl
fOverrideFeeRate = false;
nConfirmTarget = 0;
nCoinType = CoinType::ALL_COINS;
nMaxInputs = 0;
nMaxSize = 0;
}

bool HasSelected() const
Expand Down Expand Up @@ -93,6 +104,11 @@ class CCoinControl
vOutpoints.assign(setSelected.begin(), setSelected.end());
}

size_t GetSelectedSize() const
{
return setSelected.size();
}

private:
std::set<COutPoint> setSelected;
};
Expand Down
2 changes: 2 additions & 0 deletions src/wallet/rpcwallet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3081,6 +3081,8 @@ UniValue fundrawtransaction(const JSONRPCRequest& request)
setSubtractFeeFromOutputs.insert(pos);
}

LOCK2(cs_main, pwallet->cs_wallet);

CAmount nFeeOut;
std::string strFailReason;

Expand Down
6 changes: 3 additions & 3 deletions src/wallet/sigmaspendbuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
#include <stdexcept>
#include <tuple>

class SigmaSpendSigner : public InputSigner
class SigmaSpendSigner : public SigmaTxBuilderInputSigner
{
public:
const sigma::PrivateCoin coin;
Expand Down Expand Up @@ -108,7 +108,7 @@ static std::unique_ptr<SigmaSpendSigner> CreateSigner(const CSigmaEntry& coin)
}

SigmaSpendBuilder::SigmaSpendBuilder(CWallet& wallet, CHDMintWallet& mintWallet, const CCoinControl *coinControl) :
TxBuilder(wallet),
SigmaTxBuilderSuperclass(wallet),
mintWallet(mintWallet)
{
cs_main.lock();
Expand All @@ -129,7 +129,7 @@ SigmaSpendBuilder::~SigmaSpendBuilder()
cs_main.unlock();
}

CAmount SigmaSpendBuilder::GetInputs(std::vector<std::unique_ptr<InputSigner>>& signers, CAmount required)
CAmount SigmaSpendBuilder::GetInputs(std::vector<std::unique_ptr<SigmaTxBuilderInputSigner>>& signers, CAmount required)
{
// get coins to spend

Expand Down
4 changes: 2 additions & 2 deletions src/wallet/sigmaspendbuilder.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

#include <vector>

class SigmaSpendBuilder : public TxBuilder
class SigmaSpendBuilder : public SigmaTxBuilderSuperclass
{
public:
std::vector<CSigmaEntry> selected;
Expand All @@ -19,7 +19,7 @@ class SigmaSpendBuilder : public TxBuilder
~SigmaSpendBuilder() override;

protected:
CAmount GetInputs(std::vector<std::unique_ptr<InputSigner>>& signers, CAmount required) override;
CAmount GetInputs(std::vector<std::unique_ptr<SigmaTxBuilderInputSigner>>& signers, CAmount required) override;
// remint change
CAmount GetChanges(std::vector<CTxOut>& outputs, CAmount amount, CWalletDB& walletdb) override;

Expand Down
Loading