mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2026-09-24 07:25:03 -05:00
Proper reorg transaction handling, fix fork finding, wallet only connects to local node if possible (#141)
This commit is contained in:
+79
-21
@@ -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
|
||||
|
||||
@@ -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(
|
||||
(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user