import asyncio
from test_framework.util import assert_equal, wait_for
from test_framework.electrumutil import (
ElectrumTestFramework,
script_to_scripthash,
)
from test_framework.electrumconnection import ElectrumConnection
from test_framework.constants import COIN
from test_framework.script import CScript, OP_TRUE, OP_DROP
from test_framework.utilcompat import lock, unlock
from test_framework.utiltx import pad_tx
from test_framework.serialize import to_hex
from test_framework.environment import on_bch, on_nex
if on_nex():
from test_framework.nex.script import anyonecanspend_unlocking_script
elif on_bch():
from test_framework.bch.script import anyonecanspend_unlocking_script
else:
raise NotImplementedError()
DEFAULT_WAIT = 30
def confirmed_count(history):
if "history" in history:
history = history["history"]
return sum(map(lambda i: i["height"] > 0, history))
class ElectrumReorgTests(ElectrumTestFramework):
async def run_test(self):
n = self.nodes[0]
await self.bootstrap_p2p()
cli = ElectrumConnection()
await cli.connect()
try:
coinbases = await self.mine_blocks(cli, n, 100)
n.generate(100)
n.generate(110)
await self.test_reorg(cli, n)
await self.test_reorg_unspent_index_recreation(cli, n, coinbases)
await self.test_reorg_scripthash_history(cli, n, coinbases)
finally:
cli.disconnect()
async def test_reorg(self, cli, n):
address = n.getnewaddress()
await self.sync_height(cli)
await self.wait_for_mempool_count(cli, count=0)
test_tokens = False
n.sendtoaddress(address, 10)
await self.wait_for_mempool_count(cli, count=1)
if test_tokens:
token_id, _ = await self.create_token(
to_addr=n.getnewaddress(), mint_amount=42
)
await self.wait_for_mempool_count(cli, count=3)
await self.sync_mempool(cli)
blocks = n.generate(50)
await self.sync_height(cli)
await self.wait_for_mempool_count(cli, count=0)
assert (
confirmed_count(await cli.call("blockchain.address.get_history", address))
>= 1
)
if test_tokens:
assert_equal(
2,
confirmed_count(
await cli.call("token.transaction.get_history", token_id)
),
)
self.info("invalidating %d blocks", len(blocks))
n.invalidateblock(blocks[0])
self.hash_at_height.clear()
await self.sync_all(cli)
async def no_confirmed_txs():
return 0 == confirmed_count(
await cli.call("blockchain.address.get_history", address)
)
await wait_for(
10,
no_confirmed_txs,
f"got confirmed count {confirmed_count(await cli.call("blockchain.address.get_history", address))}, expecting 0",
)
if test_tokens:
assert_equal(
0,
confirmed_count(
await cli.call("token.transaction.get_history", token_id)
),
)
n.generate(50)
await self.sync_height(cli)
async def test_reorg_unspent_index_recreation( self, cli, n, coinbases
):
coinbase = coinbases.pop(0)
scriptpubkey1 = CScript([OP_TRUE, OP_DROP])
scriptpubkey2 = CScript([OP_TRUE, OP_TRUE, OP_DROP, OP_DROP])
scripthash1 = script_to_scripthash(lock(scriptpubkey1))
await self.sync_height(cli)
await self.wait_for_mempool_count(cli, count=0)
self.info("Step 1: Creating output in block N")
fee = 200
min_value = 10 * COIN
tx1_value = max(coinbase.vout[0].n_value - fee, min_value)
tx1 = self.create_transaction(
coinbase,
n=0,
value=tx1_value,
sig=anyonecanspend_unlocking_script(),
out=lock(scriptpubkey1),
)
pad_tx(tx1)
_ = tx1.get_rpc_hex_id() await cli.call("blockchain.transaction.broadcast", to_hex(tx1))
await self.wait_for_mempool_count(cli, count=1)
await self.sync_mempool(cli)
self.info("Step 2: Mining block N to confirm output")
n.generate(1)
await self.sync_height(cli)
await self.wait_for_mempool_count(cli, count=0)
balance1 = await cli.call("blockchain.scripthash.get_balance", scripthash1)
assert_equal(
tx1_value,
balance1["confirmed"],
"scripthash1 should have expected coins confirmed",
)
assert_equal(
0, balance1["unconfirmed"], "scripthash1 should have 0 unconfirmed"
)
self.info("Step 3: Spending output in block N+1")
original_balance = balance1["confirmed"]
spend_value = max(tx1.vout[0].n_value - fee, 10 * COIN)
tx2 = self.create_transaction(
tx1,
n=0,
value=spend_value,
sig=unlock(scriptpubkey1),
out=lock(scriptpubkey2),
)
pad_tx(tx2)
_ = tx2.get_rpc_hex_id() await cli.call("blockchain.transaction.broadcast", to_hex(tx2))
await self.wait_for_mempool_count(cli, count=1)
await self.sync_mempool(cli)
self.info("Step 4: Mining block N+1 to confirm spending")
n.generate(1)
block_n_plus_1 = n.getbestblockhash()
await self.sync_height(cli)
await self.wait_for_mempool_count(cli, count=0)
balance1_after_spend = await cli.call(
"blockchain.scripthash.get_balance", scripthash1
)
assert (
balance1_after_spend["confirmed"] != original_balance
), "scripthash1 balance should change after spending transaction"
self.info(
"Step 5: Invalidating block N+1 to trigger reorg and test unspent index recreation"
)
n.invalidateblock(block_n_plus_1)
self.hash_at_height.clear()
await self.sync_height(cli)
await asyncio.sleep(1)
self.info("Step 6: Verifying unspent index was recreated")
balance1_after_reorg = await cli.call(
"blockchain.scripthash.get_balance", scripthash1
)
assert_equal(
original_balance,
balance1_after_reorg["confirmed"],
f"scripthash1 should have {original_balance} confirmed after reorg "
"(unspent index should be recreated - output from block N should be restored)",
)
n.generate(1)
await self.sync_height(cli)
await self.wait_for_mempool_count(cli, count=0)
def _has_tx(self, history, txid):
for entry in history:
if entry["tx_hash"] == txid:
return True
return False
def _confirmed_history(self, history):
return [h for h in history if h["height"] > 0]
async def _create_and_confirm_funding_tx( self, cli, n, coinbase, scriptpubkey1, scripthash1
):
self.info("Step 1: Creating output in block N")
fee = 200
tx1 = self.create_transaction(
coinbase,
n=0,
value=coinbase.vout[0].n_value - fee,
sig=anyonecanspend_unlocking_script(),
out=lock(scriptpubkey1),
)
pad_tx(tx1)
txid1_hex = tx1.get_rpc_hex_id()
await cli.call("blockchain.transaction.broadcast", to_hex(tx1))
await self.wait_for_mempool_count(cli, count=1)
await self.sync_mempool(cli)
self.info(
"Step 2: Mining block N to confirm output (use node to avoid P2P issues)"
)
n.generate(1)
await self.sync_height(cli)
await self.wait_for_mempool_count(cli, count=0)
confirmed_hist1 = None
async def wait_for_conf():
nonlocal confirmed_hist1
history1 = await cli.call("blockchain.scripthash.get_history", scripthash1)
confirmed_hist1 = self._confirmed_history(history1)
return self._has_tx(confirmed_hist1, txid1_hex)
await wait_for(
10, wait_for_conf, "funding transaction should appear in confirmed history"
)
assert self._has_tx(
confirmed_hist1, txid1_hex
), "history should contain funding tx"
return tx1, txid1_hex
async def _create_and_confirm_spending_tx( self, cli, n, tx1, scriptpubkey1, scriptpubkey2, scripthash1, txid1_hex
):
self.info("Step 3: Spending output in block N+1")
fee = 200
tx2 = self.create_transaction(
tx1,
n=0,
value=tx1.vout[0].n_value - fee,
sig=unlock(scriptpubkey1),
out=lock(scriptpubkey2),
)
pad_tx(tx2)
txid2_hex = tx2.get_rpc_hex_id()
await cli.call("blockchain.transaction.broadcast", to_hex(tx2))
await self.wait_for_mempool_count(cli, count=1)
await self.sync_mempool(cli)
self.info(
"Step 4: Mining block N+1 to confirm spending (use node to avoid P2P issues)"
)
n.generate(1)
block_n_plus_1 = n.getbestblockhash()
await self.sync_height(cli)
await self.wait_for_mempool_count(cli, count=0)
confirmed_hist2 = None
async def wait_for_spending():
nonlocal confirmed_hist2
history2 = await cli.call("blockchain.scripthash.get_history", scripthash1)
confirmed_hist2 = self._confirmed_history(history2)
return self._has_tx(confirmed_hist2, txid1_hex) and self._has_tx(
confirmed_hist2, txid2_hex
)
await wait_for(
10,
wait_for_spending,
"both funding and spending transactions should appear in confirmed history",
)
assert self._has_tx(
confirmed_hist2, txid1_hex
), "history should contain funding tx"
assert self._has_tx(
confirmed_hist2, txid2_hex
), "history should contain spending tx"
return txid2_hex, block_n_plus_1
async def test_reorg_scripthash_history(self, cli, n, coinbases):
coinbase = coinbases.pop(0)
scriptpubkey1 = CScript([OP_TRUE, OP_DROP])
scriptpubkey2 = CScript([OP_TRUE, OP_TRUE, OP_DROP, OP_DROP])
scripthash1 = script_to_scripthash(lock(scriptpubkey1))
_ = script_to_scripthash(lock(scriptpubkey2))
await self.sync_height(cli)
await self.wait_for_mempool_count(cli, count=0)
tx1, txid1_hex = await self._create_and_confirm_funding_tx(
cli, n, coinbase, scriptpubkey1, scripthash1
)
txid2_hex, block_n_plus_1 = await self._create_and_confirm_spending_tx(
cli, n, tx1, scriptpubkey1, scriptpubkey2, scripthash1, txid1_hex
)
self.info("Step 5: Invalidating block N+1 to trigger reorg")
n.invalidateblock(block_n_plus_1)
self.hash_at_height.clear()
await self.sync_height(cli)
await asyncio.sleep(1)
self.info("Step 6: Verifying history after reorg")
history3 = await cli.call("blockchain.scripthash.get_history", scripthash1)
confirmed_hist3 = self._confirmed_history(history3)
assert self._has_tx(
confirmed_hist3, txid1_hex
), "history should still contain funding tx"
assert not self._has_tx(
confirmed_hist3, txid2_hex
), "history should NOT contain spending tx in confirmed (it was undone)"
self.info("✓ Test passed: Scripthash history correctly updated after reorg")
if __name__ == "__main__":
asyncio.run(ElectrumReorgTests().main())