pyln-testing: introduce canned blocks support to bitcoind fixture.

We have to add a send_and_mine_block() for cases where we want to get
a txid and then mine it (for canned blocks, we mine it then figure out
which tx it was!).

And fix up out-by-one in saving blocks.

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
Rusty Russell
2025-11-13 16:02:39 +10:30
parent 845bb30f46
commit b4eda94ed3
2 changed files with 85 additions and 34 deletions

View File

@@ -125,36 +125,40 @@ def node_cls():
@pytest.fixture @pytest.fixture
def bitcoind(directory, teardown_checks): def bitcoind(request, directory, teardown_checks):
chaind = network_daemons[env('TEST_NETWORK', 'regtest')] chaind = network_daemons[env('TEST_NETWORK', 'regtest')]
bitcoind = chaind(bitcoin_dir=directory) bitcoind = chaind(bitcoin_dir=directory)
try: # @pytest.mark.parametrize('bitcoind', [False], indirect=True) if you don't
bitcoind.start() # want bitcoind started!
except Exception: if getattr(request, 'param', True):
bitcoind.stop() try:
raise bitcoind.start()
except Exception:
bitcoind.stop()
raise
info = bitcoind.rpc.getnetworkinfo() info = bitcoind.rpc.getnetworkinfo()
# FIXME: include liquid-regtest in this check after elementsd has been # FIXME: include liquid-regtest in this check after elementsd has been
# updated # updated
if info['version'] < 200100 and env('TEST_NETWORK') != 'liquid-regtest': if info['version'] < 200100 and env('TEST_NETWORK') != 'liquid-regtest':
bitcoind.rpc.stop() bitcoind.rpc.stop()
raise ValueError("bitcoind is too old. At least version 20100 (v0.20.1)" raise ValueError("bitcoind is too old. At least version 20100 (v0.20.1)"
" is needed, current version is {}".format(info['version'])) " is needed, current version is {}".format(info['version']))
elif info['version'] < 160000: elif info['version'] < 160000:
bitcoind.rpc.stop() bitcoind.rpc.stop()
raise ValueError("elementsd is too old. At least version 160000 (v0.16.0)" raise ValueError("elementsd is too old. At least version 160000 (v0.16.0)"
" is needed, current version is {}".format(info['version'])) " is needed, current version is {}".format(info['version']))
info = bitcoind.rpc.getblockchaininfo() info = bitcoind.rpc.getblockchaininfo()
# Make sure we have some spendable funds
if info['blocks'] < 101: # Make sure we have some spendable funds
bitcoind.generate_block(101 - info['blocks']) if info['blocks'] < 101:
elif bitcoind.rpc.getwalletinfo()['balance'] < 1: bitcoind.generate_block(101 - info['blocks'])
logging.debug("Insufficient balance, generating 1 block") elif bitcoind.rpc.getwalletinfo()['balance'] < 1:
bitcoind.generate_block(1) logging.debug("Insufficient balance, generating 1 block")
bitcoind.generate_block(1)
yield bitcoind yield bitcoind

View File

@@ -411,6 +411,7 @@ class BitcoinD(TailableProc):
self.bitcoin_dir = bitcoin_dir self.bitcoin_dir = bitcoin_dir
self.rpcport = rpcport self.rpcport = rpcport
self.prefix = 'bitcoind' self.prefix = 'bitcoind'
self.canned_blocks = None
regtestdir = os.path.join(bitcoin_dir, 'regtest') regtestdir = os.path.join(bitcoin_dir, 'regtest')
if not os.path.exists(regtestdir): if not os.path.exists(regtestdir):
@@ -446,15 +447,18 @@ class BitcoinD(TailableProc):
if self.reserved_rpcport is not None: if self.reserved_rpcport is not None:
drop_unused_port(self.reserved_rpcport) drop_unused_port(self.reserved_rpcport)
def start(self): def start(self, wallet_file=None):
TailableProc.start(self) TailableProc.start(self)
self.wait_for_log("Done loading", timeout=TIMEOUT) self.wait_for_log("Done loading", timeout=TIMEOUT)
logging.info("BitcoinD started") logging.info("BitcoinD started")
try: if wallet_file:
self.rpc.createwallet("lightningd-tests") self.rpc.restorewallet("lightningd-tests", wallet_file)
except JSONRPCError: else:
self.rpc.loadwallet("lightningd-tests") try:
self.rpc.createwallet("lightningd-tests")
except JSONRPCError:
self.rpc.loadwallet("lightningd-tests")
def stop(self): def stop(self):
for p in self.proxies: for p in self.proxies:
@@ -468,6 +472,10 @@ class BitcoinD(TailableProc):
proxy.start() proxy.start()
return proxy return proxy
def set_canned_blocks(self, blocks):
"""Set blocks as an array of blocks to "generate", or None to reset"""
self.canned_blocks = blocks
# wait_for_mempool can be used to wait for the mempool before generating blocks: # wait_for_mempool can be used to wait for the mempool before generating blocks:
# True := wait for at least 1 transation # True := wait for at least 1 transation
# int > 0 := wait for at least N transactions # int > 0 := wait for at least N transactions
@@ -482,6 +490,16 @@ class BitcoinD(TailableProc):
else: else:
wait_for(lambda: len(self.rpc.getrawmempool()) >= wait_for_mempool) wait_for(lambda: len(self.rpc.getrawmempool()) >= wait_for_mempool)
# Use canned blocks if we have them (fails if we run out!).
if self.canned_blocks is not None:
ret = []
while numblocks > 0:
self.rpc.submitblock(self.canned_blocks[0])
ret.append(self.rpc.getbestblockhash())
numblocks -= 1
del self.canned_blocks[0]
return ret
mempool = self.rpc.getrawmempool(True) mempool = self.rpc.getrawmempool(True)
logging.debug("Generating {numblocks}, confirming {lenmempool} transactions: {mempool}".format( logging.debug("Generating {numblocks}, confirming {lenmempool} transactions: {mempool}".format(
numblocks=numblocks, numblocks=numblocks,
@@ -509,6 +527,21 @@ class BitcoinD(TailableProc):
return self.rpc.generatetoaddress(numblocks, to_addr) return self.rpc.generatetoaddress(numblocks, to_addr)
def send_and_mine_block(self, addr, sats):
"""Sometimes we want the txid. We assume it's the first tx for canned blocks"""
if self.canned_blocks:
self.generate_block(1)
# Find which non-coinbase txs sent to this address: return txid
for txid in self.rpc.getblock(self.rpc.getbestblockhash())['tx'][1:]:
for out in self.rpc.getrawtransaction(txid, 1)['vout']:
if out['scriptPubKey'].get('address') == addr:
return txid
assert False, f"No address {addr} in block {self.rpc.getblock(self.rpc.getbestblockhash())}"
txid = self.rpc.sendtoaddress(addr, sats / 10**8)
self.generate_block(1)
return txid
def simple_reorg(self, height, shift=0): def simple_reorg(self, height, shift=0):
""" """
Reorganize chain by creating a fork at height=[height] and re-mine all mempool Reorganize chain by creating a fork at height=[height] and re-mine all mempool
@@ -526,6 +559,7 @@ class BitcoinD(TailableProc):
forward to h1. forward to h1.
2. Set [height]=h2 and [shift]= h1-h2 2. Set [height]=h2 and [shift]= h1-h2
""" """
assert self.canned_blocks is None
hashes = [] hashes = []
fee_delta = 1000000 fee_delta = 1000000
orig_len = self.rpc.getblockcount() orig_len = self.rpc.getblockcount()
@@ -559,7 +593,7 @@ class BitcoinD(TailableProc):
"""Bundle up blocks into an array, for restore_blocks""" """Bundle up blocks into an array, for restore_blocks"""
blocks = [] blocks = []
numblocks = self.rpc.getblockcount() numblocks = self.rpc.getblockcount()
for bnum in range(1, numblocks): for bnum in range(1, numblocks + 1):
bhash = self.rpc.getblockhash(bnum) bhash = self.rpc.getblockhash(bnum)
blocks.append(self.rpc.getblock(bhash, False)) blocks.append(self.rpc.getblock(bhash, False))
return blocks return blocks
@@ -892,6 +926,10 @@ class LightningNode(object):
self.daemon.opts['grpc-port'] = grpc_port self.daemon.opts['grpc-port'] = grpc_port
self.grpc_port = grpc_port or 9736 self.grpc_port = grpc_port or 9736
# If bitcoind is serving canned blocks, it will keep initialblockdownload on true!
if self.bitcoin.canned_blocks is not None:
self.daemon.opts['dev-ignore-ibd'] = True
def _create_rpc(self, jsonschemas): def _create_rpc(self, jsonschemas):
"""Prepares anything related to the RPC. """Prepares anything related to the RPC.
""" """
@@ -987,10 +1025,12 @@ class LightningNode(object):
def fundwallet(self, sats, addrtype="bech32", mine_block=True): def fundwallet(self, sats, addrtype="bech32", mine_block=True):
addr = self.rpc.newaddr(addrtype)[addrtype] addr = self.rpc.newaddr(addrtype)[addrtype]
txid = self.bitcoin.rpc.sendtoaddress(addr, sats / 10**8)
if mine_block: if mine_block:
self.bitcoin.generate_block(1) txid = self.bitcoin.send_and_mine_block(addr, sats)
self.daemon.wait_for_log('Owning output .* txid {} CONFIRMED'.format(txid)) self.daemon.wait_for_log('Owning output .* txid {} CONFIRMED'.format(txid))
else:
txid = self.bitcoin.rpc.sendtoaddress(addr, sats / 10**8)
return addr, txid return addr, txid
def fundbalancedchannel(self, remote_node, total_capacity=FUNDAMOUNT, announce=True): def fundbalancedchannel(self, remote_node, total_capacity=FUNDAMOUNT, announce=True):
@@ -1118,8 +1158,7 @@ class LightningNode(object):
# We should not have funds on that address yet, we just generated it. # We should not have funds on that address yet, we just generated it.
assert not has_funds_on_addr(addr) assert not has_funds_on_addr(addr)
self.bitcoin.rpc.sendtoaddress(addr, (amount + 1000000) / 10**8) self.bitcoin.send_and_mine_block(addr, amount + 1000000)
self.bitcoin.generate_block(1)
# Now we should. # Now we should.
wait_for(lambda: has_funds_on_addr(addr)) wait_for(lambda: has_funds_on_addr(addr))
@@ -1134,10 +1173,18 @@ class LightningNode(object):
**kwargs) **kwargs)
blockid = self.bitcoin.generate_block(1, wait_for_mempool=res['txid'])[0] blockid = self.bitcoin.generate_block(1, wait_for_mempool=res['txid'])[0]
txnum = None
for i, txid in enumerate(self.bitcoin.rpc.getblock(blockid)['tx']): for i, txid in enumerate(self.bitcoin.rpc.getblock(blockid)['tx']):
if txid == res['txid']: if txid == res['txid']:
txnum = i txnum = i
if txnum is None:
print(f"mempool = {self.bitcoin.rpc.getrawmempool()}")
print("txs:")
for txid in self.bitcoin.rpc.getblock(blockid)['tx'][1:]:
print(f"txid {txid}: {self.bitcoin.rpc.getrawtransaction(txid)} {self.bitcoin.rpc.getrawtransaction(txid, 1)}")
assert False, f"txid {res['txid']} not found"
scid = "{}x{}x{}".format(self.bitcoin.rpc.getblockcount(), scid = "{}x{}x{}".format(self.bitcoin.rpc.getblockcount(),
txnum, res['outnum']) txnum, res['outnum'])