Skip to content

Commit 4ab00c4

Browse files
authored
Merge pull request #2016 from mintlayer/feature/wallet-mempool-sync
Wallet sync with the mempool after initial sync
2 parents 306d565 + 3454e25 commit 4ab00c4

11 files changed

Lines changed: 289 additions & 27 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',

test/functional/wallet_mempool_events.py

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -104,31 +104,18 @@ async def async_test(self):
104104
tip_id = node.chainstate_best_block_id()
105105

106106
# Submit a valid transaction
107-
token_fee = 1000
108-
coins_to_send = 1
109-
token_fee_output = {
110-
"Transfer": [
111-
{"Coin": token_fee * ATOMS_PER_COIN},
112-
{
113-
"PublicKey": {
114-
"key": {"Secp256k1Schnorr": {"pubkey_data": pub_key_bytes}}
115-
}
116-
},
117-
],
118-
}
107+
total_coins = 100
119108
tx_fee_output = {
120109
"Transfer": [
121-
{"Coin": coins_to_send * ATOMS_PER_COIN},
110+
{"Coin": total_coins * ATOMS_PER_COIN},
122111
{
123112
"PublicKey": {
124113
"key": {"Secp256k1Schnorr": {"pubkey_data": pub_key_bytes}}
125114
}
126115
},
127116
],
128117
}
129-
encoded_tx, tx_id = make_tx(
130-
[reward_input(tip_id)], [token_fee_output] + [tx_fee_output] * 2, 0
131-
)
118+
encoded_tx, tx_id = make_tx([reward_input(tip_id)], [tx_fee_output], 0)
132119

133120
self.log.debug(f"Encoded transaction {tx_id}: {encoded_tx}")
134121

@@ -148,11 +135,11 @@ async def async_test(self):
148135

149136
# both wallets have the same balances after syncing the new block
150137
assert_in(
151-
f"Coins amount: {coins_to_send * 2 + token_fee}",
138+
f"Coins amount: {total_coins}",
152139
await wallet.get_balance(),
153140
)
154141
assert_in(
155-
f"Coins amount: {coins_to_send * 2 + token_fee}",
142+
f"Coins amount: {total_coins}",
156143
await wallet2.get_balance(),
157144
)
158145

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
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+
total_coins = 100
112+
tx_fee_output = {
113+
"Transfer": [
114+
{"Coin": total_coins * ATOMS_PER_COIN},
115+
{
116+
"PublicKey": {
117+
"key": {"Secp256k1Schnorr": {"pubkey_data": pub_key_bytes}}
118+
}
119+
},
120+
],
121+
}
122+
encoded_tx, tx_id = make_tx([reward_input(tip_id)], [tx_fee_output], 0)
123+
124+
self.log.debug(f"Encoded transaction {tx_id}: {encoded_tx}")
125+
126+
assert_in("No transaction found", await wallet.get_transaction(tx_id))
127+
128+
node.mempool_submit_transaction(encoded_tx, {})
129+
assert node.mempool_contains_tx(tx_id)
130+
131+
self.generate_block()
132+
assert not node.mempool_contains_tx(tx_id)
133+
134+
# sync the wallet
135+
assert_in("Success", await wallet.sync())
136+
assert_in("Success", await wallet2.sync())
137+
138+
acc0_address = await wallet.new_address()
139+
140+
# both wallets have the same balances after syncing the new block
141+
assert_in(
142+
f"Coins amount: {total_coins}",
143+
await wallet.get_balance(),
144+
)
145+
assert_in(
146+
f"Coins amount: {total_coins}",
147+
await wallet2.get_balance(),
148+
)
149+
150+
# create new account and get an address
151+
assert_in("Success", await wallet.create_new_account())
152+
assert_in("Success", await wallet2.create_new_account())
153+
assert_in("Success", await wallet.select_account(1))
154+
acc1_address = await wallet.new_address()
155+
156+
# close wallet2
157+
await wallet2.close_wallet()
158+
159+
# go back to Acc 0 and send 1 coin to Acc 1
160+
coins_to_send = 2
161+
assert_in("Success", await wallet.select_account(DEFAULT_ACCOUNT_INDEX))
162+
assert_in(
163+
"The transaction was submitted successfully",
164+
await wallet.send_to_address(acc1_address, coins_to_send),
165+
)
166+
167+
# check mempool has 1 transaction now
168+
transactions = node.mempool_transactions()
169+
assert_equal(len(transactions), 1)
170+
171+
# check wallet 1 has it as pending
172+
pending_txs = await wallet.list_pending_transactions()
173+
assert_equal(1, len(pending_txs))
174+
transfer_tx_id = pending_txs[0]
175+
176+
# reopen wallet2 and sync it will scan the mempool on first sync
177+
await wallet2.open_wallet(wallet2_name)
178+
assert_in("Success", await wallet2.sync())
179+
180+
# check wallet 2 has the new tx from scanning the mempool
181+
pending_txs = await wallet2.list_pending_transactions()
182+
assert_equal(1, len(pending_txs))
183+
assert_equal(transfer_tx_id, pending_txs[0])
184+
185+
assert_in("Success", await wallet.select_account(1))
186+
# wallet 2 should automatically recover Acc 1
187+
assert_in("Success", await wallet2.select_account(1))
188+
189+
# check both balances have `coins_to_send` coins in-mempool state
190+
assert_in(
191+
f"Coins amount: {coins_to_send}",
192+
await wallet.get_balance(utxo_states=["in-mempool"]),
193+
)
194+
assert_in(
195+
f"Coins amount: {coins_to_send}",
196+
await wallet2.get_balance(utxo_states=["in-mempool"]),
197+
)
198+
199+
# check wallet2 can send 1 coin back to Acc0 from the not yet confirmed tx in mempool
200+
assert_in(
201+
"The transaction was submitted successfully",
202+
await wallet2.send_to_address(acc0_address, 1),
203+
)
204+
205+
# close wallet2 and recover a new one with the same mnemonic
206+
await wallet2.close_wallet()
207+
assert_in(
208+
"Wallet recovered successfully",
209+
await wallet2.recover_wallet(mnemonic),
210+
)
211+
# sync the new wallet2
212+
assert_in("Success", await wallet2.sync())
213+
# check wallet 2 has the new tx from scanning the mempool
214+
pending_txs = await wallet2.list_pending_transactions()
215+
assert_equal(2, len(pending_txs))
216+
assert_in(transfer_tx_id, pending_txs)
217+
218+
self.generate_block()
219+
220+
assert_in("Success", await wallet.sync())
221+
assert_in("Success", await wallet2.sync())
222+
223+
224+
if __name__ == "__main__":
225+
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: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ use types::{
5353
SeedWithPassPhrase, SignatureStats, TransactionToInspect, ValidatedSignatures, WalletInfo,
5454
WalletTypeArgsComputed,
5555
};
56+
use utils::set_flag::SetFlag;
5657
use wallet_storage::DefaultBackend;
5758

5859
use read::ReadOnlyController;
@@ -207,6 +208,7 @@ pub struct Controller<T, W, B: storage::Backend + 'static> {
207208
wallet_events: W,
208209

209210
mempool_events: MempoolEvents,
211+
finished_initial_sync: SetFlag,
210212
}
211213

212214
impl<T, WalletEvents, B: storage::Backend> std::fmt::Debug for Controller<T, WalletEvents, B> {
@@ -243,6 +245,7 @@ where
243245
staking_started: BTreeSet::new(),
244246
wallet_events,
245247
mempool_events,
248+
finished_initial_sync: SetFlag::new(),
246249
};
247250

248251
log::info!("Syncing the wallet...");
@@ -268,6 +271,7 @@ where
268271
staking_started: BTreeSet::new(),
269272
wallet_events,
270273
mempool_events,
274+
finished_initial_sync: SetFlag::new(),
271275
})
272276
}
273277

@@ -1360,6 +1364,28 @@ where
13601364
}
13611365
}
13621366

1367+
// after the first successful sync to the tip fetch all mempool transactions
1368+
if !self.finished_initial_sync.test() {
1369+
let txs = self.rpc_client.mempool_get_transactions().await;
1370+
1371+
match txs {
1372+
Ok(txs) => {
1373+
if let Err(err) =
1374+
self.wallet.add_mempool_transactions(&txs, &self.wallet_events)
1375+
{
1376+
log::error!("Error adding mempool transactions: {err}");
1377+
} else {
1378+
self.finished_initial_sync.set();
1379+
}
1380+
}
1381+
Err(err) => {
1382+
log::error!("Failed to fetch all transactions from the mempool: {err}");
1383+
tokio::time::sleep(ERROR_DELAY).await;
1384+
continue;
1385+
}
1386+
}
1387+
}
1388+
13631389
let mut delay = Box::pin(tokio::time::sleep(NORMAL_DELAY));
13641390

13651391
loop {

0 commit comments

Comments
 (0)