This allows a user to derive the addresses for some privkey without first having to derive the descriptor checksum.
Alternative to #24161.
263 | [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
264 | {
265 | - RPCTypeCheck(request.params, {UniValue::VSTR, UniValueType()}); // Range argument is checked later
266 | + RPCTypeCheck(request.params, {UniValue::VSTR, UniValueType(), UniValue::VBOOL}); // Range argument is checked later
267 | const std::string desc_str = request.params[0].get_str();
268 | + const bool require_checksum = request.params[2].isNull() || request.params[2].get_bool();
const bool require_checksum{request.params[2].isNull() ? true : request.params[2].get_bool()};
nit: I prefer to print the default value as literal. This makes it easier to change the value without changing the logic.
Makes sense, incorporated.
~Concept NACK~
The purpose of checksum is to ensure the integrity of data and ensure that the data has not been wrongly entered or tampered with.
Though I understand this feature can be helpful for someone regularly transacting with the same key, and hence is familiar with the risk and how to ensure the integrity of the key. Still, the potential risk involved makes this feature not worth the risk.
The purpose of checksum is to ensure the integrity of data and ensure that the data has not been wrongly entered or tampered with.
The descriptor checksum, yes. The WIF format (private key) already has a built-in checksum.
See the alternative, where the descriptor checksum is derived using getdescriptorinfo first, before being passed to deriveaddresses. The scenario is identical, but it requires an extra step due to the checksum requirement.
- Say you use a misspelled private key and used it to generate an address. Any bitcoin received on that address would potentially be unrecoverable and hence burned, which is a potentially very devastating situation.
It would fail. The WIF format includes a checksum. Besides, a WIF privkey that had a typo and still passed the WIF checksum would be a valid private key, and all derived addresses would thus be valid recipients for whoever held that private key.
It would fail. The WIF format includes a checksum.
I checked it, and it seems like it is correct.
I took the private key present in the example to derive addresses on this PR. It worked correctly as expected.

However, when I made the following change to the private key to simulate a typo:
(The tenth digit has been changed from 8 -> 2)
- cPsQTSmMZ8e3AEUWGjS73f5R364yJxH6RxcgnwbHjbKbFPUP2Dtu
+ cPsQTSmMZ2e3AEUWGjS73f5R364yJxH6RxcgnwbHjbKbFPUP2Dtu
The expression failed, showing that the private key was invalid.

I have retracted my NACK from the last review. Let me do some further reviewing before forming a decision on this PR.
I was curious about one thing, though. I went to some forums and saw WIF had been around for a long time now. Why was this redundant 2-step behavior of deriveaddresses was not addressed earlier?
I was curious about one thing, though. I went to some forums and saw WIF had been around for a long time now. Why was this redundant 2-step behavior of
deriveaddresseswas not addressed earlier?
I believe deriveaddresses is fairly new, and the "find all addresses" use case, while useful, was probably not high priority.
<!--e57a25ab6845829454e8d69fc972939a-->
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
<!--174a7506f384e20aa4161008e828411d-->
Reviewers, this pull request conflicts with the following ones:
deriveaddresses to display address information by w0xlt)If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.
242 | @@ -243,6 +243,7 @@ static RPCHelpMan deriveaddresses() 243 | { 244 | {"descriptor", RPCArg::Type::STR, RPCArg::Optional::NO, "The descriptor."}, 245 | {"range", RPCArg::Type::RANGE, RPCArg::Optional::OMITTED_NAMED_ARG, "If a ranged descriptor is used, this specifies the end or the range (in [begin,end] notation) to derive."}, 246 | + {"require_checksum", RPCArg::Type::BOOL, RPCArg::Default{true}, "Fail if the descriptor checksum is invalid or missing."},
Let's always fail if it's invalid. So if the user doesn't provide one and require_checksum is false we're happy, but if they provide a checksum, is must be valid.
That is already the case.
$ ./bitcoin-cli -regtest getdescriptorinfo "combo(cTsWy7DsaN2uKgVueRHcUKAP3mCNzX5r77GsiLWw9C9zapjiK8tQ)"
{
"descriptor": "combo(02fdbfbbfc35d0748ec58f628988090fce16985087e89639bccc4bfec4056323dc)#lc57gg0x",
"checksum": "y9v74p5f",
"isrange": false,
"issolvable": true,
"hasprivatekeys": true
}
$ ./bitcoin-cli -regtest deriveaddresses "combo(02fdbfbbfc35d0748ec58f628988090fce16985087e89639bccc4bfec4056323dc)#lc57gg0x"
[
"mtrCy8QYfDU7tjPnkpWwWdQomuRfjptFy2",
"mtrCy8QYfDU7tjPnkpWwWdQomuRfjptFy2",
"bcrt1qjg7un9j86eld6y3sg0u0scmxedv6hued2z9m89",
"2NDtkyew2s7qD1HYDXkNZTi1HY7nT7ZhB72"
]
$ ./bitcoin-cli -regtest deriveaddresses "combo(02fdbfbbfc35d0748ec58f628988090fce16985087e89639bccc4bfec4056323dc)#lc57gg0x" null false
[
"mtrCy8QYfDU7tjPnkpWwWdQomuRfjptFy2",
"mtrCy8QYfDU7tjPnkpWwWdQomuRfjptFy2",
"bcrt1qjg7un9j86eld6y3sg0u0scmxedv6hued2z9m89",
"2NDtkyew2s7qD1HYDXkNZTi1HY7nT7ZhB72"
]
$ ./bitcoin-cli -regtest deriveaddresses "combo(02fdbfbbfc35d0748ec58f628988090fce16985087e89639bccc4bfec4056323dc)#lc57gg1x" null false
error code: -5
error message:
Provided checksum 'lc57gg1x' does not match computed checksum 'lc57gg0x'
I've updated the description to reflect this.
Concept ACK
242 | @@ -243,6 +243,7 @@ static RPCHelpMan deriveaddresses() 243 | { 244 | {"descriptor", RPCArg::Type::STR, RPCArg::Optional::NO, "The descriptor."}, 245 | {"range", RPCArg::Type::RANGE, RPCArg::Optional::OMITTED_NAMED_ARG, "If a ranged descriptor is used, this specifies the end or the range (in [begin,end] notation) to derive."}, 246 | + {"require_checksum", RPCArg::Type::BOOL, RPCArg::Default{true}, "Fail even if the descriptor checksum is missing. (Always fails if a checksum is present, but invalid.)"},
"Require a checksum. If a checksum is provided it will be checked regardless of this parameter."
251 | @@ -251,14 +252,17 @@ static RPCHelpMan deriveaddresses() 252 | } 253 | }, 254 | RPCExamples{ 255 | - "First three native segwit receive addresses\n" + 256 | + "First three native segwit receive addresses:\n" + 257 | HelpExampleCli("deriveaddresses", "\"" + EXAMPLE_DESCRIPTOR + "\" \"[0,2]\"") + 258 | - HelpExampleRpc("deriveaddresses", "\"" + EXAMPLE_DESCRIPTOR + "\", \"[0,2]\"") 259 | + HelpExampleRpc("deriveaddresses", "\"" + EXAMPLE_DESCRIPTOR + "\", \"[0,2]\"") + 260 | + "Derive all addresses from a privkey cPsQTSmMZ8e3AEUWGjS73f5R364yJxH6RxcgnwbHjbKbFPUP2Dtu:\n" +
"Derive all addresses from a WIF, which has a built-in checksum:\n"
"a WIF privkey" seems more correct to me, but going with your suggestion.
utACK 8d3c91d04931c9a2ad44ca04175944c617fe149e
Note that there are several things we typically put in a descriptor that already have a checksum, namely the xpriv and xpub. The main scenario where it doesn't make sense to use a descriptor checksum, is when you have to generate the descriptor manually. That's clearly the case if you import a WIF from some older software or a paper backup. But there may be watch-only scenario's too, as many wallet software supports the export of a single xpub. That's why I removed the emphasis on "private key" in the help text below.
re-utACK e34c8e1b63628986d496b5f7bb29addac50e459a
242 | @@ -243,6 +243,7 @@ static RPCHelpMan deriveaddresses() 243 | { 244 | {"descriptor", RPCArg::Type::STR, RPCArg::Optional::NO, "The descriptor."}, 245 | {"range", RPCArg::Type::RANGE, RPCArg::Optional::OMITTED_NAMED_ARG, "If a ranged descriptor is used, this specifies the end or the range (in [begin,end] notation) to derive."}, 246 | + {"require_checksum", RPCArg::Type::BOOL, RPCArg::Default{true}, "Require a checksum. If a checksum is provided it will be checked regardless of this parameter."},
This wouldn't ordinarily be needed, and booleans in general aren't very clear as positional arguments, so it should use an options Object instead.
Switched to an options object.
Concept ACK
Changes since my last review:
require_checksum has been moved from a positional argument to an option. This is done according to the reasoning given by @luke-jr in the above comment, with which I agree.require_checksum help message. I prefer the new wordings as it better explains the working if the checksum is provided.I agree with the idea of keeping the checksum as an optional part for deriving address than as a mandatory part. Since the WIF already has an in-built checksum, it doesn't necessarily need the redundant checksum part. These changes allow the user to remove the extra step of getting the checksum before deriving the address. This PR also preserves the old behavior if a user prefers to take the traditional approach of deriving addresses.
I was NOT able to test the working of this RPC successfully. I used the string given in the example to test deriveaddresses, but it failed with the following error.
error: Error parsing JSON: {require_checksum:
Screenshot:

Alternatively you can do "{\"require_checksum\": false}".
Both of these methods worked correctly:
@kallewoof
I think the help message should be updated to indicate one of these methods:
'{"require_checksum": false}'"{\"require_checksum\": false}"
@SjorsThis is what makes option objects a bit annoying to use.
Do we have any alternatives to the options object? Or some way to bypass this annoying behavior?
@shaavan Good point, I've updated the help text to use single quotes.
Do we have any alternatives to the options object? Or some way to bypass this annoying behavior?
Ultimately having an options object rather than stacking on new parameters as we go turns out to be a generally better and more easy to use approach. I think you'll get used to it eventually. :)
ACK 5a9587c5701a4df51c11cea075128ca6e6898681
Changes since my last review:
I was able to compile the updated PR on Ubuntu 20.04 successfully. The example is working perfectly as well.
Screenshot:
@Sjors
There's #15448
Let me take a look at it.
tACK 04dfe24609fb4dde16c8ba4c314fe9653ef65741
242 | @@ -243,6 +243,13 @@ static RPCHelpMan deriveaddresses() 243 | { 244 | {"descriptor", RPCArg::Type::STR, RPCArg::Optional::NO, "The descriptor."}, 245 | {"range", RPCArg::Type::RANGE, RPCArg::Optional::OMITTED_NAMED_ARG, "If a ranged descriptor is used, this specifies the end or the range (in [begin,end] notation) to derive."}, 246 | + 247 | + {"options", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED_NAMED_ARG, "", 248 | + { 249 | + {"require_checksum", RPCArg::Type::BOOL, RPCArg::Default{true}, "Require a checksum. If a checksum is provided it will be checked regardless of this parameter."},
nit
{"require_checksum", RPCArg::Type::BOOL, RPCArg::Default{true}, "Require a checksum. If a checksum is provided it will be verified regardless of this parameter."},
Fixed.
257 | @@ -251,14 +258,30 @@ static RPCHelpMan deriveaddresses() 258 | } 259 | }, 260 | RPCExamples{ 261 | - "First three native segwit receive addresses\n" + 262 | + "First three native segwit receive addresses:\n" + 263 | HelpExampleCli("deriveaddresses", "\"" + EXAMPLE_DESCRIPTOR + "\" \"[0,2]\"") + 264 | - HelpExampleRpc("deriveaddresses", "\"" + EXAMPLE_DESCRIPTOR + "\", \"[0,2]\"") 265 | + HelpExampleRpc("deriveaddresses", "\"" + EXAMPLE_DESCRIPTOR + "\", \"[0,2]\"") + 266 | + "Derive all addresses from a WIF, which has a built-in checksum:\n" +
I don't think we should have examples without a sane use case. Couldn't this example just as well derive only a single address?
Is it not sane though? A user might not remember which address type they used, which is what combo is for. Though I guess it's likely to have been pkh()
It's literally the use case I needed to figure out the various permutations for the test case I was writing. I can agree that it's probably unusual but still sane.
I thought combo was for backward compatibility with the undesirable behaviour of legacy wallets being unable to differentiate?
In any case, having it as an example is effectively encouraging it or suggesting it, which really doesn't make sense.
I think a frequently asked question is "how do I get the bitcoin address(es) from a given private key", actually, so providing an answer to that question in the documentation doesn't seem off to me, even if it's not exactly best practice to do that on the regular. I personally had no idea about combo and ended up wasting my time writing #24160 for no good reason.
If somebody dumped a private key from an old core wallet, rather than make a regular backup, they might be in this situation where they used the SegWit address. There might also be other wallets out there that use WIF keys without specifying the address (though I suspect most adopted BIP39 before SegWit).
That said, it might be still be better to recommend that users specify which address type they want to derive, which you could illustrate with two examples, e.g. pkh(WIF) and sh(wpkh(WIF).
pkh would indeed "derive only a single address" (and it's the most likely use case for anyone with a pre-BIP39 paper wallet, e.g. from 2011-2014)
(if exchanges dedicated an FAQ to it, it's probably a thing: https://bitonic.nl/en/faq/26/i-have-created-a-bitcoin-address-on-bitaddressorg-how-can-i-sell-my-bitcoins)
Switched to pkh() example.
"checked" → "verified".
re-utACK 1345e8d817acd316b71f23315f0996580253ade3
This allows a user to derive the addresses for some privkey without first having to derive the descriptor checksum.
CI failures seem spurious.
Thanks, didn't realize there were failures. Re-ran → passed.
reACK 97a69e232be707768b6fec3539bdb825235d8d26
Changes since my last review:
pkh() example instead of combo(). I think that is the right approach as this encourages the user to specify which type of address they would like to derive.Code Review ACK https://github.com/bitcoin/bitcoin/pull/24162/commits/97a69e232be707768b6fec3539bdb825235d8d26
Perhaps require_checksum could be a parameter by itself instead of being the only item in options.
Thanks for the review!
Perhaps
require_checksumcould be a parameter by itself instead of being the only item inoptions.
The idea is that any new parameters in the future will also go into options. That turns out to be easier when you e.g. want to deprecate a parameter (see e.g. the first argument to getbalance which had to be converted into a 'dummy' argument, to avoid having to change the indices of arguments).
<!--cf906140f33d8803c4a75a2196329ecb-->
🐙 This pull request conflicts with the target branch and needs rebase.
<sub>Want to unsubscribe from rebase notifications on this pull request? Just convert this pull request to a "draft".</sub>
<!--13523179cfe9479db18ec6c5d236f789-->
There hasn't been much activity lately and the patch still needs rebase. What is the status here?
<!--13523179cfe9479db18ec6c5d236f789-->
There hasn't been much activity lately and the patch still needs rebase. What is the status here?