What changed, and why it matters
This commit only adds a test-only 'faucet' feature to Electrum's internal toy server used by unit tests. It replaces a hard-coded test transaction with a dynamically generated one so tests can create their own fake Bitcoin for testing. There is no change to production wallet code, network handling, or user-facing behavior, and no security issue is present.
No action required; this is a benign test-only change.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff extends tests/toyserver.py and tests/test_interface.py to add a faucet wallet inside the test harness. set_up_faucet() creates a deterministic regtest wallet, mines a coinbase output to it, waits for coinbase maturity, and ask_faucet() creates and signs a transaction funding test wallets. The production Electrum code paths are untouched; this is purely test infrastructure refactoring.
Changed components
tests/toyserver.pytests/test_interface.pyInspect captured patch +41 / −8
diff --git a/tests/test_interface.py b/tests/test_interface.py
index 9012afe..b730bf7 100644
--- a/tests/test_interface.py
+++ b/tests/test_interface.py
@@ -11,6 +11,7 @@ from electrum.util import OldTaskGroup, bfh
from electrum.simple_config import SimpleConfig
from electrum.transaction import Transaction, TxOutput
from electrum.wallet import Abstract_Wallet
+from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED
from electrum.blockchain import Blockchain
from . import ElectrumTestCase
@@ -115,7 +116,7 @@ class TestInterface(ElectrumTestCase):
self.network = MockNetwork(config=self.config)
for _ in range(10): # mine some blocks
await self._toyserver.mine_block()
-
+ await self._toyserver.set_up_faucet(config=self.config)
async def asyncTearDown(self):
if self.network.interface:
@@ -179,17 +180,18 @@ class TestInterface(ElectrumTestCase):
await w1.up_to_date_changed_event.wait()
self.assertEqual(self._get_server_session()._method_counts["blockchain.scripthash.get_history"], 0)
# fund w1 (in mempool)
- funding_tx = Transaction("01000000000101e855888b77b1688d08985b863bfe85b354049b4eba923db9b5cf37089975d5d10000000000fdffffff0280969800000000001600140297bde2689a3c79ffe050583b62f86f2d9dae5460abe9000000000016001472df47551b6e7e0c8428814d2e572bc5ac773dda024730440220383efa2f0f5b87f8ce5d6b6eaf48cba03bf522b23fbb23b2ac54ff9d9a8f6a8802206f67d1f909f3c7a22ac0308ac4c19853ffca3a9317e1d7e0c88cc3a86853aaac0121035061949222555a0df490978fe6e7ebbaa96332ecb5c266918fd800c0eef736e7358d1400")
- funding_txid = await self._toyserver.mempool_add_tx(funding_tx)
+ w1_addr = w1.get_receiving_address()
+ funding_tx = await self._toyserver.ask_faucet([TxOutput.from_address_and_value(w1_addr, 1 * COIN)])
+ funding_txid = funding_tx.txid()
await w1.up_to_date_changed_event.wait()
while not w1.is_up_to_date():
await w1.up_to_date_changed_event.wait()
self.assertEqual(self._get_server_session()._method_counts["blockchain.scripthash.get_history"], 1)
self.assertEqual(
- w1.adb.get_address_history("bcrt1qq2tmmcngng78nllq2pvrkchcdukemtj5jnxz44"),
+ w1.adb.get_address_history(w1_addr),
{funding_txid: 0})
# mine funding tx
- await self._toyserver.mine_block(txs=[Transaction(funding_tx)])
+ await self._toyserver.mine_block(txs=[funding_tx])
server_blockheight += 1
await w1.up_to_date_changed_event.wait()
while not w1.is_up_to_date():
@@ -197,6 +199,6 @@ class TestInterface(ElectrumTestCase):
# see if we managed to guess new history, and hence did not need to call get_history RPC
self.assertEqual(self._get_server_session()._method_counts["blockchain.scripthash.get_history"], 1)
self.assertEqual(
- w1.adb.get_address_history("bcrt1qq2tmmcngng78nllq2pvrkchcdukemtj5jnxz44"),
+ w1.adb.get_address_history(w1_addr),
{funding_txid: server_blockheight})
diff --git a/tests/toyserver.py b/tests/toyserver.py
index 7ed07a3..c8073a3 100644
--- a/tests/toyserver.py
+++ b/tests/toyserver.py
@@ -14,10 +14,16 @@ from aiorpcx import RPCError
from electrum import blockchain
from electrum.util import bfh
from electrum.logging import Logger
-from electrum.transaction import Transaction, TxOutput, TxInput, TxOutpoint
+from electrum.transaction import Transaction, TxOutput, TxInput, TxOutpoint, PartialTxOutput
from electrum import constants
-from electrum.bitcoin import script_to_scripthash
+from electrum.bitcoin import script_to_scripthash, COIN, COINBASE_MATURITY
+from electrum.simple_config import SimpleConfig
from electrum.synchronizer import history_status
+from electrum.wallet import Abstract_Wallet
+from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED
+from electrum.fee_policy import FixedFeePolicy
+
+from . import restore_wallet_from_text__for_unittest
DAEMON_ERROR = 2
@@ -48,6 +54,8 @@ class ToyServer:
self._cache_blockheight_from_txid = {} # type: dict[str, int]
self.txo_to_spender_txid = {} # type: dict[TxOutpoint, str | None] # also contains UTXOs
+ self._faucet_w = None # type: Optional[Abstract_Wallet]
+
async def start(self):
session_factory = partial(ToyServerSession, toyserver=self)
self.asyncio_server = await aiorpcx.serve_rs(session_factory, "127.0.0.1")
@@ -191,6 +199,29 @@ class ToyServer:
await session.server_send_notifications(touched_sh=touched_sh, height_changed=True)
return new_block, coinbase_tx
+ async def set_up_faucet(self, *, config: SimpleConfig):
+ assert self._faucet_w is None
+ self._faucet_w = restore_wallet_from_text__for_unittest(
+ "9dk", passphrase="faucet", path=None, config=config)['wallet'] # type: Abstract_Wallet
+ self._faucet_w.adb.get_local_height = lambda *args: self.cur_height
+ faucet_cb_txo = TxOutput.from_address_and_value(self._faucet_w.get_receiving_address(), 50 * COIN)
+ block, cb_tx = await self.mine_block(coinbase_outputs=[faucet_cb_txo])
+ faucet_tx_height = self.cur_height
+ for _ in range(COINBASE_MATURITY): # need to mine some blocks for maturity
+ await self.mine_block()
+ self._faucet_w.adb.receive_tx_callback(cb_tx, tx_height=faucet_tx_height)
+ # note: balance is unverified due to lack of SPV, gets treated as "unconfirmed":
+ assert self._faucet_w.get_balance() == (0, 50 * COIN, 0), self._faucet_w.get_balance()
+
+ async def ask_faucet(self, outputs: Sequence[TxOutput]) -> Transaction:
+ assert self._faucet_w, "faucet must be set up first using set_up_faucet()"
+ outputs = [PartialTxOutput.from_txout(txout) for txout in outputs]
+ tx = self._faucet_w.make_unsigned_transaction(outputs=outputs, fee_policy=FixedFeePolicy(0))
+ self._faucet_w.sign_transaction(tx, password=None)
+ self._faucet_w.adb.receive_tx_callback(tx, tx_height=TX_HEIGHT_UNCONFIRMED)
+ await self.mempool_add_tx(tx)
+ return tx
+
class ToyServerSession(aiorpcx.RPCSession, Logger):
"""Server-side representation of a single electrum-protocol session."""
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.