Proper reorg transaction handling, fix fork finding, wallet only connects to local node if possible (#141)

This commit is contained in:
Mariano Sorgente
2020-03-31 22:29:58 +09:00
committed by GitHub
parent 3ed39902c7
commit 5de840f13b
12 changed files with 653 additions and 61 deletions
+79 -21
View File
@@ -3,7 +3,7 @@ import logging
import multiprocessing
import time
from enum import Enum
from typing import Dict, List, Optional, Tuple
from typing import Dict, List, Optional, Tuple, Set
import asyncio
import concurrent
import blspy
@@ -219,7 +219,7 @@ class Blockchain:
ret_hashes.append(curr.header_hash)
return list(reversed(ret_hashes))
def find_fork_point(self, alternate_chain: List[bytes32]) -> uint32:
def find_fork_point_alternate_chain(self, alternate_chain: List[bytes32]) -> uint32:
"""
Takes in an alternate blockchain (headers), and compares it to self. Returns the last header
where both blockchains are equal.
@@ -714,7 +714,8 @@ class Blockchain:
prev_full_block: Optional[FullBlock]
if not genesis:
prev_full_block = await self.store.get_block(block.prev_header_hash)
assert prev_full_block is not None
if prev_full_block is None:
return Err.DOES_NOT_EXTEND
else:
prev_full_block = None
@@ -865,7 +866,7 @@ class Blockchain:
# If LCA changed update the unspent store
elif old_lca.header_hash != self.lca_block.header_hash:
# New LCA is lower height but not the a parent of old LCA (Reorg)
fork_h = self._find_fork_for_lca(old_lca, self.lca_block)
fork_h = self._find_fork_point_in_chain(old_lca, self.lca_block)
# Rollback to fork
await self.unspent_store.rollback_lca_to_block(fork_h)
@@ -911,20 +912,20 @@ class Blockchain:
curr_new = self.headers[curr_new.prev_header_hash]
curr_old = self.headers[curr_old.prev_header_hash]
def _find_fork_for_lca(self, old_lca: Header, new_lca: Header) -> uint32:
""" Tries to find height where new chain (current) diverged from the old chain where old_lca was the LCA"""
tmp_old: Header = old_lca
while tmp_old.header_hash != self.genesis.header_hash:
if tmp_old.header_hash == self.genesis.header_hash:
return uint32(0)
if tmp_old.height in self.height_to_hash:
chain_hash_at_h = self.height_to_hash[tmp_old.height]
if (
chain_hash_at_h == tmp_old.header_hash
and chain_hash_at_h != new_lca.header_hash
):
return tmp_old.height
tmp_old = self.headers[tmp_old.prev_header_hash]
def _find_fork_point_in_chain(self, block_1: Header, block_2: Header) -> uint32:
""" Tries to find height where new chain (block_2) diverged from block_1 (assuming prev blocks
are all included in chain)"""
while block_2.height > 0 or block_1.height > 0:
if block_2.height > block_1.height:
block_2 = self.headers[block_2.prev_header_hash]
elif block_1.height > block_2.height:
block_1 = self.headers[block_1.prev_header_hash]
else:
if block_2.header_hash == block_1.header_hash:
return block_2.height
block_2 = self.headers[block_2.prev_header_hash]
block_1 = self.headers[block_1.prev_header_hash]
assert block_2 == block_1 # Genesis block is the same, genesis fork
return uint32(0)
async def _create_diffs_for_tips(self, target: Header):
@@ -1011,6 +1012,8 @@ class Blockchain:
async def _validate_transactions(
self, block: FullBlock, fee_base: uint64
) -> Optional[Err]:
# TODO(straya): review, further test the code, and number all the validation steps
# 1. Check that transactions generator is present
if not block.transactions_generator:
return Err.UNKNOWN
@@ -1063,6 +1066,32 @@ class Blockchain:
return Err.DOUBLE_SPEND
# Check if removals exist and were not previously spend. (unspent_db + diff_store + this_block)
fork_h = self._find_fork_point_in_chain(self.lca_block, block.header)
# Get additions and removals since (after) fork_h but not including this block
additions_since_fork: Dict[bytes32, Tuple[Coin, uint32]] = {}
removals_since_fork: Set[bytes32] = set()
coinbases_since_fork: Dict[bytes32, uint32] = {}
curr: Optional[FullBlock] = await self.store.get_block(block.prev_header_hash)
assert curr is not None
while curr.height > fork_h:
removals_in_curr, additions_in_curr = await curr.tx_removals_and_additions()
for c_name in removals_in_curr:
removals_since_fork.add(c_name)
for c in additions_in_curr:
additions_since_fork[c.name()] = (c, curr.height)
additions_since_fork[curr.header.data.coinbase.name()] = (
curr.header.data.coinbase,
curr.height,
)
additions_since_fork[curr.header.data.fees_coin.name()] = (
curr.header.data.fees_coin,
curr.height,
)
coinbases_since_fork[curr.header.data.coinbase.name()] = curr.height
curr = await self.store.get_block(curr.prev_header_hash)
assert curr is not None
removal_coin_records: Dict[bytes32, CoinRecord] = {}
for rem in removals:
if rem in additions_dic:
@@ -1073,9 +1102,13 @@ class Blockchain:
else:
assert prev_header is not None
unspent = await self.unspent_store.get_coin_record(rem, prev_header)
if unspent:
if unspent.spent == 1:
if unspent is not None and unspent.confirmed_block_index <= fork_h:
# Spending something in the current chain, confirmed before fork
# (We ignore all coins confirmed after fork)
if unspent.spent == 1 and unspent.spent_block_index <= fork_h:
# Spend in an ancestor block, so this is a double spend
return Err.DOUBLE_SPEND
# If it's a coinbase, check that it's not frozen
if unspent.coinbase == 1:
if (
block.height
@@ -1084,7 +1117,32 @@ class Blockchain:
return Err.COINBASE_NOT_YET_SPENDABLE
removal_coin_records[unspent.name] = unspent
else:
return Err.UNKNOWN_UNSPENT
# This coin is not in the current heaviest chain, so it must be in the fork
if rem not in additions_since_fork:
# This coin does not exist in the fork
return Err.UNKNOWN_UNSPENT
if rem in coinbases_since_fork:
# This coin is a coinbase coin
if (
block.height
< coinbases_since_fork[rem] + self.coinbase_freeze
):
return Err.COINBASE_NOT_YET_SPENDABLE
new_coin, confirmed_height = additions_since_fork[rem]
new_coin_record: CoinRecord = CoinRecord(
new_coin,
confirmed_height,
uint32(0),
False,
(rem in coinbases_since_fork),
)
removal_coin_records[new_coin_record.name] = new_coin_record
# This check applies to both coins created before fork (pulled from coin_store),
# and coins created after fork (additions_since_fork)>
if rem in removals_since_fork:
# This coin was spent in the fork
return Err.DOUBLE_SPEND
# Check fees
removed = 0
-1
View File
@@ -42,7 +42,6 @@ class CoinStore:
self = cls()
self.cache_size = cache_size
# All full blocks which have been added to the blockchain. Header_hash -> block
self.coin_record_db = connection
await self.coin_record_db.execute(
(
+9 -1
View File
@@ -318,7 +318,9 @@ class FullNode:
# Finding the fork point allows us to only download headers and blocks from the fork point
header_hashes = self.store.get_potential_hashes()
fork_point_height: uint32 = self.blockchain.find_fork_point(header_hashes)
fork_point_height: uint32 = self.blockchain.find_fork_point_alternate_chain(
header_hashes
)
fork_point_hash: bytes32 = header_hashes[fork_point_height]
self.log.info(f"Fork point: {fork_point_hash} at height {fork_point_height}")
@@ -594,6 +596,12 @@ class FullNode:
async for msg in self._send_tips_to_farmers():
yield msg
lca = self.blockchain.lca_block
new_lca = wallet_protocol.NewLCA(lca.header_hash, lca.height, lca.weight)
yield OutboundMessage(
NodeType.WALLET, Message("new_lca", new_lca), Delivery.BROADCAST
)
@api_request
async def new_tip(
self, request: full_node_protocol.NewTip
+2 -2
View File
@@ -383,14 +383,14 @@ class ChiaServer:
raise ProtocolError(Err.INVALID_HANDSHAKE)
if inbound_handshake.node_id == self._node_id:
raise ProtocolError(Err.INVALID_HANDSHAKE)
raise ProtocolError(Err.SELF_CONNECTION)
# Makes sure that we only start one connection with each peer
connection.node_id = inbound_handshake.node_id
connection.peer_server_port = int(inbound_handshake.server_port)
connection.connection_type = inbound_handshake.node_type
if not self.global_connections.add(connection):
raise ProtocolError(Err.INVALID_HANDSHAKE)
raise ProtocolError(Err.DUPLICATE_CONNECTION)
# Send Ack message
await connection.send(Message("handshake_ack", HandshakeAck()))
+1 -2
View File
@@ -49,13 +49,12 @@ async def main():
log.info("Initializing blockchain from disk")
blockchain = await Blockchain.create(unspent_store, store)
log.info("Blockchain initialized")
mempool_manager = MempoolManager(unspent_store)
await mempool_manager.new_tips(await blockchain.get_full_tips())
# await mempool.initialize() TODO uncomment once it's implemented
full_node = FullNode(store, blockchain, config, mempool_manager, unspent_store)
# Starts the full node server (which full nodes can connect to)
if config["enable_upnp"]:
log.info(f"Attempting to enable UPnP (open up port {config['port']})")
+8 -7
View File
@@ -8,13 +8,14 @@ class Err(Enum):
BAD_HEADER_SIGNATURE = -2
MISSING_FROM_STORAGE = -3
INVALID_PROTOCOL_MESSAGE = -4
INVALID_HANDSHAKE = -5
INVALID_ACK = -6
INCOMPATIBLE_PROTOCOL_VERSION = -7
DUPLICATE_CONNECTION = -8
BLOCK_NOT_IN_BLOCKCHAIN = -9
NO_PROOF_OF_SPACE_FOUND = -10
PEERS_DONT_HAVE_BLOCK = -11
SELF_CONNECTION = -5
INVALID_HANDSHAKE = -6
INVALID_ACK = -7
INCOMPATIBLE_PROTOCOL_VERSION = -8
DUPLICATE_CONNECTION = -9
BLOCK_NOT_IN_BLOCKCHAIN = -10
NO_PROOF_OF_SPACE_FOUND = -11
PEERS_DONT_HAVE_BLOCK = -12
UNKNOWN = -9999
+3
View File
@@ -149,6 +149,9 @@ wallet:
starting_height: 0
num_sync_batches: 10
# Note that if this peer is connected, the wallet will not make additional connections.
# Comment out the next three lines if you don't want this functionality, and instead
# you want to connect to multiple external (random) full nodes.
full_node_peer:
host: 127.0.0.1
port: 8444
+19 -2
View File
@@ -228,7 +228,24 @@ class WalletNode:
diff = self.config["target_peer_count"] - len(
self.server.global_connections.get_full_node_connections()
)
return diff if diff >= 0 else 0
if diff < 0:
return 0
if "full_node_peer" in self.config:
full_node_peer = PeerInfo(
self.config["full_node_peer"]["host"],
self.config["full_node_peer"]["port"],
)
peers = [
c.get_peer_info()
for c in self.server.global_connections.get_full_node_connections()
]
if full_node_peer in peers:
self.log.info(
f"Will not attempt to connect to other nodes, already connected to {full_node_peer}"
)
return 0
return diff
@api_request
async def respond_peers(
@@ -300,7 +317,7 @@ class WalletNode:
raise TimeoutError("Took too long to fetch header hashes.")
# 2. Find fork point
fork_point_height: uint32 = self.wallet_state_manager.find_fork_point(
fork_point_height: uint32 = self.wallet_state_manager.find_fork_point_alternate_chain(
self.header_hashes
)
fork_point_hash: bytes32 = self.header_hashes[fork_point_height]
+19 -15
View File
@@ -442,7 +442,7 @@ class WalletStateManager:
async def get_transaction(self, tx_id: SpendBundle) -> Optional[TransactionRecord]:
return await self.tx_store.get_transaction_record(tx_id)
def find_fork_point(self, alternate_chain: List[bytes32]) -> uint32:
def find_fork_point_alternate_chain(self, alternate_chain: List[bytes32]) -> uint32:
"""
Takes in an alternate blockchain (headers), and compares it to self. Returns the last header
where both blockchains are equal. Used for syncing.
@@ -524,7 +524,9 @@ class WalletStateManager:
# Not genesis, updated LCA
if block.weight > self.block_records[self.lca].weight:
fork_h = self.find_fork_for_lca(block)
fork_h = self._find_fork_point_in_chain(
self.block_records[self.lca], block
)
await self.reorg_rollback(fork_h)
# Add blocks between fork point and new lca
@@ -783,20 +785,22 @@ class WalletStateManager:
return False
return True
def find_fork_for_lca(self, new_lca: BlockRecord) -> uint32:
""" Tries to find height where new chain (current) diverged from the old chain where old_lca was the LCA"""
tmp_old: BlockRecord = self.block_records[self.lca]
while new_lca.height > 0 or tmp_old.height > 0:
if new_lca.height > tmp_old.height:
new_lca = self.block_records[new_lca.prev_header_hash]
elif tmp_old.height > new_lca.height:
tmp_old = self.block_records[tmp_old.prev_header_hash]
def _find_fork_point_in_chain(
self, block_1: BlockRecord, block_2: BlockRecord
) -> uint32:
""" Tries to find height where new chain (block_2) diverged from block_1 (assuming prev blocks
are all included in chain)"""
while block_2.height > 0 or block_1.height > 0:
if block_2.height > block_1.height:
block_2 = self.block_records[block_2.prev_header_hash]
elif block_1.height > block_2.height:
block_1 = self.block_records[block_1.prev_header_hash]
else:
if new_lca.header_hash == tmp_old.header_hash:
return new_lca.height
new_lca = self.block_records[new_lca.prev_header_hash]
tmp_old = self.block_records[tmp_old.prev_header_hash]
assert new_lca == tmp_old # Genesis block is the same, genesis fork
if block_2.header_hash == block_1.header_hash:
return block_2.height
block_2 = self.block_records[block_2.prev_header_hash]
block_1 = self.block_records[block_1.prev_header_hash]
assert block_2 == block_1 # Genesis block is the same, genesis fork
return uint32(0)
def validate_select_proofs(
+8 -7
View File
@@ -384,14 +384,15 @@ async def start_websocket_server():
server = ChiaServer(config["port"], wallet_node, NodeType.WALLET)
wallet_node.set_server(server)
_ = await server.start_server("127.0.0.1", None, config)
full_node_peer = PeerInfo(
config["full_node_peer"]["host"], config["full_node_peer"]["port"]
)
_ = await server.start_server(config["host"], None, config)
if "full_node_peer" in config:
full_node_peer = PeerInfo(
config["full_node_peer"]["host"], config["full_node_peer"]["port"]
)
log.info(f"Connecting to full node peer at {full_node_peer}")
server.global_connections.peers.add(full_node_peer)
_ = await server.start_client(full_node_peer, None, config)
log.info(f"Connecting to full node peer at {full_node_peer}")
server.global_connections.peers.add(full_node_peer)
_ = await server.start_client(full_node_peer, None, config)
log.info("Starting websocket server.")
websocket_server = await websockets.serve(
+42
View File
@@ -622,6 +622,48 @@ class TestReorgs:
await connection.close()
@pytest.mark.asyncio
async def test_find_fork_point(self):
blocks = bt.get_consecutive_blocks(test_constants, 10, [], 9, b"7")
blocks_2 = bt.get_consecutive_blocks(test_constants, 6, blocks[:5], 9, b"8")
blocks_3 = bt.get_consecutive_blocks(test_constants, 8, blocks[:3], 9, b"9")
blocks_reorg = bt.get_consecutive_blocks(test_constants, 3, blocks[:9], 9, b"9")
db_path = Path("blockchain_test.db")
connection = await aiosqlite.connect(db_path)
unspent_store = await CoinStore.create(connection)
store = await FullNodeStore.create(connection)
await store._clear_database()
b: Blockchain = await Blockchain.create(unspent_store, store, test_constants)
for i in range(1, len(blocks)):
await b.receive_block(blocks[i])
for i in range(1, len(blocks_2)):
await b.receive_block(blocks_2[i])
assert b._find_fork_point_in_chain(blocks[10].header, blocks_2[10].header) == 4
for i in range(1, len(blocks_3)):
await b.receive_block(blocks_3[i])
assert b._find_fork_point_in_chain(blocks[10].header, blocks_3[10].header) == 2
assert b.lca_block.data == blocks[2].header.data
for i in range(1, len(blocks_reorg)):
await b.receive_block(blocks_reorg[i])
assert (
b._find_fork_point_in_chain(blocks[10].header, blocks_reorg[10].header) == 8
)
assert (
b._find_fork_point_in_chain(blocks_2[10].header, blocks_reorg[10].header)
== 4
)
assert b.lca_block.data == blocks[4].header.data
await connection.close()
@pytest.mark.asyncio
async def test_get_header_hashes(self):
blocks = bt.get_consecutive_blocks(test_constants, 5, [], 9, b"0")
+463 -3
View File
@@ -164,8 +164,7 @@ class TestBlockchainTransactions:
assert error is Err.DOUBLE_SPEND
@pytest.mark.asyncio
async def test_validate_blockchain_with_double_output(self, two_nodes):
async def test_validate_blockchain_duplicate_output(self, two_nodes):
num_blocks = 10
wallet_a = WalletTool()
coinbase_puzzlehash = wallet_a.get_new_puzzlehash()
@@ -209,8 +208,469 @@ class TestBlockchainTransactions:
assert error is Err.DUPLICATE_OUTPUT
@pytest.mark.asyncio
async def test_assert_my_coin_id(self, two_nodes):
async def test_validate_blockchain_with_reorg_double_spend(self, two_nodes):
num_blocks = 10
wallet_a = WalletTool()
coinbase_puzzlehash = wallet_a.get_new_puzzlehash()
wallet_receiver = WalletTool()
receiver_puzzlehash = wallet_receiver.get_new_puzzlehash()
blocks = bt.get_consecutive_blocks(
test_constants, num_blocks, [], 10, b"", coinbase_puzzlehash
)
full_node_1, full_node_2, server_1, server_2 = two_nodes
for block in blocks:
async for _ in full_node_1.respond_block(
full_node_protocol.RespondBlock(block)
):
pass
spent_block = blocks[1]
spend_bundle = wallet_a.generate_signed_transaction(
1000, receiver_puzzlehash, spent_block.header.data.coinbase
)
block_spendbundle = SpendBundle.aggregate([spend_bundle])
program = best_solution_program(block_spendbundle)
aggsig = block_spendbundle.aggregated_signature
dic_h = {11: (program, aggsig)}
blocks = bt.get_consecutive_blocks(
test_constants, 10, blocks, 10, b"", coinbase_puzzlehash, dic_h
)
# Move chain to height 20, with a spend at height 11
for block in blocks:
async for _ in full_node_1.respond_block(
full_node_protocol.RespondBlock(block)
):
pass
# Reorg at block 5, same spend at block 13 and 14 that was previously at block 11
dic_h = {13: (program, aggsig), 14: (program, aggsig)}
new_blocks = bt.get_consecutive_blocks(
test_constants,
9,
blocks[:6],
10,
b"another seed",
coinbase_puzzlehash,
dic_h,
)
for block in new_blocks[:13]:
async for _ in full_node_1.respond_block(
full_node_protocol.RespondBlock(block)
):
pass
next_block = new_blocks[13]
error = await full_node_1.blockchain._validate_transactions(
next_block, next_block.header.data.fees_coin.amount
)
assert error is None
[
_
async for _ in full_node_1.respond_block(
full_node_protocol.RespondBlock(new_blocks[13])
)
]
next_block = new_blocks[14]
error = await full_node_1.blockchain._validate_transactions(
next_block, next_block.header.data.fees_coin.amount
)
assert error is Err.DOUBLE_SPEND
# Now test Reorg at block 5, same spend at block 9 that was previously at block 11
dic_h = {9: (program, aggsig)}
new_blocks = bt.get_consecutive_blocks(
test_constants,
4,
blocks[:6],
10,
b"another seed 2",
coinbase_puzzlehash,
dic_h,
)
for block in new_blocks[:9]:
async for _ in full_node_1.respond_block(
full_node_protocol.RespondBlock(block)
):
pass
next_block = new_blocks[9]
error = await full_node_1.blockchain._validate_transactions(
next_block, next_block.header.data.fees_coin.amount
)
assert error is None
# Now test Reorg at block 10, same spend at block 11 that was previously at block 11
dic_h = {11: (program, aggsig)}
new_blocks = bt.get_consecutive_blocks(
test_constants,
4,
blocks[:11],
10,
b"another seed 3",
coinbase_puzzlehash,
dic_h,
)
for block in new_blocks[:11]:
async for _ in full_node_1.respond_block(
full_node_protocol.RespondBlock(block)
):
pass
next_block = new_blocks[11]
error = await full_node_1.blockchain._validate_transactions(
next_block, next_block.header.data.fees_coin.amount
)
assert error is None
# Now test Reorg at block 11, same spend at block 12 that was previously at block 11
dic_h = {12: (program, aggsig)}
new_blocks = bt.get_consecutive_blocks(
test_constants,
4,
blocks[:12],
10,
b"another seed 4",
coinbase_puzzlehash,
dic_h,
)
for block in new_blocks[:12]:
async for _ in full_node_1.respond_block(
full_node_protocol.RespondBlock(block)
):
pass
next_block = new_blocks[12]
error = await full_node_1.blockchain._validate_transactions(
next_block, next_block.header.data.fees_coin.amount
)
assert error is Err.DOUBLE_SPEND
# Now test Reorg at block 11, same spend at block 15 that was previously at block 11
dic_h = {15: (program, aggsig)}
new_blocks = bt.get_consecutive_blocks(
test_constants,
4,
blocks[:12],
10,
b"another seed 5",
coinbase_puzzlehash,
dic_h,
)
for block in new_blocks[:15]:
async for _ in full_node_1.respond_block(
full_node_protocol.RespondBlock(block)
):
pass
next_block = new_blocks[15]
error = await full_node_1.blockchain._validate_transactions(
next_block, next_block.header.data.fees_coin.amount
)
assert error is Err.DOUBLE_SPEND
@pytest.mark.asyncio
async def test_validate_blockchain_spend_reorg_coin(self, two_nodes):
num_blocks = 10
wallet_a = WalletTool()
coinbase_puzzlehash = wallet_a.get_new_puzzlehash()
receiver_1_puzzlehash = wallet_a.get_new_puzzlehash()
receiver_2_puzzlehash = wallet_a.get_new_puzzlehash()
receiver_3_puzzlehash = wallet_a.get_new_puzzlehash()
blocks = bt.get_consecutive_blocks(
test_constants, num_blocks, [], 10, b"", coinbase_puzzlehash
)
full_node_1, full_node_2, server_1, server_2 = two_nodes
for block in blocks:
async for _ in full_node_1.respond_block(
full_node_protocol.RespondBlock(block)
):
pass
spent_block = blocks[1]
spend_bundle = wallet_a.generate_signed_transaction(
1000, receiver_1_puzzlehash, spent_block.header.data.coinbase
)
block_spendbundle = SpendBundle.aggregate([spend_bundle])
program = best_solution_program(block_spendbundle)
aggsig = block_spendbundle.aggregated_signature
dic_h = {5: (program, aggsig)}
new_blocks = bt.get_consecutive_blocks(
test_constants,
1,
blocks[:5],
10,
b"spend_reorg_coin",
coinbase_puzzlehash,
dic_h,
)
[
_
async for _ in full_node_1.respond_block(
full_node_protocol.RespondBlock(new_blocks[-1])
)
]
assert new_blocks[-1].header_hash in full_node_1.blockchain.headers
coin_2 = None
for coin in new_blocks[-1].additions():
if coin.puzzle_hash == receiver_1_puzzlehash:
coin_2 = coin
break
assert coin_2 is not None
spend_bundle = wallet_a.generate_signed_transaction(
1000, receiver_2_puzzlehash, coin_2
)
block_spendbundle = SpendBundle.aggregate([spend_bundle])
program = best_solution_program(block_spendbundle)
aggsig = block_spendbundle.aggregated_signature
dic_h = {6: (program, aggsig)}
new_blocks = bt.get_consecutive_blocks(
test_constants,
1,
new_blocks[:6],
10,
b"spend_reorg_coin",
coinbase_puzzlehash,
dic_h,
)
[
_
async for _ in full_node_1.respond_block(
full_node_protocol.RespondBlock(new_blocks[-1])
)
]
assert new_blocks[-1].header_hash in full_node_1.blockchain.headers
coin_3 = None
for coin in new_blocks[-1].additions():
if coin.puzzle_hash == receiver_2_puzzlehash:
coin_3 = coin
break
assert coin_3 is not None
spend_bundle = wallet_a.generate_signed_transaction(
1000, receiver_3_puzzlehash, coin_3
)
block_spendbundle = SpendBundle.aggregate([spend_bundle])
program = best_solution_program(block_spendbundle)
aggsig = block_spendbundle.aggregated_signature
dic_h = {7: (program, aggsig)}
new_blocks = bt.get_consecutive_blocks(
test_constants,
1,
new_blocks[:7],
10,
b"spend_reorg_coin",
coinbase_puzzlehash,
dic_h,
)
[
_
async for _ in full_node_1.respond_block(
full_node_protocol.RespondBlock(new_blocks[-1])
)
]
assert new_blocks[-1].header_hash in full_node_1.blockchain.headers
coin_4 = None
for coin in new_blocks[-1].additions():
if coin.puzzle_hash == receiver_3_puzzlehash:
coin_4 = coin
break
assert coin_4 is not None
@pytest.mark.asyncio
async def test_validate_blockchain_spend_reorg_cb_coin(self, two_nodes):
num_blocks = 10
wallet_a = WalletTool()
coinbase_puzzlehash = wallet_a.get_new_puzzlehash()
receiver_1_puzzlehash = wallet_a.get_new_puzzlehash()
blocks = bt.get_consecutive_blocks(
test_constants, num_blocks, [], 10, b"", coinbase_puzzlehash
)
full_node_1, full_node_2, server_1, server_2 = two_nodes
for block in blocks:
async for _ in full_node_1.respond_block(
full_node_protocol.RespondBlock(block)
):
pass
# Spends a coinbase created in reorg
new_blocks = bt.get_consecutive_blocks(
test_constants, 1, blocks[:6], 10, b"reorg cb coin", coinbase_puzzlehash
)
[
_
async for _ in full_node_1.respond_block(
full_node_protocol.RespondBlock(new_blocks[-1])
)
]
assert new_blocks[-1].header_hash in full_node_1.blockchain.headers
spent_block = new_blocks[-1]
spend_bundle = wallet_a.generate_signed_transaction(
1000, receiver_1_puzzlehash, spent_block.header.data.coinbase
)
spend_bundle_2 = wallet_a.generate_signed_transaction(
1000, receiver_1_puzzlehash, spent_block.header.data.fees_coin
)
block_spendbundle = SpendBundle.aggregate([spend_bundle, spend_bundle_2])
program = best_solution_program(block_spendbundle)
aggsig = block_spendbundle.aggregated_signature
dic_h = {7: (program, aggsig)}
new_blocks = bt.get_consecutive_blocks(
test_constants,
1,
new_blocks,
10,
b"reorg cb coin",
coinbase_puzzlehash,
dic_h,
)
error = await full_node_1.blockchain._validate_transactions(
new_blocks[-1], new_blocks[-1].header.data.fees_coin.amount
)
assert error is None
[
_
async for _ in full_node_1.respond_block(
full_node_protocol.RespondBlock(new_blocks[-1])
)
]
assert new_blocks[-1].header_hash in full_node_1.blockchain.headers
coins_created = []
for coin in new_blocks[-1].additions():
if coin.puzzle_hash == receiver_1_puzzlehash:
coins_created.append(coin)
assert len(coins_created) == 2
@pytest.mark.asyncio
async def test_validate_blockchain_spend_reorg_cb_coin_freeze(
self, two_nodes_standard_freeze
):
num_blocks = 10
wallet_a = WalletTool()
coinbase_puzzlehash = wallet_a.get_new_puzzlehash()
receiver_1_puzzlehash = wallet_a.get_new_puzzlehash()
blocks = bt.get_consecutive_blocks(
test_constants, num_blocks, [], 10, b"", coinbase_puzzlehash
)
full_node_1, full_node_2, server_1, server_2 = two_nodes_standard_freeze
for block in blocks:
async for _ in full_node_1.respond_block(
full_node_protocol.RespondBlock(block)
):
pass
# Spends a coinbase created in reorg
new_blocks = bt.get_consecutive_blocks(
test_constants, 1, blocks[:6], 10, b"reorg cb coin", coinbase_puzzlehash
)
[
_
async for _ in full_node_1.respond_block(
full_node_protocol.RespondBlock(new_blocks[-1])
)
]
assert new_blocks[-1].header_hash in full_node_1.blockchain.headers
spent_block = new_blocks[-1]
spend_bundle = wallet_a.generate_signed_transaction(
1000, receiver_1_puzzlehash, spent_block.header.data.coinbase
)
block_spendbundle = SpendBundle.aggregate([spend_bundle])
program = best_solution_program(block_spendbundle)
aggsig = block_spendbundle.aggregated_signature
dic_h = {7: (program, aggsig)}
new_blocks = bt.get_consecutive_blocks(
test_constants,
1,
new_blocks,
10,
b"reorg cb coin",
coinbase_puzzlehash,
dic_h,
)
error = await full_node_1.blockchain._validate_transactions(
new_blocks[-1], new_blocks[-1].header.data.fees_coin.amount
)
assert error is Err.COINBASE_NOT_YET_SPENDABLE
@pytest.mark.asyncio
async def test_validate_blockchain_spend_reorg_since_genesis(self, two_nodes):
num_blocks = 10
wallet_a = WalletTool()
coinbase_puzzlehash = wallet_a.get_new_puzzlehash()
receiver_1_puzzlehash = wallet_a.get_new_puzzlehash()
blocks = bt.get_consecutive_blocks(
test_constants, num_blocks, [], 10, b"", coinbase_puzzlehash
)
full_node_1, full_node_2, server_1, server_2 = two_nodes
for block in blocks:
async for _ in full_node_1.respond_block(
full_node_protocol.RespondBlock(block)
):
pass
spent_block = blocks[1]
spend_bundle = wallet_a.generate_signed_transaction(
1000, receiver_1_puzzlehash, spent_block.header.data.coinbase
)
block_spendbundle = SpendBundle.aggregate([spend_bundle])
program = best_solution_program(block_spendbundle)
aggsig = block_spendbundle.aggregated_signature
dic_h = {11: (program, aggsig)}
new_blocks = bt.get_consecutive_blocks(
test_constants, 1, blocks, 10, b"", coinbase_puzzlehash, dic_h
)
[
_
async for _ in full_node_1.respond_block(
full_node_protocol.RespondBlock(new_blocks[-1])
)
]
# Spends a coin in a genesis reorg, that was already spent
dic_h = {5: (program, aggsig)}
new_blocks = bt.get_consecutive_blocks(
test_constants,
12,
[],
10,
b"reorg since genesis",
coinbase_puzzlehash,
dic_h,
)
for block in new_blocks:
[
_
async for _ in full_node_1.respond_block(
full_node_protocol.RespondBlock(block)
)
]
assert new_blocks[-1].header_hash in full_node_1.blockchain.headers
@pytest.mark.asyncio
async def test_assert_my_coin_id(self, two_nodes):
num_blocks = 10
wallet_a = WalletTool()
coinbase_puzzlehash = wallet_a.get_new_puzzlehash()