Skip to content

Commit 7fccdd7

Browse files
committed
exp: test scenario
1 parent 1caddd4 commit 7fccdd7

File tree

2 files changed

+105
-2
lines changed

2 files changed

+105
-2
lines changed

src/wallet/scriptpubkeyman.cpp

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1308,6 +1308,27 @@ SigningResult DescriptorScriptPubKeyMan::SignMessage(const std::string& message,
13081308

13091309
std::optional<PSBTError> DescriptorScriptPubKeyMan::FillPSBT(PartiallySignedTransaction& psbtx, const PrecomputedTransactionData& txdata, std::optional<int> sighash_type, bool sign, bool bip32derivs, int* n_signed, bool finalize) const
13101310
{
1311+
// Log descriptor in public, private, and normalized forms
1312+
{
1313+
LOCK(cs_desc_man);
1314+
std::string public_desc = m_wallet_descriptor.descriptor->ToString();
1315+
WalletLogPrintf("FillPSBT: Descriptor (public): %s\n", public_desc);
1316+
1317+
std::string private_desc;
1318+
if (GetDescriptorString(private_desc, /*priv=*/true)) {
1319+
WalletLogPrintf("FillPSBT: Descriptor (private): %s\n", private_desc);
1320+
} else {
1321+
WalletLogPrintf("FillPSBT: Descriptor (private): [unable to retrieve private descriptor]\n");
1322+
}
1323+
1324+
std::string normalized_desc;
1325+
if (GetDescriptorString(normalized_desc, /*priv=*/false)) {
1326+
WalletLogPrintf("FillPSBT: Descriptor (normalized): %s\n", normalized_desc);
1327+
} else {
1328+
WalletLogPrintf("FillPSBT: Descriptor (normalized): [unable to retrieve normalized descriptor]\n");
1329+
}
1330+
}
1331+
13111332
if (n_signed) {
13121333
*n_signed = 0;
13131334
}
@@ -1343,7 +1364,21 @@ std::optional<PSBTError> DescriptorScriptPubKeyMan::FillPSBT(PartiallySignedTran
13431364
pubkeys.reserve(input.hd_keypaths.size() + 2);
13441365

13451366
// ECDSA Pubkeys
1346-
for (const auto& [pk, _] : input.hd_keypaths) {
1367+
for (const auto& [pk, origin] : input.hd_keypaths) {
1368+
std::string path_str;
1369+
for (size_t j = 0; j < origin.path.size(); ++j) {
1370+
if (j > 0) path_str += "/";
1371+
uint32_t index = origin.path[j];
1372+
if (index & 0x80000000) {
1373+
path_str += strprintf("%d'", index & ~0x80000000);
1374+
} else {
1375+
path_str += strprintf("%d", index);
1376+
}
1377+
}
1378+
// Log KeyOriginInfo
1379+
WalletLogPrintf("PSBT input %d: Found KeyOriginInfo for pubkey %s - fingerprint: %02x%02x%02x%02x, path: %s\n",
1380+
i, HexStr(pk),
1381+
origin.fingerprint[0], origin.fingerprint[1], origin.fingerprint[2], origin.fingerprint[3], path_str);
13471382
pubkeys.push_back(pk);
13481383
}
13491384

test/functional/rpc_psbt.py

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,14 @@
6161
import os
6262

6363

64+
KEYPOOL_SIZE = 10 # smaller than default size to speed-up test
65+
6466
class PSBTTest(BitcoinTestFramework):
6567
def set_test_params(self):
6668
self.num_nodes = 3
6769
self.extra_args = [
6870
["-walletrbf=1", "-addresstype=bech32", "-changetype=bech32"], #TODO: Remove address type restrictions once taproot has psbt extensions
69-
["-walletrbf=0", "-changetype=legacy"],
71+
["-walletrbf=0", "-changetype=legacy", "-keypool={}".format(KEYPOOL_SIZE)],
7072
[]
7173
]
7274
# whitelist peers to speed up tx relay / mempool sync
@@ -149,6 +151,71 @@ def test_utxo_conversion(self):
149151
self.connect_nodes(1, 0)
150152
self.connect_nodes(0, 2)
151153

154+
def test_offline_gap_limit(self):
155+
self.log.info("Test offline signing with addresses beyond initial keypool")
156+
offline_node = self.nodes[1]
157+
online_node = self.nodes[2]
158+
159+
# Create offline wallet with small keypool
160+
offline_node.createwallet(wallet_name='offline_small_keypool', descriptors=True)
161+
offline_signer = offline_node.get_wallet_rpc('offline_small_keypool')
162+
163+
# Get the descriptor from the offline wallet
164+
descs = offline_signer.listdescriptors()["descriptors"]
165+
166+
# Create watch-only wallet on online node with the same descriptors
167+
online_node.createwallet(wallet_name='watch_only', disable_private_keys=True, descriptors=True, blank=True)
168+
watch_only = online_node.get_wallet_rpc('watch_only')
169+
170+
# Disconnect offline node from others
171+
self.disconnect_nodes(0, 1)
172+
self.disconnect_nodes(1, 2)
173+
174+
import_res = watch_only.importdescriptors(descs)
175+
assert_equal(import_res[0]["success"], True)
176+
177+
# Generate several addresses on the watch-only wallet to gap limit of offline wallet
178+
for _ in range(KEYPOOL_SIZE):
179+
watch_only.getnewaddress()
180+
181+
# Fund one of the later addresses (e.g., the 20th address)
182+
target_addr = watch_only.getnewaddress()
183+
default_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
184+
default_wallet.sendtoaddress(address=target_addr, amount=1.0)
185+
self.generate(self.nodes[0], nblocks=1, sync_fun=lambda: self.sync_all([online_node, self.nodes[0]]))
186+
187+
# Verify the watch-only wallet can see the transaction
188+
utxos = watch_only.listunspent(addresses=[target_addr])
189+
assert_equal(len(utxos), 1)
190+
assert_equal(utxos[0]["address"], target_addr)
191+
192+
# Create a PSBT on the watch-only (online) wallet to spend this UTXO
193+
dest_addr = default_wallet.getnewaddress()
194+
psbt = watch_only.walletcreatefundedpsbt(
195+
inputs=[{"txid": utxos[0]["txid"], "vout": utxos[0]["vout"]}],
196+
outputs=[{dest_addr: 0.999}],
197+
options={"fee_rate": 1}
198+
)["psbt"]
199+
200+
# Verify the offline wallet can sign the PSBT
201+
signed_psbt = offline_signer.walletprocesspsbt(psbt)
202+
assert_equal(signed_psbt["complete"], True)
203+
204+
# Broadcast the transaction from the online node
205+
txid = online_node.sendrawtransaction(signed_psbt["hex"])
206+
self.generate(online_node, nblocks=1, sync_fun=lambda: self.sync_all([online_node, self.nodes[0]]))
207+
208+
# Verify transaction was confirmed
209+
assert_equal(online_node.gettxout(txid, 0)["confirmations"], 1)
210+
211+
# Cleanup
212+
watch_only.unloadwallet()
213+
offline_signer.unloadwallet()
214+
215+
# Reconnect
216+
self.connect_nodes(1, 0)
217+
self.connect_nodes(0, 2)
218+
152219
def test_input_confs_control(self):
153220
self.nodes[0].createwallet("minconf")
154221
wallet = self.nodes[0].get_wallet_rpc("minconf")
@@ -859,6 +926,7 @@ def run_test(self):
859926

860927
self.test_utxo_conversion()
861928
self.test_psbt_incomplete_after_invalid_modification()
929+
self.test_offline_gap_limit()
862930

863931
self.test_input_confs_control()
864932

0 commit comments

Comments
 (0)