Skip to content

Commit aa9a3f5

Browse files
committed
Descriptor support in PAK infrastructure and tests
1 parent 3f43dfd commit aa9a3f5

File tree

7 files changed

+312
-86
lines changed

7 files changed

+312
-86
lines changed

src/wallet/rpcdump.cpp

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525

2626
#include <univalue.h>
2727

28+
#include <script/descriptor.h> // getwalletpakinfo
29+
2830

2931
int64_t static DecodeDumpTime(const std::string &str) {
3032
static const boost::posix_time::ptime epoch = boost::posix_time::from_time_t(0);
@@ -1276,8 +1278,6 @@ UniValue importmulti(const JSONRPCRequest& mainRequest)
12761278
return response;
12771279
}
12781280

1279-
extern CTxDestination DeriveBitcoinOfflineAddress(const CExtPubKey& xpub, const uint32_t counter);
1280-
12811281
UniValue getwalletpakinfo(const JSONRPCRequest& request)
12821282
{
12831283
std::shared_ptr<CWallet> const wallet = GetWalletForJSONRPCRequest(request);
@@ -1293,8 +1293,8 @@ UniValue getwalletpakinfo(const JSONRPCRequest& request)
12931293
"\nReturns relevant pegout authorization key (PAK) information about this wallet. Throws an error if initpegoutwallet` has not been invoked on this wallet.\n"
12941294
"\nResult:\n"
12951295
"{\n"
1296-
"\"derivation_path\" (string) The next index to be used by the wallet for `sendtomainchain`.\n"
1297-
"\"bitcoin_xpub\" (string) The Bitcoin xpubkey loaded in the wallet for pegouts.\n"
1296+
"\"bip32_counter\" (string) The next index to be used by the wallet for `sendtomainchain`.\n"
1297+
"\"bitcoin_descriptor\" (string) The Bitcoin script descriptor loaded in the wallet for pegouts.\n"
12981298
"\"liquid_pak\" (string) Pubkey in hex corresponding to the Liquid PAK loaded in the wallet for pegouts.\n"
12991299
"\"liquid_pak_address\" (string) The corresponding address for `liquid_pak`. Useful for `dumpprivkey` for wallet backup or transfer.\n"
13001300
"\"address_lookahead\"(array) The three next Bitcoin addresses the wallet will use for `sendtomainchain` based on the internal counter.\n"
@@ -1310,18 +1310,28 @@ UniValue getwalletpakinfo(const JSONRPCRequest& request)
13101310
UniValue ret(UniValue::VOBJ);
13111311
std::stringstream ss;
13121312
ss << pwallet->offline_counter;
1313-
ret.push_back(Pair("derivation_path", "/0/"+ss.str()));
1313+
ret.push_back(Pair("bip32_counter", ss.str()));
1314+
1315+
const std::string desc_str = pwallet->offline_desc;
13141316

1315-
CExtPubKey& xpub = pwallet->offline_xpub;
1317+
FlatSigningProvider provider;
1318+
const auto& desc = Parse(desc_str, provider);
13161319

1317-
ret.pushKV("bitcoin_xpub", EncodeExtPubKey(xpub));
1320+
ret.pushKV("bitcoin_descriptor", desc_str);
13181321
ret.pushKV("liquid_pak", HexStr(pwallet->online_key));
13191322
ret.pushKV("liquid_pak_address", EncodeDestination((pwallet->online_key.GetID())));
13201323

13211324
UniValue address_list(UniValue::VARR);
13221325
for (unsigned int i = 0; i < 3; i++) {
1323-
address_list.push_back(EncodeParentDestination(DeriveBitcoinOfflineAddress(xpub, pwallet->offline_counter+i)));
1326+
std::vector<CScript> scripts;
1327+
if (!desc->Expand(i, provider, scripts, provider)) {
1328+
throw JSONRPCError(RPC_WALLET_ERROR, "Could not generate lookahead addresses with descriptor. This is a bug.");
1329+
}
1330+
CTxDestination destination;
1331+
ExtractDestination(scripts[0], destination);
1332+
address_list.push_back(EncodeParentDestination(destination));
13241333
}
1334+
13251335
ret.push_back(Pair("address_lookahead", address_list));
13261336
return ret;
13271337
}

src/wallet/rpcwallet.cpp

Lines changed: 149 additions & 63 deletions
Large diffs are not rendered by default.

src/wallet/wallet.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4492,6 +4492,16 @@ bool CWallet::SetOfflineXPubKey(const CExtPubKey& offline_xpub_in)
44924492
return true;
44934493
}
44944494

4495+
bool CWallet::SetOfflineDescriptor(const std::string& offline_desc_in)
4496+
{
4497+
LOCK(cs_wallet);
4498+
if (!WalletBatch(*database).WriteOfflineDescriptor(offline_desc_in)) {
4499+
return false;
4500+
}
4501+
offline_desc = offline_desc_in;
4502+
return true;
4503+
}
4504+
44954505
bool CWallet::SetOfflineCounter(int counter) {
44964506
LOCK(cs_wallet);
44974507
if (!WalletBatch(*database).WriteOfflineCounter(counter)) {

src/wallet/wallet.h

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -836,11 +836,17 @@ class CWallet final : public CCryptoKeyStore, public CValidationInterface
836836
//! The online PAK aka `liquid_pak` in the wallet set by `initpegoutwallet`
837837
CPubKey online_key;
838838

839-
//! The offline xpub aka `bitcoin_xpub` in the wallet set by `initpegoutwallet`
840-
CExtPubKey offline_xpub;
841839

842840
//! The derivation counter for offline_xpub
843841
int offline_counter = -1;
842+
843+
//! The offline descriptor aka `bitcoind_descriptor` set by `initpegoutwallet`
844+
std::string offline_desc;
845+
846+
//! DEPRECATED:
847+
//The offline xpub aka `bitcoin_xpub` in the wallet set by `initpegoutwallet`
848+
CExtPubKey offline_xpub;
849+
844850
// END ELEMENTS
845851

846852
const CWalletTx* GetWalletTx(const uint256& hash) const;
@@ -1225,9 +1231,10 @@ class CWallet final : public CCryptoKeyStore, public CValidationInterface
12251231
// ELEMENTS
12261232
//! Setters for online/offline pubkey pairs for PAK
12271233
bool SetOnlinePubKey(const CPubKey& online_key_in);
1228-
bool SetOfflineXPubKey(const CExtPubKey& offline_xpub_in);
12291234
bool SetOfflineCounter(int counter);
1230-
1235+
bool SetOfflineDescriptor(const std::string& offline_desc_in);
1236+
// DEPRECATED
1237+
bool SetOfflineXPubKey(const CExtPubKey& offline_xpub_in);
12311238
};
12321239

12331240
/** A key allocated from the key pool. */

src/wallet/walletdb.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,11 @@ bool WalletBatch::WriteOfflineXPubKey(const CExtPubKey& offline_xpub)
184184
return WriteIC(std::string("offlinexpub"), vxpub);
185185
}
186186

187+
bool WalletBatch::WriteOfflineDescriptor(const std::string& offline_desc)
188+
{
189+
return WriteIC(std::string("offlinedesc"), offline_desc);
190+
}
191+
187192
bool WalletBatch::WriteOfflineCounter(int counter)
188193
{
189194
return WriteIC(std::string("offlinecounter"), counter);
@@ -547,6 +552,12 @@ ReadKeyValue(CWallet* pwallet, CDataStream& ssKey, CDataStream& ssValue,
547552
int counter;
548553
ssValue >> counter;
549554
pwallet->offline_counter = counter;
555+
}
556+
else if (strType == "offlinedesc")
557+
{
558+
std::string descriptor;
559+
ssValue >> descriptor;
560+
pwallet->offline_desc = descriptor;
550561
} else if (strType != "bestblock" && strType != "bestblock_nomerkle") {
551562
wss.m_unknown_records++;
552563
}

src/wallet/walletdb.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,10 @@ class WalletBatch
216216

217217
/// ELEMENTS: Storage of PAK settings
218218
bool WriteOnlineKey(const CPubKey& online_key);
219-
bool WriteOfflineXPubKey(const CExtPubKey& offline_xpub);
220219
bool WriteOfflineCounter(int counter);
220+
bool WriteOfflineDescriptor(const std::string& offline_desc);
221+
// DEPRECATED
222+
bool WriteOfflineXPubKey(const CExtPubKey& offline_xpub);
221223

222224
DBErrors LoadWallet(CWallet* pwallet);
223225
DBErrors FindWalletTx(std::vector<uint256>& vTxHash, std::vector<CWalletTx>& vWtx);

test/functional/feature_pak.py

Lines changed: 110 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ def pak_to_option(pak):
3333
("02f4a7445f9c48ee8590a930d3fc4f0f5763e3d1d003fdf5fc822e7ba18f380632", "036b3786f029751ada9f02f519a86c7e02fb2963a7013e7e668eb5f7ec069b9e7e")]
3434

3535
# Args that will be re-used in slightly different ways across runs
36-
args = [["-acceptnonstdtxn=0", "-initialfreecoins=100000000"]] \
37-
+ [["-acceptnonstdtxn=0", "-enforce_pak=1", "-initialfreecoins=100000000"]]*4
36+
# TODO remove lol once parent chain hrp default is changed
37+
args = [["-acceptnonstdtxn=0", "-initialfreecoins=100000000", "-parent_bech32_hrp=lol"]] \
38+
+ [["-acceptnonstdtxn=0", "-enforce_pak=1", "-initialfreecoins=100000000", "-parent_bech32_hrp=lol"]]*4
3839
args[i_reject] = args[i_reject] + ['-pak=reject']
3940
# Novalidate has pak entry, should not act on it ever
4041
args[i_novalidate] = args[i_novalidate] + pak_to_option(pak1)
@@ -199,6 +200,7 @@ def compare(actual, expected):
199200
# We will re-use the same xpub, but each wallet will create its own online pak
200201
# so the lists will be incompatible, even if all else was synced
201202
xpub = "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B"
203+
xpub_desc = "pkh("+xpub+"/0/*)" # Transform this into a descriptor
202204
init_results = []
203205
info_results = []
204206
for i in range(5):
@@ -213,8 +215,8 @@ def compare(actual, expected):
213215
assert_equal(init_results[i]["address_lookahead"], info_results[i]["address_lookahead"])
214216
assert_equal(init_results[i]["liquid_pak"], info_results[i]["liquid_pak"])
215217
assert_equal(init_results[i]["liquid_pak_address"], info_results[i]["liquid_pak_address"])
216-
assert_equal(info_results[i]["bitcoin_xpub"], xpub)
217-
assert_equal(info_results[i]["derivation_path"], "/0/0")
218+
assert_equal(info_results[i]["bitcoin_descriptor"], xpub_desc)
219+
assert_equal(info_results[i]["bip32_counter"], "0")
218220

219221
# Use custom derivation counter values, check if stored correctly,
220222
# address lookahead looks correct and that new liquid_pak was chosen
@@ -224,7 +226,7 @@ def compare(actual, expected):
224226
assert_raises_rpc_error(-8, "bip32_counter must be between 0 and 1,000,000,000, inclusive.", self.nodes[i_undefined].initpegoutwallet, xpub, 1000000001)
225227

226228
new_init = self.nodes[i_undefined].initpegoutwallet(xpub, 2)
227-
assert_equal(self.nodes[i_undefined].getwalletpakinfo()["derivation_path"], "/0/2")
229+
assert_equal(self.nodes[i_undefined].getwalletpakinfo()["bip32_counter"], "2")
228230
assert_equal(new_init["address_lookahead"][0], init_results[i_undefined]["address_lookahead"][2])
229231
assert(new_init["liquid_pak"] != init_results[i_undefined]["liquid_pak"])
230232

@@ -244,9 +246,9 @@ def compare(actual, expected):
244246

245247
# Check PAK settings persistance in wallet across restart
246248
restarted_info = self.nodes[i_undefined].getwalletpakinfo()
247-
assert_equal(restarted_info["bitcoin_xpub"], xpub)
249+
assert_equal(restarted_info["bitcoin_descriptor"], xpub_desc)
248250
assert_equal(restarted_info["liquid_pak"], new_init["liquid_pak"])
249-
assert_equal(restarted_info["derivation_path"], "/0/2")
251+
assert_equal(restarted_info["bip32_counter"], "2")
250252

251253
# Have nodes send pegouts, check it fails to enter mempool of other nodes with incompatible
252254
# PAK settings
@@ -260,10 +262,12 @@ def compare(actual, expected):
260262

261263
# pak1 will now create a pegout.
262264
pak1_pegout_txid = self.nodes[i_pak1].sendtomainchain("", 1)["txid"]
263-
assert_equal(self.nodes[i_pak1].getwalletpakinfo()["derivation_path"], "/0/1")
265+
assert_equal(self.nodes[i_pak1].getwalletpakinfo()["bip32_counter"], "1")
266+
# Also spend the change to make chained payment that will be rejected as well
267+
pak1_child_txid = self.nodes[i_pak1].sendtoaddress(self.nodes[i_pak1].getnewaddress(), self.nodes[i_pak1].getbalance(), "", "", True)
264268

265269

266-
# Wait for two nodes to get transaction in mempool only
270+
# Wait for node("follow the leader" conf-undefined) to get transaction in
267271
time_to_wait = 15
268272
while time_to_wait > 0:
269273
# novalidate doesn't allow >80 byte op_return outputs due to no enforce_pak
@@ -283,15 +287,111 @@ def compare(actual, expected):
283287

284288
assert_equal(pak1_pegout_txid in self.nodes[i_novalidate].getrawmempool(), False)
285289
assert_equal(pak1_pegout_txid in self.nodes[i_undefined].getrawmempool(), False)
290+
assert_equal(pak1_pegout_txid in self.nodes[i_pak1].getrawmempool(), True)
286291
assert_equal(pak1_pegout_txid in self.nodes[i_pak2].getrawmempool(), False)
287292
assert_equal(pak1_pegout_txid in self.nodes[i_reject].getrawmempool(), False)
288293

294+
assert_equal(self.nodes[i_pak1].gettransaction(pak1_pegout_txid)["confirmations"], 0)
295+
296+
# Make sure child payment also bumped from mempool
297+
assert_equal(pak1_child_txid in self.nodes[i_novalidate].getrawmempool(), False)
298+
assert_equal(pak1_child_txid in self.nodes[i_undefined].getrawmempool(), False)
299+
assert_equal(pak1_child_txid in self.nodes[i_pak1].getrawmempool(), True)
300+
assert_equal(pak1_child_txid in self.nodes[i_pak2].getrawmempool(), False)
301+
assert_equal(pak1_child_txid in self.nodes[i_reject].getrawmempool(), False)
302+
303+
assert_equal(self.nodes[i_pak1].gettransaction(pak1_child_txid)["confirmations"], 0)
289304
# Fail to peg-out too-small value
290305
assert_raises_rpc_error(-8, "Invalid amount for send, must send more than 0.0001 BTC", self.nodes[i_undefined].sendtomainchain, "", Decimal('0.0009'))
291306

292307
# Use wrong network's extended pubkey
293308
mainnetxpub = "xpub6AATBi58516uxLogbuaG3jkom7x1qyDoZzMN2AePBuQnMFKUV9xC2BW9vXsFJ9rELsvbeGQcFWhtbyM4qDeijM22u3AaSiSYEvuMZkJqtLn"
294-
assert_raises_rpc_error(-8, "bitcoin_xpub is invalid for this network", self.nodes[i_undefined].initpegoutwallet, mainnetxpub)
309+
assert_raises_rpc_error(-8, "bitcoin_descriptor is not a valid descriptor string.", self.nodes[i_undefined].initpegoutwallet, mainnetxpub)
310+
311+
# Test fixed online pubkey
312+
init_info = self.nodes[i_pak1].initpegoutwallet(xpub)
313+
init_info2 = self.nodes[i_pak1].initpegoutwallet(xpub, 0, init_info['liquid_pak'])
314+
assert_equal(init_info, init_info2)
315+
init_info3 = self.nodes[i_pak1].initpegoutwallet(xpub)
316+
assert(init_info != init_info3)
317+
318+
# Test Descriptor PAK Support
319+
320+
# Non-supported descriptors
321+
assert_raises_rpc_error(-8, "bitcoin_descriptor is not of any type supported: pkh(<xpub>), sh(wpkh(<xpub>)), wpkh(<xpub>), or <xpub>.", self.nodes[i_pak1].initpegoutwallet, "pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/*)")
322+
323+
assert_raises_rpc_error(-8, "bitcoin_descriptor must be a ranged descriptor.", self.nodes[i_pak1].initpegoutwallet, "pkh(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B)")
324+
325+
# key origins aren't supported in 0.17
326+
assert_raises_rpc_error(-8, "bitcoin_descriptor is not a valid descriptor string.", self.nodes[i_pak1].initpegoutwallet, "pkh([d34db33f/44'/0'/0']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/*)")
327+
328+
# Peg out with each new type, check that destination script matches
329+
wpkh_desc = "wpkh("+xpub+"/0/*)"
330+
wpkh_info = self.nodes[i_pak1].initpegoutwallet(wpkh_desc)
331+
wpkh_pak_info = self.nodes[i_pak1].getwalletpakinfo()
332+
333+
# Add to pak list for pak1, restart
334+
self.stop_nodes()
335+
extra_args = copy.deepcopy(args)
336+
extra_args[i_pak1] = extra_args[i_pak1]+["-"+wpkh_info["pakentry"]]
337+
self.start_nodes(extra_args)
338+
339+
# Make block commitment and get some block subsidy
340+
self.nodes[i_pak1].generate(101)
341+
wpkh_stmc = self.nodes[i_pak1].sendtomainchain("", 1)
342+
wpkh_txid = wpkh_stmc['txid']
343+
344+
# Also check some basic return fields of sendtomainchain with pak
345+
assert_equal(wpkh_stmc["bitcoin_address"], wpkh_info["address_lookahead"][0])
346+
validata = self.nodes[i_pak1].validateaddress(wpkh_stmc["bitcoin_address"])
347+
assert(not validata["isvalid"])
348+
assert(validata["isvalid_parent"])
349+
assert_equal(wpkh_pak_info["bip32_counter"], wpkh_stmc["bip32_counter"])
350+
assert_equal(wpkh_pak_info["bitcoin_descriptor"], wpkh_stmc["bitcoin_descriptor"])
351+
352+
sh_wpkh_desc = "sh(wpkh("+xpub+"/0/1/*))"
353+
sh_wpkh_info = self.nodes[i_pak1].initpegoutwallet(sh_wpkh_desc)
354+
355+
# Add to pak list for pak1, restart
356+
self.stop_nodes()
357+
extra_args = copy.deepcopy(args)
358+
extra_args[i_pak1] = extra_args[i_pak1]+["-"+sh_wpkh_info["pakentry"]]
359+
360+
# Restart and connect peers
361+
self.start_nodes(extra_args)
362+
connect_nodes_bi(self.nodes,0,1)
363+
connect_nodes_bi(self.nodes,1,2)
364+
connect_nodes_bi(self.nodes,2,3)
365+
connect_nodes_bi(self.nodes,3,4)
366+
367+
self.nodes[i_pak1].generate(1)
368+
sh_wpkh_txid = self.nodes[i_pak1].sendtomainchain("", 1)['txid']
369+
370+
# Make sure peg-outs look correct
371+
wpkh_raw = self.nodes[i_pak1].decoderawtransaction(self.nodes[i_pak1].gettransaction(wpkh_txid)['hex'])
372+
sh_wpkh_raw = self.nodes[i_pak1].decoderawtransaction(self.nodes[i_pak1].gettransaction(sh_wpkh_txid)['hex'])
373+
374+
peg_out_found = False
375+
for output in wpkh_raw["vout"]:
376+
if "pegout_addresses" in output["scriptPubKey"]:
377+
if output["scriptPubKey"]["pegout_addresses"][0] \
378+
== wpkh_info["address_lookahead"][0]:
379+
peg_out_found = True
380+
break
381+
else:
382+
raise Exception("Found unexpected peg-out output")
383+
assert(peg_out_found)
384+
385+
peg_out_found = False
386+
for output in sh_wpkh_raw["vout"]:
387+
if "pegout_addresses" in output["scriptPubKey"]:
388+
if output["scriptPubKey"]["pegout_addresses"][0] \
389+
== sh_wpkh_info["address_lookahead"][0]:
390+
peg_out_found = True
391+
break
392+
else:
393+
raise Exception("Found unexpected peg-out output")
394+
assert(peg_out_found)
295395

296396
if __name__ == '__main__':
297397
PAKTest ().main ()

0 commit comments

Comments
 (0)