Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions doc/release-notes-28414.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
RPC Wallet
Comment thread
knst marked this conversation as resolved.
----------

- RPC `walletprocesspsbt` return object now includes field `hex` (if the transaction
is complete) containing the serialized transaction suitable for RPC `sendrawtransaction`. (#28414)
25 changes: 18 additions & 7 deletions src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,11 @@ static const char* DEFAULT_ASMAP_FILENAME="ip_asn.map";
* The PID file facilities.
*/
static const char* BITCOIN_PID_FILENAME = "dashd.pid";
/**
* True if this process has created a PID file.
* Used to determine whether we should remove the PID file on shutdown.
*/
static bool g_generated_pid{false};

static fs::path GetPidFile(const ArgsManager& args)
{
Expand All @@ -198,12 +203,24 @@ static fs::path GetPidFile(const ArgsManager& args)
#else
tfm::format(file, "%d\n", getpid());
#endif
g_generated_pid = true;
return true;
} else {
return InitError(strprintf(_("Unable to create the PID file '%s': %s"), fs::PathToString(GetPidFile(args)), SysErrorString(errno)));
}
}

static void RemovePidFile(const ArgsManager& args)
{
if (!g_generated_pid) return;
const auto pid_path{GetPidFile(args)};
if (std::error_code error; !fs::remove(pid_path, error)) {
std::string msg{error ? error.message() : "File does not exist"};
LogPrintf("Unable to remove PID file (%s): %s\n", fs::PathToString(pid_path), msg);
}
}


//////////////////////////////////////////////////////////////////////////////
//
// Shutdown
Expand Down Expand Up @@ -475,13 +492,7 @@ void Shutdown(NodeContext& node)
node.chainman.reset();
node.scheduler.reset();

try {
if (!fs::remove(GetPidFile(*node.args))) {
LogPrintf("%s: Unable to remove PID file: File does not exist\n", __func__);
}
} catch (const fs::filesystem_error& e) {
LogPrintf("%s: Unable to remove PID file: %s\n", __func__, fsbridge::get_filesystem_error_message(e));
}
RemovePidFile(*node.args);

LogPrintf("%s: done\n", __func__);
}
Expand Down
4 changes: 4 additions & 0 deletions src/qt/test/optiontests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ void OptionTests::migrateSettings()
QVERIFY(!settings.contains("addrSeparateProxyTor"));

std::ifstream file(gArgs.GetDataDirNet() / "settings.json");
std::string default_warning = strprintf("This file is automatically generated and updated by %s. Please do not edit this file while the node "
"is running, as any changes might be ignored or overwritten.",
PACKAGE_NAME);
QCOMPARE(std::string(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()).c_str(), "{\n"
" \"_warning_\": \""+ default_warning+"\",\n"
" \"dbcache\": \"600\",\n"
" \"listen\": false,\n"
" \"onion\": \"onion:234\",\n"
Expand Down
19 changes: 14 additions & 5 deletions src/rpc/mining.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,30 @@ using node::UpdateTime;

/**
* Return average network hashes per second based on the last 'lookup' blocks,
* or from the last difficulty change if 'lookup' is nonpositive.
* If 'height' is nonnegative, compute the estimate at the time when a given block was found.
* or from the last difficulty change if 'lookup' is -1.
* If 'height' is -1, compute the estimate from current chain tip.
* If 'height' is a valid block height, compute the estimate at the time when a given block was found.
*/
static UniValue GetNetworkHashPS(int lookup, int height, const CChain& active_chain) {
if (lookup < -1 || lookup == 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid nblocks. Must be a positive number or -1.");
}

if (height < -1 || height > active_chain.Height()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "Block does not exist at specified height");
}

const CBlockIndex* pb = active_chain.Tip();

if (height >= 0 && height < active_chain.Height()) {
if (height >= 0) {
pb = active_chain[height];
}

if (pb == nullptr || !pb->nHeight)
return 0;

// If lookup is -1, then use blocks since last difficulty change.
if (lookup <= 0)
if (lookup == -1)
lookup = pb->nHeight % Params().GetConsensus().DifficultyAdjustmentInterval() + 1;

// If lookup is larger than chain, then set it to chain length.
Expand Down Expand Up @@ -102,7 +111,7 @@ static RPCHelpMan getnetworkhashps()
"Pass in [blocks] to override # of blocks, -1 specifies since last difficulty change.\n"
"Pass in [height] to estimate the network speed at the time when a certain block was found.\n",
{
{"nblocks", RPCArg::Type::NUM, RPCArg::Default{120}, "The number of blocks, or -1 for blocks since last difficulty change."},
{"nblocks", RPCArg::Type::NUM, RPCArg::Default{120}, "The number of previous blocks to calculate estimate from, or -1 for blocks since last difficulty change."},
{"height", RPCArg::Type::NUM, RPCArg::Default{-1}, "To estimate at the time of the given height."},
},
RPCResult{
Expand Down
4 changes: 3 additions & 1 deletion src/test/settings_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ BOOST_AUTO_TEST_CASE(ReadWrite)
// Check invalid json not allowed
WriteText(path, R"(invalid json)");
BOOST_CHECK(!util::ReadSettings(path, values, errors));
std::vector<std::string> fail_parse = {strprintf("Unable to parse settings file %s", fs::PathToString(path))};
std::vector<std::string> fail_parse = {strprintf("Settings file %s does not contain valid JSON. This is probably caused by disk corruption or a crash, "
"and can be fixed by removing the file, which will reset settings to default values.",
fs::PathToString(path))};
BOOST_CHECK_EQUAL_COLLECTIONS(errors.begin(), errors.end(), fail_parse.begin(), fail_parse.end());
}

Expand Down
15 changes: 14 additions & 1 deletion src/util/settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
#include <fs.h>
#include <util/settings.h>

#if defined(HAVE_CONFIG_H)
#include <config/bitcoin-config.h>
#endif

#include <tinyformat.h>
#include <univalue.h>

Expand Down Expand Up @@ -90,7 +94,9 @@ bool ReadSettings(const fs::path& path, std::map<std::string, SettingsValue>& va

SettingsValue in;
if (!in.read(std::string{std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()})) {
errors.emplace_back(strprintf("Unable to parse settings file %s", fs::PathToString(path)));
errors.emplace_back(strprintf("Settings file %s does not contain valid JSON. This is probably caused by disk corruption or a crash, "
"and can be fixed by removing the file, which will reset settings to default values.",
fs::PathToString(path)));
return false;
}

Expand Down Expand Up @@ -123,6 +129,13 @@ bool WriteSettings(const fs::path& path,
std::vector<std::string>& errors)
{
SettingsValue out(SettingsValue::VOBJ);
// Add auto-generated warning comment only if it does not exist
if (!values.contains("_warning_")) {
out.pushKV("_warning_", strprintf("This file is automatically generated and updated by %s. Please do not edit this file while the node "
"is running, as any changes might be ignored or overwritten.",
PACKAGE_NAME));
Comment on lines +133 to +136

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep the warning field out of runtime settings

After the first settings write, the next startup reads _warning_ into m_settings.rw_settings because ReadSettings() imports every JSON key. ArgsManager::ReadSettingsFile() then treats it as an unknown setting and logs Ignoring unknown rw_settings value _warning_, followed by logging it as a setting, on every normal startup. Filter this metadata key while reading (or before settings validation) so the generated warning is not interpreted as configuration.

Useful? React with 👍 / 👎.

}
// Push settings values
for (const auto& value : values) {
out.__pushKV(value.first, value.second);
}
Expand Down
3 changes: 1 addition & 2 deletions src/wallet/rpc/encrypt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,6 @@ RPCHelpMan encryptwallet()
"After this, any calls that interact with private keys such as sending or signing \n"
"will require the passphrase to be set prior the making these calls.\n"
"Use the walletpassphrase call for this, and then walletlock call.\n"
"If the wallet is already encrypted, use the walletpassphrasechange call.\n",
{
Comment on lines 222 to 223

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Blocking: Incorrect bitcoin#28980 adaptation removes a required initializer separator

The Dash parent and the upstream pre-PR code both ended the RPCHelpMan description with "If the wallet is already encrypted, use the walletpassphrasechange call.\n",. Upstream bitcoin#28980 retained that sentence while moving the terminating comma after its new warning. Dash correctly needs to omit the upstream warning because its HD wallets retain their existing seed, but commit 921eef1 deleted the pre-existing sentence and comma as well. The string literal on line 222 is now followed directly by the argument-list braced initializer on line 223, so the second and third RPCHelpMan constructor arguments are not separated and this translation unit cannot compile. Restore the pre-existing sentence and comma while continuing to omit the inaccurate warning.

Suggested change
"Use the walletpassphrase call for this, and then walletlock call.\n"
"If the wallet is already encrypted, use the walletpassphrasechange call.\n",
{
"Use the walletpassphrase call for this, and then walletlock call.\n"
"If the wallet is already encrypted, use the walletpassphrasechange call.\n",
{

source: ['codex']

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

yes, , is missing now

{"passphrase", RPCArg::Type::STR, RPCArg::Optional::NO, "The pass phrase to encrypt the wallet with. It must be at least 1 character, but should be long."},
},
Expand Down Expand Up @@ -266,7 +265,7 @@ RPCHelpMan encryptwallet()
if (pwallet->IsHDEnabled()) {
return "wallet encrypted; If you forget the passphrase, you will lose access to your funds. Make sure that you have backup of your seed or mnemonic.";
}
return "wallet encrypted; The keypool has been flushed. You need to make a new backup.";
return "wallet encrypted; The keypool has been flushed. You need to make a new backup with the backupwallet RPC.";
},
};
}
Expand Down
9 changes: 9 additions & 0 deletions src/wallet/rpc/spend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1278,6 +1278,7 @@ RPCHelpMan walletprocesspsbt()
{
{RPCResult::Type::STR, "psbt", "The base64-encoded partially signed transaction"},
{RPCResult::Type::BOOL, "complete", "If the transaction has a complete set of signatures"},
{RPCResult::Type::STR_HEX, "hex", /*optional=*/true, "The hex-encoded network transaction if complete"},
}
},
RPCExamples{
Expand Down Expand Up @@ -1327,6 +1328,14 @@ RPCHelpMan walletprocesspsbt()
ssTx << psbtx;
result.pushKV("psbt", EncodeBase64(ssTx.str()));
result.pushKV("complete", complete);
if (complete) {
CMutableTransaction mtx;
// Returns true if complete, which we already think it is.
CHECK_NONFATAL(FinalizeAndExtractPSBT(psbtx, mtx));
CDataStream ssTx_final(SER_NETWORK, PROTOCOL_VERSION);
ssTx_final << mtx;
result.pushKV("hex", HexStr(ssTx_final));
}

return result;
},
Expand Down
5 changes: 4 additions & 1 deletion test/functional/feature_filelock.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ def run_test(self):
expected_msg = f"Error: Cannot obtain a lock on data directory {datadir}. {self.config['environment']['PACKAGE_NAME']} is probably already running."
self.nodes[1].assert_start_raises_init_error(extra_args=[f'-datadir={self.nodes[0].datadir}', '-noserver'], expected_msg=expected_msg)

self.log.info("Check that cookie and PID file are not deleted when attempting to start a second dashd using the same datadir")
cookie_file = Path(datadir) / ".cookie"
assert cookie_file.exists() # should not be deleted during the second bitcoind instance shutdown
assert cookie_file.exists() # should not be deleted during the second dashd instance shutdown
pid_file = Path(datadir) / "dashd.pid"
assert pid_file.exists()

if self.is_wallet_compiled():
def check_wallet_filelock(descriptors):
Expand Down
9 changes: 5 additions & 4 deletions test/functional/feature_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ def run_test(self):
settings = Path(node.datadir, self.chain, "settings.json")
conf = Path(node.datadir, "dash.conf")

# Assert empty settings file was created
# Assert default settings file was created
self.stop_node(0)
default_settings = {"_warning_": "This file is automatically generated and updated by Dash Core. Please do not edit this file while the node is running, as any changes might be ignored or overwritten."}
with settings.open() as fp:
assert_equal(json.load(fp), {})
assert_equal(json.load(fp), default_settings)

# Assert settings are parsed and logged
with settings.open("w") as fp:
Expand All @@ -49,12 +50,12 @@ def run_test(self):

# Assert settings are unchanged after shutdown
with settings.open() as fp:
assert_equal(json.load(fp), {"string": "string", "num": 5, "bool": True, "null": None, "list": [6, 7]})
assert_equal(json.load(fp), {**default_settings, **{"string": "string", "num": 5, "bool": True, "null": None, "list": [6, 7]}})

# Test invalid json
with settings.open("w") as fp:
fp.write("invalid json")
node.assert_start_raises_init_error(expected_msg='Unable to parse settings file', match=ErrorMatch.PARTIAL_REGEX)
node.assert_start_raises_init_error(expected_msg='does not contain valid JSON. This is probably caused by disk corruption or a crash', match=ErrorMatch.PARTIAL_REGEX)

# Test invalid json object
with settings.open("w") as fp:
Expand Down
34 changes: 31 additions & 3 deletions test/functional/rpc_blockchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,18 +428,46 @@ def _test_getdifficulty(self):

def _test_getnetworkhashps(self):
self.log.info("Test getnetworkhashps")
hashes_per_second = self.nodes[0].getnetworkhashps()
assert_raises_rpc_error(
-8,
"Block does not exist at specified height",
lambda: self.nodes[0].getnetworkhashps(100, self.nodes[0].getblockcount() + 1),
)
assert_raises_rpc_error(
-8,
"Block does not exist at specified height",
lambda: self.nodes[0].getnetworkhashps(100, -10),
)
assert_raises_rpc_error(
-8,
"Invalid nblocks. Must be a positive number or -1.",
lambda: self.nodes[0].getnetworkhashps(-100),
)
assert_raises_rpc_error(
-8,
"Invalid nblocks. Must be a positive number or -1.",
lambda: self.nodes[0].getnetworkhashps(0),
)

# Genesis block height estimate should return 0
hashes_per_second = self.nodes[0].getnetworkhashps(100, 0)
assert_equal(hashes_per_second, 0)

# This should be 2 hashes every 2.6 minutes (156 seconds) or 1/78
hashes_per_second = self.nodes[0].getnetworkhashps()
assert abs(hashes_per_second * 78 - 1) < 0.0001

# Test setting the first param of getnetworkhashps to negative value returns the average network
# Test setting the first param of getnetworkhashps to -1 returns the average network
# hashes per second from the last difficulty change.
current_block_height = self.nodes[0].getmininginfo()['blocks']
blocks_since_last_diff_change = current_block_height % DIFFICULTY_ADJUSTMENT_INTERVAL + 1
expected_hashes_per_second_since_diff_change = self.nodes[0].getnetworkhashps(blocks_since_last_diff_change)

assert_equal(self.nodes[0].getnetworkhashps(-1), expected_hashes_per_second_since_diff_change)
assert_equal(self.nodes[0].getnetworkhashps(-2), expected_hashes_per_second_since_diff_change)

# Ensure long lookups get truncated to chain length
hashes_per_second = self.nodes[0].getnetworkhashps(self.nodes[0].getblockcount() + 1000)
assert hashes_per_second > 0.003

def _test_stopatheight(self):
assert_equal(self.nodes[0].getblockcount(), HEIGHT)
Expand Down
32 changes: 20 additions & 12 deletions test/functional/rpc_psbt.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,21 @@ def run_test(self):

self.nodes[0].walletpassphrase(passphrase="password", timeout=1000000)

# Sign the transaction and send
signed_tx = self.nodes[0].walletprocesspsbt(psbt=psbtx, finalize=False)['psbt']
finalized_tx = self.nodes[0].walletprocesspsbt(psbt=psbtx, finalize=True)['psbt']
assert signed_tx != finalized_tx
final_tx = self.nodes[0].finalizepsbt(signed_tx)['hex']
self.nodes[0].sendrawtransaction(final_tx)
# Sign the transaction but don't finalize
processed_psbt = self.nodes[0].walletprocesspsbt(psbt=psbtx, finalize=False)
assert "hex" not in processed_psbt
signed_psbt = processed_psbt['psbt']

# Finalize and send
finalized_hex = self.nodes[0].finalizepsbt(signed_psbt)['hex']
self.nodes[0].sendrawtransaction(finalized_hex)

# Alternative method: sign AND finalize in one command
processed_finalized_psbt = self.nodes[0].walletprocesspsbt(psbt=psbtx, finalize=True)
finalized_psbt = processed_finalized_psbt['psbt']
finalized_psbt_hex = processed_finalized_psbt['hex']
assert signed_psbt != finalized_psbt
assert finalized_psbt_hex == finalized_hex

# Manually selected inputs can be locked:
assert_equal(len(self.nodes[0].listlockunspent()), 0)
Expand Down Expand Up @@ -139,7 +148,7 @@ def run_test(self):
# Check decodepsbt fee calculation (input values shall only be counted once per UTXO)
assert_equal(decoded['fee'], created_psbt['fee'])
assert_equal(walletprocesspsbt_out['complete'], True)
self.nodes[1].sendrawtransaction(self.nodes[1].finalizepsbt(walletprocesspsbt_out['psbt'])['hex'])
self.nodes[1].sendrawtransaction(walletprocesspsbt_out['hex'])

self.log.info("Test walletcreatefundedpsbt fee rate of 10000 sat/vB and 0.1 BTC/kvB produces a total fee at or slightly below -maxtxfee (~0.05290000)")
res1 = self.nodes[1].walletcreatefundedpsbt(inputs, outputs, 0, {"fee_rate": 10000, "add_inputs": True})
Expand Down Expand Up @@ -230,7 +239,7 @@ def run_test(self):
# partially sign with node 2. This should be complete and sendable
walletprocesspsbt_out = self.nodes[2].walletprocesspsbt(psbtx)
assert_equal(walletprocesspsbt_out['complete'], True)
self.nodes[2].sendrawtransaction(self.nodes[2].finalizepsbt(walletprocesspsbt_out['psbt'])['hex'])
self.nodes[2].sendrawtransaction(walletprocesspsbt_out['hex'])

# check that walletprocesspsbt fails to decode a non-psbt
rawtx = self.nodes[1].createrawtransaction([{"txid":txid,"vout":p2pkh_pos}], {self.nodes[1].getnewaddress():9.99})
Expand Down Expand Up @@ -514,14 +523,13 @@ def test_psbt_input_keys(psbt_input, keys):
assert not signed['complete']
signed = self.nodes[0].walletprocesspsbt(signed['psbt'])
assert signed['complete']
self.nodes[0].finalizepsbt(signed['psbt'])

psbt = wallet.walletcreatefundedpsbt([ext_utxo], {self.nodes[0].getnewaddress(): 15}, 0, {"add_inputs": True, "solving_data":{"descriptors": [desc]}})
signed = wallet.walletprocesspsbt(psbt['psbt'])
assert not signed['complete']
signed = self.nodes[0].walletprocesspsbt(signed['psbt'])
assert signed['complete']
final = self.nodes[0].finalizepsbt(signed['psbt'], False)
final = signed['hex']

dec = self.nodes[0].decodepsbt(signed["psbt"])
for i, txin in enumerate(dec["tx"]["vin"]):
Expand Down Expand Up @@ -555,8 +563,8 @@ def test_psbt_input_keys(psbt_input, keys):
)
signed = wallet.walletprocesspsbt(psbt["psbt"])
signed = self.nodes[0].walletprocesspsbt(signed["psbt"])
final = self.nodes[0].finalizepsbt(signed["psbt"])
assert self.nodes[0].testmempoolaccept([final["hex"]])[0]["allowed"]
final = signed["hex"]
assert self.nodes[0].testmempoolaccept([final])[0]["allowed"]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

28414: mark as partial due to missing changes for taproot.

# Reducing the weight should have a lower fee
psbt2 = wallet.walletcreatefundedpsbt(
inputs=[{"txid": ext_utxo["txid"], "vout": ext_utxo["vout"], "size": low_input_weight}],
Expand Down
3 changes: 1 addition & 2 deletions test/functional/wallet_fundrawtransaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -530,8 +530,7 @@ def test_spend_2of2(self):
funded_psbt = wmulti.walletcreatefundedpsbt(inputs=inputs, outputs=outputs, options={'changeAddress': w2.getrawchangeaddress()})['psbt']

signed_psbt = w2.walletprocesspsbt(funded_psbt)
final_psbt = w2.finalizepsbt(signed_psbt['psbt'])
self.nodes[2].sendrawtransaction(final_psbt['hex'])
self.nodes[2].sendrawtransaction(signed_psbt['hex'])
self.generate(self.nodes[2], 1)

# Make sure funds are received at node1.
Expand Down
3 changes: 1 addition & 2 deletions test/functional/wallet_multisig_descriptor_psbt.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,7 @@ def run_test(self):
signing_wallet = participants["signers"][m]
psbt = signing_wallet.walletprocesspsbt(psbt["psbt"])
assert_equal(psbt["complete"], m == self.M - 1)
finalized = coordinator_wallet.finalizepsbt(psbt["psbt"])
coordinator_wallet.sendrawtransaction(finalized["hex"])
coordinator_wallet.sendrawtransaction(psbt["hex"])

self.log.info("Check that balances are correct after the transaction has been included in a block.")
self.generate(self.nodes[0], 1)
Expand Down
Loading
Loading