Skip to content

Commit 2700f09

Browse files
committed
rpc: signer: add enumeratesigners to list external signers
1 parent 07b7c94 commit 2700f09

File tree

5 files changed

+157
-16
lines changed

5 files changed

+157
-16
lines changed

src/wallet/external_signer.cpp

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,57 @@
22
// Distributed under the MIT software license, see the accompanying
33
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
44

5+
#include <chainparams.h>
56
#include <wallet/external_signer.h>
6-
#include <util/system.h>
77

8-
ExternalSigner::ExternalSigner(const std::string& command, const std::string& fingerprint): m_command(command), m_fingerprint(fingerprint) {}
8+
ExternalSigner::ExternalSigner(const std::string& command, const std::string& fingerprint, std::string chain, std::string name): m_command(command), m_fingerprint(fingerprint), m_chain(chain), m_name(name) {}
9+
10+
const std::string ExternalSigner::NetworkArg() const
11+
{
12+
return " --chain " + m_chain;
13+
}
14+
15+
#ifdef ENABLE_EXTERNAL_SIGNER
16+
17+
bool ExternalSigner::Enumerate(const std::string& command, std::vector<ExternalSigner>& signers, std::string chain, bool ignore_errors)
18+
{
19+
// Call <command> enumerate
20+
const UniValue result = RunCommandParseJSON(command + " enumerate");
21+
if (!result.isArray()) {
22+
if (ignore_errors) return false;
23+
throw ExternalSignerException(strprintf("'%s' received invalid response, expected array of signers", command));
24+
}
25+
for (UniValue signer : result.getValues()) {
26+
// Check for error
27+
const UniValue& error = find_value(signer, "error");
28+
if (!error.isNull()) {
29+
if (ignore_errors) return false;
30+
if (!error.isStr()) {
31+
throw ExternalSignerException(strprintf("'%s' error", command));
32+
}
33+
throw ExternalSignerException(strprintf("'%s' error: %s", command, error.getValStr()));
34+
}
35+
// Check if fingerprint is present
36+
const UniValue& fingerprint = find_value(signer, "fingerprint");
37+
if (fingerprint.isNull()) {
38+
if (ignore_errors) return false;
39+
throw ExternalSignerException(strprintf("'%s' received invalid response, missing signer fingerprint", command));
40+
}
41+
std::string fingerprintStr = fingerprint.get_str();
42+
// Skip duplicate signer
43+
bool duplicate = false;
44+
for (ExternalSigner signer : signers) {
45+
if (signer.m_fingerprint.compare(fingerprintStr) == 0) duplicate = true;
46+
}
47+
if (duplicate) break;
48+
std::string name = "";
49+
const UniValue& model_field = find_value(signer, "model");
50+
if (model_field.isStr() && model_field.getValStr() != "") {
51+
name += model_field.getValStr();
52+
}
53+
signers.push_back(ExternalSigner(command, fingerprintStr, chain, name));
54+
}
55+
return true;
56+
}
57+
58+
#endif

src/wallet/external_signer.h

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <stdexcept>
99
#include <string>
1010
#include <univalue.h>
11+
#include <util/system.h>
1112

1213
class ExternalSignerException : public std::runtime_error {
1314
public:
@@ -25,10 +26,30 @@ class ExternalSigner
2526
public:
2627
//! @param[in] command the command which handles interaction with the external signer
2728
//! @param[in] fingerprint master key fingerprint of the signer
28-
ExternalSigner(const std::string& command, const std::string& fingerprint);
29+
//! @param[in] chain "main", "test", "regtest" or "signet"
30+
//! @param[in] name device name
31+
ExternalSigner(const std::string& command, const std::string& fingerprint, std::string chain, std::string name);
2932

3033
//! Master key fingerprint of the signer
3134
std::string m_fingerprint;
35+
36+
//! Bitcoin mainnet, testnet, etc
37+
std::string m_chain;
38+
39+
//! Name of signer
40+
std::string m_name;
41+
42+
const std::string NetworkArg() const;
43+
44+
#ifdef ENABLE_EXTERNAL_SIGNER
45+
//! Obtain a list of signers. Calls `<command> enumerate`.
46+
//! @param[in] command the command which handles interaction with the external signer
47+
//! @param[in,out] signers vector to which new signers (with a unique master key fingerprint) are added
48+
//! @param chain "main", "test", "regtest" or "signet"
49+
//! @param[out] success Boolean
50+
static bool Enumerate(const std::string& command, std::vector<ExternalSigner>& signers, std::string chain, bool ignore_errors = false);
51+
52+
#endif
3253
};
3354

3455
#endif // BITCOIN_WALLET_EXTERNAL_SIGNER_H

src/wallet/rpcsigner.cpp

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,69 @@
22
// Distributed under the MIT software license, see the accompanying
33
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
44

5+
#include <chainparamsbase.h>
56
#include <rpc/server.h>
7+
#include <rpc/util.h>
68
#include <util/strencodings.h>
79
#include <wallet/rpcsigner.h>
10+
#include <wallet/rpcwallet.h>
811
#include <wallet/wallet.h>
912

1013
#ifdef ENABLE_EXTERNAL_SIGNER
1114

12-
// CRPCCommand table won't compile with an empty array
13-
static RPCHelpMan dummy()
15+
static RPCHelpMan enumeratesigners()
1416
{
15-
return RPCHelpMan{"dummy",
16-
"\nDoes nothing.\n"
17-
"",
18-
{},
19-
RPCResult{RPCResult::Type::NONE, "", ""},
20-
RPCExamples{""},
21-
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
22-
{
23-
return NullUniValue;
24-
},
17+
return RPCHelpMan{
18+
"enumeratesigners",
19+
"Returns a list of external signers from -signer.",
20+
{},
21+
RPCResult{
22+
RPCResult::Type::OBJ, "", "",
23+
{
24+
{RPCResult::Type::ARR, "signers", /* optional */ false, "",
25+
{
26+
{RPCResult::Type::STR_HEX, "masterkeyfingerprint", "Master key fingerprint"},
27+
{RPCResult::Type::STR, "name", "Device name"},
28+
},
29+
}
30+
}
31+
},
32+
RPCExamples{""},
33+
[](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue {
34+
std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
35+
if (!wallet) return NullUniValue;
36+
37+
const std::string command = gArgs.GetArg("-signer", "");
38+
if (command == "") throw JSONRPCError(RPC_WALLET_ERROR, "Error: restart bitcoind with -signer=<cmd>");
39+
std::string chain = gArgs.GetChainName();
40+
UniValue signers_res = UniValue::VARR;
41+
try {
42+
std::vector<ExternalSigner> signers;
43+
ExternalSigner::Enumerate(command, signers, chain);
44+
for (ExternalSigner signer : signers) {
45+
UniValue signer_res = UniValue::VOBJ;
46+
signer_res.pushKV("fingerprint", signer.m_fingerprint);
47+
signer_res.pushKV("name", signer.m_name);
48+
signers_res.push_back(signer_res);
49+
}
50+
} catch (const ExternalSignerException& e) {
51+
throw JSONRPCError(RPC_WALLET_ERROR, e.what());
52+
}
53+
UniValue result(UniValue::VOBJ);
54+
result.pushKV("signers", signers_res);
55+
return result;
56+
}
2557
};
2658
}
2759

2860
Span<const CRPCCommand> GetSignerRPCCommands()
2961
{
62+
3063
// clang-format off
3164
static const CRPCCommand commands[] =
3265
{ // category actor (function)
3366
// --------------------- ------------------------
34-
{ "signer", &dummy, },
67+
{ "signer", &enumeratesigners, },
3568
};
3669
// clang-format on
3770
return MakeSpan(commands);

test/functional/mocks/signer.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,16 @@ def perform_pre_checks():
1717
sys.stdout.write(mock_result[2:])
1818
sys.exit(int(mock_result[0]))
1919

20+
def enumerate(args):
21+
sys.stdout.write(json.dumps([{"fingerprint": "00000001", "type": "trezor", "model": "trezor_t"}, {"fingerprint": "00000002"}]))
22+
2023
parser = argparse.ArgumentParser(prog='./signer.py', description='External signer mock')
2124
subparsers = parser.add_subparsers(description='Commands', dest='command')
2225
subparsers.required = True
2326

27+
parser_enumerate = subparsers.add_parser('enumerate', help='list available signers')
28+
parser_enumerate.set_defaults(func=enumerate)
29+
2430
args = parser.parse_args()
2531

2632
perform_pre_checks()

test/functional/wallet_signer.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,36 @@ def clear_mock_result(self, node):
4848
def run_test(self):
4949
self.log.debug(f"-signer={self.mock_signer_path()}")
5050

51+
assert_raises_rpc_error(-4, 'Error: restart bitcoind with -signer=<cmd>',
52+
self.nodes[0].enumeratesigners
53+
)
54+
55+
# Handle script missing:
56+
assert_raises_rpc_error(-1, 'execve failed: No such file or directory',
57+
self.nodes[2].enumeratesigners
58+
)
59+
60+
# Handle error thrown by script
61+
self.set_mock_result(self.nodes[1], "2")
62+
assert_raises_rpc_error(-1, 'RunCommandParseJSON error',
63+
self.nodes[1].enumeratesigners
64+
)
65+
self.clear_mock_result(self.nodes[1])
66+
67+
self.set_mock_result(self.nodes[1], '0 [{"type": "trezor", "model": "trezor_t", "error": "fingerprint not found"}]')
68+
assert_raises_rpc_error(-4, 'fingerprint not found',
69+
self.nodes[1].enumeratesigners
70+
)
71+
self.clear_mock_result(self.nodes[1])
72+
73+
# Create new wallets with private keys disabled:
74+
self.nodes[1].createwallet(wallet_name='hww', disable_private_keys=True, descriptors=True)
75+
hww = self.nodes[1].get_wallet_rpc('hww')
76+
77+
result = hww.enumeratesigners()
78+
assert_equal(len(result['signers']), 2)
79+
assert_equal(result['signers'][0]["fingerprint"], "00000001")
80+
assert_equal(result['signers'][0]["name"], "trezor_t")
81+
5182
if __name__ == '__main__':
5283
SignerTest().main()

0 commit comments

Comments
 (0)