Skip to content

Commit e9d5aa3

Browse files
committed
Wallet sync with the mempool after initial sync
1 parent 1b3aa0c commit e9d5aa3

10 files changed

Lines changed: 296 additions & 9 deletions

File tree

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ class UnicodeOnWindowsError(ValueError):
155155
'wallet_sweep_delegation.py',
156156
'wallet_recover_accounts.py',
157157
'wallet_mempool_events.py',
158+
'wallet_scan_mempool.py',
158159
'wallet_tokens.py',
159160
'wallet_tokens_freeze.py',
160161
'wallet_tokens_transfer_from_multisig_addr.py',
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2026 RBB S.r.l
3+
# Copyright (c) 2017-2021 The Bitcoin Core developers
4+
# opensource@mintlayer.org
5+
# SPDX-License-Identifier: MIT
6+
# Licensed under the MIT License;
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
"""Wallet scan mempool on startup test
18+
19+
Check that:
20+
* We create 2 wallets with same mnemonic,
21+
* get an address from the first wallet
22+
* send coins to the wallet's address
23+
* sync both wallets with the node
24+
* check balance in both wallets
25+
* close the second wallet
26+
* from the first wallet send coins from Acc 0 to Acc 1 without creating a block
27+
* reopen the second wallet and create a third wallet with the same mnemonic
28+
* they both should get the new Tx from the mempool upon creation/opening
29+
* second wallet can create a new unconfirmed Tx on top of the Tx in mempool
30+
"""
31+
32+
import asyncio
33+
34+
from test_framework.mintlayer import (ATOMS_PER_COIN, block_input_data_obj,
35+
make_tx, reward_input)
36+
from test_framework.test_framework import BitcoinTestFramework
37+
from test_framework.util import assert_equal, assert_in
38+
from test_framework.wallet_cli_controller import (DEFAULT_ACCOUNT_INDEX,
39+
WalletCliController)
40+
41+
42+
class WalletMempoolScanning(BitcoinTestFramework):
43+
44+
def set_test_params(self):
45+
self.setup_clean_chain = True
46+
self.num_nodes = 1
47+
self.extra_args = [
48+
[
49+
"--blockprod-min-peers-to-produce-blocks=0",
50+
]
51+
]
52+
53+
def setup_network(self):
54+
self.setup_nodes()
55+
self.sync_all(self.nodes[0:1])
56+
57+
def generate_block(self, transactions=[]):
58+
node = self.nodes[0]
59+
60+
block_input_data = {"PoW": {"reward_destination": "AnyoneCanSpend"}}
61+
block_input_data = block_input_data_obj.encode(block_input_data).to_hex()[2:]
62+
63+
# create a new block, taking transactions from mempool
64+
block = node.blockprod_generate_block(
65+
block_input_data, transactions, [], "FillSpaceFromMempool"
66+
)
67+
node.chainstate_submit_block(block)
68+
block_id = node.chainstate_best_block_id()
69+
70+
# Wait for mempool to sync
71+
self.wait_until(
72+
lambda: node.mempool_local_best_block_id() == block_id, timeout=5
73+
)
74+
75+
return block_id
76+
77+
def run_test(self):
78+
asyncio.run(self.async_test())
79+
80+
async def async_test(self):
81+
node = self.nodes[0]
82+
async with WalletCliController(
83+
node, self.config, self.log
84+
) as wallet, WalletCliController(node, self.config, self.log) as wallet2:
85+
# new wallet
86+
await wallet.create_wallet()
87+
# create wallet2 with the same mnemonic
88+
mnemonic = await wallet.show_seed_phrase()
89+
assert mnemonic is not None
90+
wallet2_name = "wallet2"
91+
assert_in(
92+
"Wallet recovered successfully",
93+
await wallet2.recover_wallet(mnemonic, name=wallet2_name),
94+
)
95+
96+
# check it is on genesis
97+
best_block_height = await wallet.get_best_block_height()
98+
self.log.info(f"best block height = {best_block_height}")
99+
assert_equal(best_block_height, "0")
100+
best_block_height = await wallet2.get_best_block_height()
101+
assert_equal(best_block_height, "0")
102+
103+
# new address
104+
pub_key_bytes = await wallet.new_public_key()
105+
assert_equal(len(pub_key_bytes), 33)
106+
107+
# Get chain tip
108+
tip_id = node.chainstate_best_block_id()
109+
110+
# Submit a valid transaction
111+
token_fee = 1000
112+
coins_to_send = 1
113+
token_fee_output = {
114+
"Transfer": [
115+
{"Coin": token_fee * ATOMS_PER_COIN},
116+
{
117+
"PublicKey": {
118+
"key": {"Secp256k1Schnorr": {"pubkey_data": pub_key_bytes}}
119+
}
120+
},
121+
],
122+
}
123+
tx_fee_output = {
124+
"Transfer": [
125+
{"Coin": coins_to_send * ATOMS_PER_COIN},
126+
{
127+
"PublicKey": {
128+
"key": {"Secp256k1Schnorr": {"pubkey_data": pub_key_bytes}}
129+
}
130+
},
131+
],
132+
}
133+
encoded_tx, tx_id = make_tx(
134+
[reward_input(tip_id)], [token_fee_output] + [tx_fee_output] * 2, 0
135+
)
136+
137+
self.log.debug(f"Encoded transaction {tx_id}: {encoded_tx}")
138+
139+
assert_in("No transaction found", await wallet.get_transaction(tx_id))
140+
141+
node.mempool_submit_transaction(encoded_tx, {})
142+
assert node.mempool_contains_tx(tx_id)
143+
144+
self.generate_block()
145+
assert not node.mempool_contains_tx(tx_id)
146+
147+
# sync the wallet
148+
assert_in("Success", await wallet.sync())
149+
assert_in("Success", await wallet2.sync())
150+
151+
acc0_address = await wallet.new_address()
152+
153+
# both wallets have the same balances after syncing the new block
154+
assert_in(
155+
f"Coins amount: {coins_to_send * 2 + token_fee}",
156+
await wallet.get_balance(),
157+
)
158+
assert_in(
159+
f"Coins amount: {coins_to_send * 2 + token_fee}",
160+
await wallet2.get_balance(),
161+
)
162+
163+
# create new account and get an address
164+
assert_in("Success", await wallet.create_new_account())
165+
assert_in("Success", await wallet2.create_new_account())
166+
assert_in("Success", await wallet.select_account(1))
167+
acc1_address = await wallet.new_address()
168+
169+
# close wallet2
170+
await wallet2.close_wallet()
171+
172+
# go back to Acc 0 and send 1 coin to Acc 1
173+
coins_to_send = 2
174+
assert_in("Success", await wallet.select_account(DEFAULT_ACCOUNT_INDEX))
175+
assert_in(
176+
"The transaction was submitted successfully",
177+
await wallet.send_to_address(acc1_address, coins_to_send),
178+
)
179+
180+
# check mempool has 1 transaction now
181+
transactions = node.mempool_transactions()
182+
assert_equal(len(transactions), 1)
183+
184+
# check wallet 1 has it as pending
185+
pending_txs = await wallet.list_pending_transactions()
186+
assert_equal(1, len(pending_txs))
187+
transfer_tx_id = pending_txs[0]
188+
189+
# reopen wallet2 and sync it will scan the mempool on first sync
190+
await wallet2.open_wallet(wallet2_name)
191+
assert_in("Success", await wallet2.sync())
192+
193+
# check wallet 2 has the new tx from scanning the mempool
194+
pending_txs = await wallet2.list_pending_transactions()
195+
assert_equal(1, len(pending_txs))
196+
assert_equal(transfer_tx_id, pending_txs[0])
197+
198+
assert_in("Success", await wallet.select_account(1))
199+
# wallet 2 should automatically recover Acc 1
200+
assert_in("Success", await wallet2.select_account(1))
201+
202+
# check both balances have `coins_to_send` coins in-mempool state
203+
assert_in(
204+
f"Coins amount: {coins_to_send}",
205+
await wallet.get_balance(utxo_states=["in-mempool"]),
206+
)
207+
assert_in(
208+
f"Coins amount: {coins_to_send}",
209+
await wallet2.get_balance(utxo_states=["in-mempool"]),
210+
)
211+
212+
# check wallet2 can send 1 coin back to Acc0 from the not yet confirmed tx in mempool
213+
assert_in(
214+
"The transaction was submitted successfully",
215+
await wallet2.send_to_address(acc0_address, 1),
216+
)
217+
218+
# close wallet2 and recover a new one with the same mnemonic
219+
await wallet2.close_wallet()
220+
assert_in(
221+
"Wallet recovered successfully",
222+
await wallet2.recover_wallet(mnemonic),
223+
)
224+
# sync the new wallet2
225+
assert_in("Success", await wallet2.sync())
226+
# check wallet 2 has the new tx from scanning the mempool
227+
pending_txs = await wallet2.list_pending_transactions()
228+
assert_equal(2, len(pending_txs))
229+
assert_in(transfer_tx_id, pending_txs)
230+
231+
self.generate_block()
232+
233+
assert_in("Success", await wallet.sync())
234+
assert_in("Success", await wallet2.sync())
235+
236+
237+
if __name__ == "__main__":
238+
WalletMempoolScanning().main()

test/functional/wallet_watch_address.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -143,24 +143,23 @@ async def async_test(self):
143143
output = await wallet.send_to_address(address_from_wallet1, 1)
144144
assert_in("The transaction was submitted successfully", output)
145145
receive_coins_tx_id = output.splitlines()[-1]
146+
tx = await wallet.get_raw_signed_transaction(receive_coins_tx_id)
146147

147148
# check in wallet2
148149
await wallet.close_wallet()
149150
await wallet.open_wallet('wallet2')
151+
assert_in("Success", await wallet.sync())
150152

151153
# tx is still in mempool
152154
assert node.mempool_contains_tx(receive_coins_tx_id)
153155

154-
assert_in("No transaction found", await wallet.get_raw_signed_transaction(receive_coins_tx_id))
156+
# wallet2 should also have the tx because it scanned the mempool
157+
assert_equal(tx, await wallet.get_raw_signed_transaction(receive_coins_tx_id))
155158

156159
block_id = self.generate_block()
157160
assert not node.mempool_contains_tx(receive_coins_tx_id)
158161
assert_in("Success", await wallet.sync())
159162

160-
# after syncing the tx should be found
161-
assert_not_in("No transaction found", await wallet.get_raw_signed_transaction(receive_coins_tx_id))
162-
163-
164163
# go back to wallet 1
165164
await wallet.close_wallet()
166165
await wallet.open_wallet('wallet1')
@@ -176,19 +175,18 @@ async def async_test(self):
176175
# go back to wallet 2
177176
await wallet.close_wallet()
178177
await wallet.open_wallet('wallet2')
178+
assert_in("Success", await wallet.sync())
179179

180180
# tx is still in mempool
181181
assert node.mempool_contains_tx(send_coins_tx_id)
182182

183-
assert_in("No transaction found", await wallet.get_raw_signed_transaction(send_coins_tx_id))
183+
# wallet2 should again have the tx present
184+
assert_not_in("No transaction found", await wallet.get_raw_signed_transaction(send_coins_tx_id))
184185

185186
block_id = self.generate_block()
186187
assert not node.mempool_contains_tx(send_coins_tx_id)
187188
assert_in("Success", await wallet.sync())
188189

189-
# after syncing the tx should be found
190-
assert_not_in("No transaction found", await wallet.get_raw_signed_transaction(send_coins_tx_id))
191-
192190
output = await wallet.get_standalone_addresses()
193191
assert_in(address_from_wallet1, output)
194192
if label:

wallet/wallet-controller/src/lib.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ pub struct Controller<T, W, B: storage::Backend + 'static> {
207207
wallet_events: W,
208208

209209
mempool_events: MempoolEvents,
210+
finished_initial_sync: bool,
210211
}
211212

212213
impl<T, WalletEvents, B: storage::Backend> std::fmt::Debug for Controller<T, WalletEvents, B> {
@@ -243,6 +244,7 @@ where
243244
staking_started: BTreeSet::new(),
244245
wallet_events,
245246
mempool_events,
247+
finished_initial_sync: false,
246248
};
247249

248250
log::info!("Syncing the wallet...");
@@ -268,6 +270,7 @@ where
268270
staking_started: BTreeSet::new(),
269271
wallet_events,
270272
mempool_events,
273+
finished_initial_sync: false,
271274
})
272275
}
273276

@@ -1360,6 +1363,28 @@ where
13601363
}
13611364
}
13621365

1366+
// after the first successful sync to the tip fetch all mempool transactions
1367+
if !self.finished_initial_sync {
1368+
let txs = self.rpc_client.mempool_get_transactions().await;
1369+
1370+
match txs {
1371+
Ok(txs) => {
1372+
if let Err(err) =
1373+
self.wallet.add_mempool_transactions(&txs, &self.wallet_events)
1374+
{
1375+
log::error!("Txs from mempool failed to be added in the wallet because of an error: {err}");
1376+
} else {
1377+
self.finished_initial_sync = true;
1378+
}
1379+
}
1380+
Err(err) => {
1381+
log::error!("Failed to fetch all transactios from the mempool: {err}");
1382+
tokio::time::sleep(ERROR_DELAY).await;
1383+
continue;
1384+
}
1385+
}
1386+
}
1387+
13631388
let mut delay = Box::pin(tokio::time::sleep(NORMAL_DELAY));
13641389

13651390
loop {

wallet/wallet-controller/src/sync/tests/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,10 @@ impl NodeInterface for MockNode {
446446
unreachable!()
447447
}
448448

449+
async fn mempool_get_transactions(&self) -> Result<Vec<SignedTransaction>, Self::Error> {
450+
unreachable!()
451+
}
452+
449453
async fn mempool_subscribe_to_events(&self) -> Result<MempoolEvents, Self::Error> {
450454
unreachable!()
451455
}

wallet/wallet-node-client/src/handles_client/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,4 +479,9 @@ impl NodeInterface for WalletHandlesClient {
479479
let res = self.mempool.call(move |this| this.transaction(&tx_id)).await?;
480480
Ok(res)
481481
}
482+
483+
async fn mempool_get_transactions(&self) -> Result<Vec<SignedTransaction>, Self::Error> {
484+
let res = self.mempool.call(move |this| this.get_all()).await?;
485+
Ok(res)
486+
}
482487
}

wallet/wallet-node-client/src/mock.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,10 @@ impl NodeInterface for ClonableMockNodeInterface {
322322
self.lock().await.mempool_get_transaction(tx_id).await
323323
}
324324

325+
async fn mempool_get_transactions(&self) -> Result<Vec<SignedTransaction>, Self::Error> {
326+
self.lock().await.mempool_get_transactions().await
327+
}
328+
325329
async fn mempool_subscribe_to_events(&self) -> Result<MempoolEvents, Self::Error> {
326330
self.lock().await.mempool_subscribe_to_events().await
327331
}

wallet/wallet-node-client/src/node_traits.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ pub trait NodeInterface {
152152
&self,
153153
tx_id: Id<Transaction>,
154154
) -> Result<Option<SignedTransaction>, Self::Error>;
155+
async fn mempool_get_transactions(&self) -> Result<Vec<SignedTransaction>, Self::Error>;
155156
async fn mempool_subscribe_to_events(&self) -> Result<MempoolEvents, Self::Error>;
156157

157158
async fn get_utxo(&self, outpoint: UtxoOutPoint) -> Result<Option<TxOutput>, Self::Error>;

0 commit comments

Comments
 (0)