chia-blockchain/tests/full_node/test_full_node.py

1374 lines
50 KiB
Python

import asyncio
import pytest
from clvm.casts import int_to_bytes
import random
import time
from typing import Dict
from secrets import token_bytes
from src.consensus.constants import constants
from src.protocols import (
full_node_protocol as fnp,
timelord_protocol,
wallet_protocol,
introducer_protocol,
)
from src.server.outbound_message import NodeType
from src.types.peer_info import PeerInfo
from src.types.full_block import FullBlock
from src.types.proof_of_space import ProofOfSpace
from src.types.spend_bundle import SpendBundle
from src.util.bundle_tools import best_solution_program
from src.util.ints import uint16, uint32, uint64, uint8
from src.util.hash import std_hash
from src.full_node.full_node import FullNode
from src.types.condition_var_pair import ConditionVarPair
from src.types.condition_opcodes import ConditionOpcode
from tests.setup_nodes import setup_two_nodes, test_constants, bt
from tests.wallet_tools import WalletTool
from src.types.mempool_inclusion_status import MempoolInclusionStatus
from src.types.coin import hash_coin_list
from src.util.merkle_set import (
MerkleSet,
confirm_included_already_hashed,
confirm_not_included_already_hashed,
)
from src.util.errors import Err, ConsensusError
async def get_block_path(full_node: FullNode):
blocks_list = [(await full_node.blockchain.get_full_tips())[0]]
while blocks_list[0].height != 0:
b = await full_node.block_store.get_block(blocks_list[0].prev_header_hash)
assert b is not None
blocks_list.insert(0, b)
return blocks_list
@pytest.fixture(scope="module")
def event_loop():
loop = asyncio.get_event_loop()
yield loop
@pytest.fixture(scope="module")
async def two_nodes():
async for _ in setup_two_nodes({"COINBASE_FREEZE_PERIOD": 0}):
yield _
async def wb(num_blocks, two_nodes):
full_node_1, _, _, _ = two_nodes
wallet_a = WalletTool()
coinbase_puzzlehash = wallet_a.get_new_puzzlehash()
wallet_receiver = WalletTool()
blocks = bt.get_consecutive_blocks(
test_constants, num_blocks, [], 10, reward_puzzlehash=coinbase_puzzlehash
)
for i in range(1, num_blocks):
async for _ in full_node_1.respond_block(fnp.RespondBlock(blocks[i])):
pass
return wallet_a, wallet_receiver, blocks
@pytest.fixture(scope="module")
async def wallet_blocks(two_nodes):
"""
Sets up the node with 3 blocks, and returns a payer and payee wallet.
"""
return await wb(3, two_nodes)
@pytest.fixture(scope="module")
async def wallet_blocks_five(two_nodes):
return await wb(5, two_nodes)
class TestFullNodeProtocol:
@pytest.mark.asyncio
async def test_new_tip(self, two_nodes, wallet_blocks):
full_node_1, full_node_2, server_1, server_2 = two_nodes
_, _, blocks = wallet_blocks
await server_2.start_client(PeerInfo("127.0.0.1", uint16(server_1._port)), None)
await asyncio.sleep(2) # Allow connections to get made
new_tip_1 = fnp.NewTip(
blocks[-1].height, blocks[-1].weight, blocks[-1].header_hash
)
msgs_1 = [x async for x in full_node_1.new_tip(new_tip_1)]
assert len(msgs_1) == 1
assert msgs_1[0].message.data == fnp.RequestBlock(
uint32(3), blocks[-1].header_hash
)
new_tip_2 = fnp.NewTip(
blocks[2].height, blocks[2].weight, blocks[2].header_hash
)
msgs_2 = [x async for x in full_node_1.new_tip(new_tip_2)]
assert len(msgs_2) == 0
@pytest.mark.asyncio
async def test_new_transaction(self, two_nodes, wallet_blocks_five):
full_node_1, full_node_2, server_1, server_2 = two_nodes
wallet_a, wallet_receiver, blocks = wallet_blocks_five
conditions_dict: Dict = {ConditionOpcode.CREATE_COIN: []}
# Mempool has capacity of 100, make 110 unspents that we can use
puzzle_hashes = []
for _ in range(110):
receiver_puzzlehash = wallet_receiver.get_new_puzzlehash()
puzzle_hashes.append(receiver_puzzlehash)
output = ConditionVarPair(
ConditionOpcode.CREATE_COIN, receiver_puzzlehash, int_to_bytes(1000)
)
conditions_dict[ConditionOpcode.CREATE_COIN].append(output)
spend_bundle = wallet_a.generate_signed_transaction(
100,
receiver_puzzlehash,
blocks[1].header.data.coinbase,
condition_dic=conditions_dict,
)
assert spend_bundle is not None
new_transaction = fnp.NewTransaction(
spend_bundle.get_hash(), uint64(100), uint64(100)
)
# Not seen
msgs = [x async for x in full_node_1.new_transaction(new_transaction)]
assert len(msgs) == 1
assert msgs[0].message.data == fnp.RequestTransaction(spend_bundle.get_hash())
respond_transaction_2 = fnp.RespondTransaction(spend_bundle)
[x async for x in full_node_1.respond_transaction(respond_transaction_2)]
program = best_solution_program(spend_bundle)
aggsig = spend_bundle.aggregated_signature
dic_h = {5: (program, aggsig)}
coinbase_puzzlehash = wallet_a.get_new_puzzlehash()
blocks_new = bt.get_consecutive_blocks(
test_constants,
3,
blocks[:-1],
10,
reward_puzzlehash=coinbase_puzzlehash,
transaction_data_at_height=dic_h,
)
# Already seen
msgs = [x async for x in full_node_1.new_transaction(new_transaction)]
assert len(msgs) == 0
# Farm one block
for block in blocks_new:
[_ async for _ in full_node_1.respond_block(fnp.RespondBlock(block))]
spend_bundles = []
total_fee = 0
# Fill mempool
for puzzle_hash in puzzle_hashes:
coin_record = (
await full_node_1.coin_store.get_coin_records_by_puzzle_hash(
puzzle_hash, blocks_new[-3].header
)
)[0]
receiver_puzzlehash = wallet_receiver.get_new_puzzlehash()
fee = random.randint(2, 499)
spend_bundle = wallet_receiver.generate_signed_transaction(
500, receiver_puzzlehash, coin_record.coin, fee=fee
)
respond_transaction = fnp.RespondTransaction(spend_bundle)
res = [
x async for x in full_node_1.respond_transaction(respond_transaction)
]
# Added to mempool
if len(res) > 0:
total_fee += fee
spend_bundles.append(spend_bundle)
# Mempool is full
new_transaction = fnp.NewTransaction(
token_bytes(32), uint64(1000000), uint64(1)
)
msgs = [x async for x in full_node_1.new_transaction(new_transaction)]
assert len(msgs) == 0
agg = SpendBundle.aggregate(spend_bundles)
program = best_solution_program(agg)
aggsig = agg.aggregated_signature
dic_h = {8: (program, aggsig)}
coinbase_puzzlehash = wallet_a.get_new_puzzlehash()
blocks_new = bt.get_consecutive_blocks(
test_constants,
1,
blocks_new,
10,
reward_puzzlehash=coinbase_puzzlehash,
transaction_data_at_height=dic_h,
fees=uint64(total_fee),
)
# Farm one block to clear mempool
[_ async for _ in full_node_1.respond_block(fnp.RespondBlock(blocks_new[-1]))]
@pytest.mark.asyncio
async def test_request_respond_transaction(self, two_nodes, wallet_blocks_five):
full_node_1, full_node_2, server_1, server_2 = two_nodes
wallet_a, wallet_receiver, blocks = wallet_blocks_five
tx_id = token_bytes(32)
request_transaction = fnp.RequestTransaction(tx_id)
msgs = [x async for x in full_node_1.request_transaction(request_transaction)]
assert len(msgs) == 1
assert msgs[0].message.data == fnp.RejectTransactionRequest(tx_id)
receiver_puzzlehash = wallet_receiver.get_new_puzzlehash()
spend_bundle = wallet_a.generate_signed_transaction(
100, receiver_puzzlehash, blocks[2].header.data.coinbase,
)
assert spend_bundle is not None
respond_transaction = fnp.RespondTransaction(spend_bundle)
prop = [x async for x in full_node_1.respond_transaction(respond_transaction)]
assert len(prop) == 1
assert isinstance(prop[0].message.data, fnp.NewTransaction)
request_transaction = fnp.RequestTransaction(spend_bundle.get_hash())
msgs = [x async for x in full_node_1.request_transaction(request_transaction)]
assert len(msgs) == 1
assert msgs[0].message.data == fnp.RespondTransaction(spend_bundle)
@pytest.mark.asyncio
async def test_respond_transaction_fail(self, two_nodes, wallet_blocks):
full_node_1, full_node_2, server_1, server_2 = two_nodes
wallet_a, wallet_receiver, blocks = wallet_blocks
tx_id = token_bytes(32)
request_transaction = fnp.RequestTransaction(tx_id)
msgs = [x async for x in full_node_1.request_transaction(request_transaction)]
assert len(msgs) == 1
assert msgs[0].message.data == fnp.RejectTransactionRequest(tx_id)
receiver_puzzlehash = wallet_receiver.get_new_puzzlehash()
# Invalid transaction does not propagate
spend_bundle = wallet_a.generate_signed_transaction(
100000000000000, receiver_puzzlehash, blocks[3].header.data.coinbase,
)
assert spend_bundle is not None
respond_transaction = fnp.RespondTransaction(spend_bundle)
assert (
len([x async for x in full_node_1.respond_transaction(respond_transaction)])
== 0
)
@pytest.mark.asyncio
async def test_new_pot(self, two_nodes, wallet_blocks):
full_node_1, full_node_2, server_1, server_2 = two_nodes
wallet_a, wallet_receiver, _ = wallet_blocks
no_unf_block = fnp.NewProofOfTime(uint32(5), bytes(32 * [1]), uint64(124512))
assert len([x async for x in full_node_1.new_proof_of_time(no_unf_block)]) == 0
coinbase_puzzlehash = wallet_a.get_new_puzzlehash()
blocks = await get_block_path(full_node_1)
blocks_new = bt.get_consecutive_blocks(
test_constants,
1,
blocks[:-1],
10,
reward_puzzlehash=coinbase_puzzlehash,
seed=b"1212412",
)
unf_block = FullBlock(
blocks_new[-1].proof_of_space,
None,
blocks_new[-1].header,
blocks_new[-1].transactions_generator,
blocks_new[-1].transactions_filter,
)
unf_block_req = fnp.RespondUnfinishedBlock(unf_block)
res = [x async for x in full_node_1.respond_unfinished_block(unf_block_req)]
dont_have = fnp.NewProofOfTime(
unf_block.height,
unf_block.proof_of_space.challenge_hash,
res[0].message.data.iterations_needed,
)
assert len([x async for x in full_node_1.new_proof_of_time(dont_have)]) == 1
[x async for x in full_node_1.respond_block(fnp.RespondBlock(blocks_new[-1]))]
already_have = fnp.NewProofOfTime(
unf_block.height,
unf_block.proof_of_space.challenge_hash,
res[0].message.data.iterations_needed,
)
assert len([x async for x in full_node_1.new_proof_of_time(already_have)]) == 0
@pytest.mark.asyncio
async def test_request_pot(self, two_nodes, wallet_blocks):
full_node_1, full_node_2, server_1, server_2 = two_nodes
wallet_a, wallet_receiver, blocks = wallet_blocks
request = fnp.RequestProofOfTime(
blocks[3].height,
blocks[3].proof_of_space.challenge_hash,
blocks[3].proof_of_time.number_of_iterations,
)
res = [x async for x in full_node_1.request_proof_of_time(request)]
assert len(res) == 1
assert res[0].message.data.proof == blocks[3].proof_of_time
request_bad = fnp.RequestProofOfTime(
blocks[3].height,
blocks[3].proof_of_space.challenge_hash,
blocks[3].proof_of_time.number_of_iterations + 1,
)
res_bad = [x async for x in full_node_1.request_proof_of_time(request_bad)]
assert len(res_bad) == 1
assert isinstance(res_bad[0].message.data, fnp.RejectProofOfTimeRequest)
@pytest.mark.asyncio
async def test_respond_pot(self, two_nodes, wallet_blocks):
full_node_1, full_node_2, server_1, server_2 = two_nodes
wallet_a, wallet_receiver, blocks = wallet_blocks
coinbase_puzzlehash = wallet_a.get_new_puzzlehash()
blocks_list = await get_block_path(full_node_1)
blocks_new = bt.get_consecutive_blocks(
test_constants,
1,
blocks_list,
10,
reward_puzzlehash=coinbase_puzzlehash,
seed=b"another seed",
)
assert blocks_new[-1].proof_of_time is not None
new_pot = fnp.NewProofOfTime(
blocks_new[-1].height,
blocks_new[-1].proof_of_space.challenge_hash,
blocks_new[-1].proof_of_time.number_of_iterations,
)
[x async for x in full_node_1.new_proof_of_time(new_pot)]
# Don't have unfinished block
respond_pot = fnp.RespondProofOfTime(blocks_new[-1].proof_of_time)
res = [x async for x in full_node_1.respond_proof_of_time(respond_pot)]
assert len(res) == 0
unf_block = FullBlock(
blocks_new[-1].proof_of_space,
None,
blocks_new[-1].header,
blocks_new[-1].transactions_generator,
blocks_new[-1].transactions_filter,
)
unf_block_req = fnp.RespondUnfinishedBlock(unf_block)
[x async for x in full_node_1.respond_unfinished_block(unf_block_req)]
# Have unfinished block, finish
assert blocks_new[-1].proof_of_time is not None
respond_pot = fnp.RespondProofOfTime(blocks_new[-1].proof_of_time)
res = [x async for x in full_node_1.respond_proof_of_time(respond_pot)]
assert len(res) == 4
@pytest.mark.asyncio
async def test_new_unfinished(self, two_nodes, wallet_blocks):
full_node_1, full_node_2, server_1, server_2 = two_nodes
wallet_a, wallet_receiver, blocks = wallet_blocks
coinbase_puzzlehash = wallet_a.get_new_puzzlehash()
blocks_list = await get_block_path(full_node_1)
blocks_new = bt.get_consecutive_blocks(
test_constants,
1,
blocks_list,
10,
reward_puzzlehash=coinbase_puzzlehash,
seed=b"another seed 2",
)
assert blocks_new[-1].proof_of_time is not None
assert blocks_new[-2].proof_of_time is not None
already_have = fnp.NewUnfinishedBlock(
blocks_new[-2].prev_header_hash,
blocks_new[-2].proof_of_time.number_of_iterations,
blocks_new[-2].header_hash,
)
assert (
len([x async for x in full_node_1.new_unfinished_block(already_have)]) == 0
)
bad_prev = fnp.NewUnfinishedBlock(
blocks_new[-1].header_hash,
blocks_new[-1].proof_of_time.number_of_iterations,
blocks_new[-1].header_hash,
)
assert len([x async for x in full_node_1.new_unfinished_block(bad_prev)]) == 0
good = fnp.NewUnfinishedBlock(
blocks_new[-1].prev_header_hash,
blocks_new[-1].proof_of_time.number_of_iterations,
blocks_new[-1].header_hash,
)
assert len([x async for x in full_node_1.new_unfinished_block(good)]) == 1
unf_block = FullBlock(
blocks_new[-1].proof_of_space,
None,
blocks_new[-1].header,
blocks_new[-1].transactions_generator,
blocks_new[-1].transactions_filter,
)
unf_block_req = fnp.RespondUnfinishedBlock(unf_block)
[x async for x in full_node_1.respond_unfinished_block(unf_block_req)]
assert len([x async for x in full_node_1.new_unfinished_block(good)]) == 0
@pytest.mark.asyncio
async def test_request_unfinished(self, two_nodes, wallet_blocks):
full_node_1, full_node_2, server_1, server_2 = two_nodes
wallet_a, wallet_receiver, blocks = wallet_blocks
coinbase_puzzlehash = wallet_a.get_new_puzzlehash()
blocks_list = await get_block_path(full_node_1)
blocks_new = bt.get_consecutive_blocks(
test_constants,
2,
blocks_list,
10,
reward_puzzlehash=coinbase_puzzlehash,
seed=b"another seed 3",
)
# Add one block
[_ async for _ in full_node_1.respond_block(fnp.RespondBlock(blocks_new[-2]))]
unf_block = FullBlock(
blocks_new[-1].proof_of_space,
None,
blocks_new[-1].header,
blocks_new[-1].transactions_generator,
blocks_new[-1].transactions_filter,
)
unf_block_req = fnp.RespondUnfinishedBlock(unf_block)
# Don't have
req = fnp.RequestUnfinishedBlock(unf_block.header_hash)
res = [x async for x in full_node_1.request_unfinished_block(req)]
assert len(res) == 1
assert res[0].message.data == fnp.RejectUnfinishedBlockRequest(
unf_block.header_hash
)
# Have unfinished block
[x async for x in full_node_1.respond_unfinished_block(unf_block_req)]
res = [x async for x in full_node_1.request_unfinished_block(req)]
assert len(res) == 1
assert res[0].message.data == fnp.RespondUnfinishedBlock(unf_block)
# Have full block (genesis in this case)
req = fnp.RequestUnfinishedBlock(blocks_new[0].header_hash)
res = [x async for x in full_node_1.request_unfinished_block(req)]
assert len(res) == 1
assert res[0].message.data.block.header_hash == blocks_new[0].header_hash
@pytest.mark.asyncio
async def test_respond_unfinished(self, two_nodes, wallet_blocks):
full_node_1, full_node_2, server_1, server_2 = two_nodes
wallet_a, wallet_receiver, blocks = wallet_blocks
coinbase_puzzlehash = wallet_a.get_new_puzzlehash()
blocks_list = await get_block_path(full_node_1)
blocks_new = bt.get_consecutive_blocks(
test_constants,
1,
blocks_list[:],
4,
reward_puzzlehash=coinbase_puzzlehash,
seed=b"Another seed 4",
)
for block in blocks_new:
[_ async for _ in full_node_1.respond_block(fnp.RespondBlock(block))]
candidates = []
for i in range(50):
blocks_new_2 = bt.get_consecutive_blocks(
test_constants,
1,
blocks_new[:],
4,
reward_puzzlehash=coinbase_puzzlehash,
seed=i.to_bytes(4, "big") + b"Another seed",
)
candidates.append(blocks_new_2[-1])
unf_block_not_child = FullBlock(
blocks_new[-7].proof_of_space,
None,
blocks_new[-7].header,
blocks_new[-7].transactions_generator,
blocks_new[-7].transactions_filter,
)
unf_block_req_bad = fnp.RespondUnfinishedBlock(unf_block_not_child)
assert (
len(
[
x
async for x in full_node_1.respond_unfinished_block(
unf_block_req_bad
)
]
)
== 0
)
candidates = sorted(candidates, key=lambda c: c.proof_of_time.number_of_iterations) # type: ignore
def get_cand(index: int):
unf_block = FullBlock(
candidates[index].proof_of_space,
None,
candidates[index].header,
candidates[index].transactions_generator,
candidates[index].transactions_filter,
)
return fnp.RespondUnfinishedBlock(unf_block)
# Highest height should propagate
# Slow block should delay prop
start = time.time()
propagation_messages = [
x async for x in full_node_1.respond_unfinished_block(get_cand(20))
]
assert len(propagation_messages) == 2
assert isinstance(
propagation_messages[0].message.data, timelord_protocol.ProofOfSpaceInfo
)
assert isinstance(propagation_messages[1].message.data, fnp.NewUnfinishedBlock)
# TODO: fix
# assert time.time() - start > 3
# Already seen
assert (
len([x async for x in full_node_1.respond_unfinished_block(get_cand(20))])
== 0
)
# Slow equal height should not propagate
assert (
len([x async for x in full_node_1.respond_unfinished_block(get_cand(49))])
== 0
)
# Fastest equal height should propagate
start = time.time()
assert (
len([x async for x in full_node_1.respond_unfinished_block(get_cand(0))])
== 2
)
assert time.time() - start < 3
# Equal height (fast) should propagate
for i in range(1, 5):
# Checks a few blocks in case they have the same PoS
if (
candidates[i].proof_of_space.get_hash()
!= candidates[0].proof_of_space.get_hash()
):
start = time.time()
assert (
len(
[
x
async for x in full_node_1.respond_unfinished_block(
get_cand(i)
)
]
)
== 2
)
assert time.time() - start < 3
break
# Equal height (slow) should not propagate
assert (
len([x async for x in full_node_1.respond_unfinished_block(get_cand(40))])
== 0
)
# Don't propagate at old height
[_ async for _ in full_node_1.respond_block(fnp.RespondBlock(candidates[0]))]
blocks_new_3 = bt.get_consecutive_blocks(
test_constants,
1,
blocks_new[:] + [candidates[0]],
10,
reward_puzzlehash=coinbase_puzzlehash,
)
unf_block_new = FullBlock(
blocks_new_3[-1].proof_of_space,
None,
blocks_new_3[-1].header,
blocks_new_3[-1].transactions_generator,
blocks_new_3[-1].transactions_filter,
)
unf_block_new_req = fnp.RespondUnfinishedBlock(unf_block_new)
[x async for x in full_node_1.respond_unfinished_block(unf_block_new_req)]
assert (
len([x async for x in full_node_1.respond_unfinished_block(get_cand(10))])
== 0
)
@pytest.mark.asyncio
async def test_request_all_header_hashes(self, two_nodes, wallet_blocks):
full_node_1, full_node_2, server_1, server_2 = two_nodes
wallet_a, wallet_receiver, blocks = wallet_blocks
tips = full_node_1.blockchain.get_current_tips()
request = fnp.RequestAllHeaderHashes(tips[0].header_hash)
msgs = [x async for x in full_node_1.request_all_header_hashes(request)]
assert len(msgs) == 1
assert len(msgs[0].message.data.header_hashes) > 0
@pytest.mark.asyncio
async def test_request_block(self, two_nodes, wallet_blocks):
full_node_1, full_node_2, server_1, server_2 = two_nodes
wallet_a, wallet_receiver, blocks = wallet_blocks
msgs = [
x
async for x in full_node_1.request_header_block(
fnp.RequestHeaderBlock(uint32(1), blocks[1].header_hash)
)
]
assert len(msgs) == 1
assert msgs[0].message.data.header_block.header_hash == blocks[1].header_hash
msgs_reject_1 = [
x
async for x in full_node_1.request_header_block(
fnp.RequestHeaderBlock(uint32(1), blocks[2].header_hash)
)
]
assert len(msgs_reject_1) == 1
assert msgs_reject_1[0].message.data == fnp.RejectHeaderBlockRequest(
uint32(1), blocks[2].header_hash
)
msgs_reject_2 = [
x
async for x in full_node_1.request_header_block(
fnp.RequestHeaderBlock(uint32(1), bytes([0] * 32))
)
]
assert len(msgs_reject_2) == 1
assert msgs_reject_2[0].message.data == fnp.RejectHeaderBlockRequest(
uint32(1), bytes([0] * 32)
)
# Full blocks
msgs_2 = [
x
async for x in full_node_1.request_block(
fnp.RequestBlock(uint32(1), blocks[1].header_hash)
)
]
assert len(msgs_2) == 1
assert msgs_2[0].message.data.block.header_hash == blocks[1].header_hash
msgs_reject_3 = [
x
async for x in full_node_1.request_block(
fnp.RequestHeaderBlock(uint32(1), bytes([0] * 32))
)
]
assert len(msgs_reject_3) == 1
assert msgs_reject_3[0].message.data == fnp.RejectBlockRequest(
uint32(1), bytes([0] * 32)
)
@pytest.mark.asyncio
async def test_respond_block(self, two_nodes, wallet_blocks):
full_node_1, full_node_2, server_1, server_2 = two_nodes
wallet_a, wallet_receiver, blocks = wallet_blocks
# Already seen
msgs = [_ async for _ in full_node_1.respond_block(fnp.RespondBlock(blocks[0]))]
assert len(msgs) == 0
tip_hashes = set(
[t.header_hash for t in full_node_1.blockchain.get_current_tips()]
)
blocks_list = await get_block_path(full_node_1)
blocks_new = bt.get_consecutive_blocks(
test_constants, 5, blocks_list[:], 10, seed=b"Another seed 5",
)
# In sync mode
full_node_1.sync_store.set_sync_mode(True)
msgs = [
_ async for _ in full_node_1.respond_block(fnp.RespondBlock(blocks_new[-5]))
]
assert len(msgs) == 0
full_node_1.sync_store.set_sync_mode(False)
# If invalid, do nothing
block_invalid = FullBlock(
ProofOfSpace(
blocks_new[-5].proof_of_space.challenge_hash,
blocks_new[-5].proof_of_space.pool_pubkey,
blocks_new[-5].proof_of_space.plot_pubkey,
uint8(blocks_new[-5].proof_of_space.size + 1),
blocks_new[-5].proof_of_space.proof,
),
blocks_new[-5].proof_of_time,
blocks_new[-5].header,
blocks_new[-5].transactions_generator,
blocks_new[-5].transactions_filter,
)
threw = False
try:
msgs = [
_
async for _ in full_node_1.respond_block(
fnp.RespondBlock(block_invalid)
)
]
except ConsensusError:
threw = True
assert threw
# If a few blocks behind, request short sync
msgs = [
_ async for _ in full_node_1.respond_block(fnp.RespondBlock(blocks_new[-3]))
]
assert len(msgs) == 1
assert isinstance(msgs[0].message.data, fnp.RequestBlock)
# Updates full nodes, farmers, and timelords
tip_hashes_again = set(
[t.header_hash for t in full_node_1.blockchain.get_current_tips()]
)
assert tip_hashes_again == tip_hashes
msgs = [
_ async for _ in full_node_1.respond_block(fnp.RespondBlock(blocks_new[-5]))
]
assert len(msgs) == 5 or len(msgs) == 6
# Updates blockchain tips
tip_hashes_again = set(
[t.header_hash for t in full_node_1.blockchain.get_current_tips()]
)
assert tip_hashes_again != tip_hashes
# If orphan, don't send anything
blocks_orphan = bt.get_consecutive_blocks(
test_constants, 1, blocks_list[:-5], 10, seed=b"Another seed 6",
)
msgs = [
_
async for _ in full_node_1.respond_block(
fnp.RespondBlock(blocks_orphan[-1])
)
]
assert len(msgs) == 0
@pytest.mark.asyncio
async def test_request_peers(self, two_nodes, wallet_blocks):
full_node_1, full_node_2, server_1, server_2 = two_nodes
wallet_a, wallet_receiver, blocks = wallet_blocks
await server_2.start_client(PeerInfo("localhost", uint16(server_1._port)), None)
await asyncio.sleep(2) # Allow connections to get made
msgs = [
_
async for _ in full_node_1.request_peers(introducer_protocol.RequestPeers())
]
assert len(msgs[0].message.data.peer_list) > 0
class TestWalletProtocol:
@pytest.mark.asyncio
async def test_send_transaction(self, two_nodes, wallet_blocks):
full_node_1, full_node_2, server_1, server_2 = two_nodes
wallet_a, wallet_receiver, blocks = wallet_blocks
await server_2.start_client(PeerInfo("localhost", uint16(server_1._port)), None)
blocks_list = await get_block_path(full_node_1)
blocks_new = bt.get_consecutive_blocks(
test_constants,
1,
block_list=blocks_list,
seed=b"test_request_additions",
reward_puzzlehash=wallet_a.get_new_puzzlehash(),
)
async for _ in full_node_1.respond_block(fnp.RespondBlock(blocks_new[-1])):
pass
spend_bundle = wallet_a.generate_signed_transaction(
100, wallet_a.get_new_puzzlehash(), blocks_new[-1].header.data.coinbase,
)
spend_bundle_bad = wallet_a.generate_signed_transaction(
uint64.from_bytes(constants["MAX_COIN_AMOUNT"]),
wallet_a.get_new_puzzlehash(),
blocks_new[-1].header.data.coinbase,
)
msgs = [
_
async for _ in full_node_1.send_transaction(
wallet_protocol.SendTransaction(spend_bundle)
)
]
assert len(msgs) == 2
wallet_message = None
for msg in msgs:
if msg.peer_type == NodeType.WALLET:
wallet_message = msg
assert wallet_message is not None
assert wallet_message.message.data == wallet_protocol.TransactionAck(
spend_bundle.name(), MempoolInclusionStatus.SUCCESS, None
)
msgs = [
_
async for _ in full_node_1.send_transaction(
wallet_protocol.SendTransaction(spend_bundle)
)
]
assert len(msgs) == 2
ack_msg = None
for msg in msgs:
if msg.message.function == "transaction_ack":
ack_msg = msg
assert ack_msg is not None
assert ack_msg.message.data == wallet_protocol.TransactionAck(
spend_bundle.name(), MempoolInclusionStatus.SUCCESS, None
)
msgs = [
_
async for _ in full_node_1.send_transaction(
wallet_protocol.SendTransaction(spend_bundle_bad)
)
]
assert len(msgs) == 1
assert msgs[0].message.data == wallet_protocol.TransactionAck(
spend_bundle_bad.name(),
MempoolInclusionStatus.FAILED,
Err.COIN_AMOUNT_EXCEEDS_MAXIMUM.name,
)
@pytest.mark.asyncio
async def test_request_all_proof_hashes(self, two_nodes):
full_node_1, full_node_2, server_1, server_2 = two_nodes
blocks_list = await get_block_path(full_node_1)
msgs = [
_
async for _ in full_node_1.request_all_proof_hashes(
wallet_protocol.RequestAllProofHashes()
)
]
hashes = msgs[0].message.data.hashes
assert len(hashes) >= len(blocks_list) - 2
for i in range(len(hashes)):
if (
i % test_constants["DIFFICULTY_EPOCH"]
== test_constants["DIFFICULTY_DELAY"]
):
assert hashes[i][1] is not None
elif i > 0:
assert hashes[i][1] is None
if (
i % test_constants["DIFFICULTY_EPOCH"]
== test_constants["DIFFICULTY_EPOCH"] - 1
):
assert hashes[i][2] is not None
else:
assert hashes[i][2] is None
assert hashes[i][0] == std_hash(
blocks_list[i].proof_of_space.get_hash()
+ blocks_list[i].proof_of_time.output.get_hash()
)
@pytest.mark.asyncio
async def test_request_all_header_hashes_after(self, two_nodes):
full_node_1, full_node_2, server_1, server_2 = two_nodes
blocks_list = await get_block_path(full_node_1)
msgs = [
_
async for _ in full_node_1.request_all_header_hashes_after(
wallet_protocol.RequestAllHeaderHashesAfter(
uint32(5), blocks_list[5].proof_of_space.challenge_hash
)
)
]
assert len(msgs) == 1
assert isinstance(
msgs[0].message.data, wallet_protocol.RespondAllHeaderHashesAfter
)
assert msgs[0].message.data.starting_height == 5
assert (
msgs[0].message.data.previous_challenge_hash
== blocks_list[5].proof_of_space.challenge_hash
)
assert msgs[0].message.data.hashes[:3] == [
b.header_hash for b in blocks_list[5:8]
]
# Wrong prev challenge
msgs = [
_
async for _ in full_node_1.request_all_header_hashes_after(
wallet_protocol.RequestAllHeaderHashesAfter(
uint32(5), blocks_list[4].proof_of_space.challenge_hash
)
)
]
assert len(msgs) == 1
assert isinstance(
msgs[0].message.data, wallet_protocol.RejectAllHeaderHashesAfterRequest
)
assert msgs[0].message.data.starting_height == 5
assert (
msgs[0].message.data.previous_challenge_hash
== blocks_list[4].proof_of_space.challenge_hash
)
@pytest.mark.asyncio
async def test_request_header(self, two_nodes):
full_node_1, full_node_2, server_1, server_2 = two_nodes
num_blocks = 2
blocks = bt.get_consecutive_blocks(
test_constants, num_blocks, [], 10, seed=b"test_request_header"
)
for block in blocks[:2]:
async for _ in full_node_1.respond_block(fnp.RespondBlock(block)):
pass
msgs = [
_
async for _ in full_node_1.request_header(
wallet_protocol.RequestHeader(uint32(1), blocks[1].header_hash)
)
]
assert len(msgs) == 1
assert isinstance(msgs[0].message.data, wallet_protocol.RespondHeader)
assert msgs[0].message.data.header_block.header == blocks[1].header
assert msgs[0].message.data.transactions_filter == blocks[1].transactions_filter
# Don't have
msgs = [
_
async for _ in full_node_1.request_header(
wallet_protocol.RequestHeader(uint32(2), blocks[2].header_hash)
)
]
assert len(msgs) == 1
assert isinstance(msgs[0].message.data, wallet_protocol.RejectHeaderRequest)
assert msgs[0].message.data.height == 2
assert msgs[0].message.data.header_hash == blocks[2].header_hash
@pytest.mark.asyncio
async def test_request_removals(self, two_nodes, wallet_blocks):
full_node_1, full_node_2, server_1, server_2 = two_nodes
wallet_a, wallet_receiver, blocks = wallet_blocks
await server_2.start_client(PeerInfo("localhost", uint16(server_1._port)), None)
blocks_list = await get_block_path(full_node_1)
blocks_new = bt.get_consecutive_blocks(
test_constants, 5, seed=b"test_request_removals"
)
# Request removals for nonexisting block fails
msgs = [
_
async for _ in full_node_1.request_removals(
wallet_protocol.RequestRemovals(
blocks_new[-1].height, blocks_new[-1].header_hash, None
)
)
]
assert len(msgs) == 1
assert isinstance(msgs[0].message.data, wallet_protocol.RejectRemovalsRequest)
# Request removals for orphaned block fails
for block in blocks_new:
async for _ in full_node_1.respond_block(fnp.RespondBlock(block)):
pass
msgs = [
_
async for _ in full_node_1.request_removals(
wallet_protocol.RequestRemovals(
blocks_new[-1].height, blocks_new[-1].header_hash, None
)
)
]
assert len(msgs) == 1
assert isinstance(msgs[0].message.data, wallet_protocol.RejectRemovalsRequest)
# If there are no transactions, empty proof and coins
blocks_new = bt.get_consecutive_blocks(
test_constants,
10,
block_list=blocks_list,
reward_puzzlehash=wallet_a.get_new_puzzlehash(),
)
for block in blocks_new:
[_ async for _ in full_node_1.respond_block(fnp.RespondBlock(block))]
msgs = [
_
async for _ in full_node_1.request_removals(
wallet_protocol.RequestRemovals(
blocks_new[-4].height, blocks_new[-4].header_hash, None
)
)
]
assert len(msgs) == 1
assert isinstance(msgs[0].message.data, wallet_protocol.RespondRemovals)
assert len(msgs[0].message.data.coins) == 0
assert msgs[0].message.data.proofs is None
# Add a block with transactions
spend_bundles = []
for i in range(5):
spend_bundles.append(
wallet_a.generate_signed_transaction(
100,
wallet_a.get_new_puzzlehash(),
blocks_new[i - 8].header.data.coinbase,
)
)
height_with_transactions = len(blocks_new) + 1
agg = SpendBundle.aggregate(spend_bundles)
dic_h = {
height_with_transactions: (
best_solution_program(agg),
agg.aggregated_signature,
)
}
blocks_new = bt.get_consecutive_blocks(
test_constants, 5, block_list=blocks_new, transaction_data_at_height=dic_h
)
for block in blocks_new:
[_ async for _ in full_node_1.respond_block(fnp.RespondBlock(block))]
# If no coins requested, respond all coins and NO proof
msgs = [
_
async for _ in full_node_1.request_removals(
wallet_protocol.RequestRemovals(
blocks_new[height_with_transactions].height,
blocks_new[height_with_transactions].header_hash,
None,
)
)
]
assert len(msgs) == 1
assert isinstance(msgs[0].message.data, wallet_protocol.RespondRemovals)
assert len(msgs[0].message.data.coins) == 5
assert msgs[0].message.data.proofs is None
removals_merkle_set = MerkleSet()
for sb in spend_bundles:
for coin in sb.removals():
if coin is not None:
removals_merkle_set.add_already_hashed(coin.name())
# Ask for one coin and check PoI
coin_list = [spend_bundles[0].removals()[0].name()]
msgs = [
_
async for _ in full_node_1.request_removals(
wallet_protocol.RequestRemovals(
blocks_new[height_with_transactions].height,
blocks_new[height_with_transactions].header_hash,
coin_list,
)
)
]
assert len(msgs) == 1
assert isinstance(msgs[0].message.data, wallet_protocol.RespondRemovals)
assert len(msgs[0].message.data.coins) == 1
assert msgs[0].message.data.proofs is not None
assert len(msgs[0].message.data.proofs) == 1
assert confirm_included_already_hashed(
blocks_new[height_with_transactions].header.data.removals_root,
coin_list[0],
msgs[0].message.data.proofs[0][1],
)
# Ask for one coin and check PoE
coin_list = [token_bytes(32)]
msgs = [
_
async for _ in full_node_1.request_removals(
wallet_protocol.RequestRemovals(
blocks_new[height_with_transactions].height,
blocks_new[height_with_transactions].header_hash,
coin_list,
)
)
]
assert len(msgs) == 1
assert isinstance(msgs[0].message.data, wallet_protocol.RespondRemovals)
assert len(msgs[0].message.data.coins) == 1
assert msgs[0].message.data.coins[0][1] is None
assert msgs[0].message.data.proofs is not None
assert len(msgs[0].message.data.proofs) == 1
assert confirm_not_included_already_hashed(
blocks_new[height_with_transactions].header.data.removals_root,
coin_list[0],
msgs[0].message.data.proofs[0][1],
)
# Ask for two coins
coin_list = [spend_bundles[0].removals()[0].name(), token_bytes(32)]
msgs = [
_
async for _ in full_node_1.request_removals(
wallet_protocol.RequestRemovals(
blocks_new[height_with_transactions].height,
blocks_new[height_with_transactions].header_hash,
coin_list,
)
)
]
assert len(msgs) == 1
assert isinstance(msgs[0].message.data, wallet_protocol.RespondRemovals)
assert len(msgs[0].message.data.coins) == 2
assert msgs[0].message.data.coins[0][1] is not None
assert msgs[0].message.data.coins[1][1] is None
assert msgs[0].message.data.proofs is not None
assert len(msgs[0].message.data.proofs) == 2
assert confirm_included_already_hashed(
blocks_new[height_with_transactions].header.data.removals_root,
coin_list[0],
msgs[0].message.data.proofs[0][1],
)
assert confirm_not_included_already_hashed(
blocks_new[height_with_transactions].header.data.removals_root,
coin_list[1],
msgs[0].message.data.proofs[1][1],
)
@pytest.mark.asyncio
async def test_request_additions(self, two_nodes, wallet_blocks):
full_node_1, full_node_2, server_1, server_2 = two_nodes
wallet_a, wallet_receiver, blocks = wallet_blocks
await server_2.start_client(PeerInfo("localhost", uint16(server_1._port)), None)
blocks_list = await get_block_path(full_node_1)
blocks_new = bt.get_consecutive_blocks(
test_constants, 5, seed=b"test_request_additions"
)
# Request additinos for nonexisting block fails
msgs = [
_
async for _ in full_node_1.request_additions(
wallet_protocol.RequestAdditions(
blocks_new[-1].height, blocks_new[-1].header_hash, None
)
)
]
assert len(msgs) == 1
assert isinstance(msgs[0].message.data, wallet_protocol.RejectAdditionsRequest)
# Request additions for orphaned block fails
for block in blocks_new:
async for _ in full_node_1.respond_block(fnp.RespondBlock(block)):
pass
msgs = [
_
async for _ in full_node_1.request_additions(
wallet_protocol.RequestAdditions(
blocks_new[-1].height, blocks_new[-1].header_hash, None
)
)
]
assert len(msgs) == 1
assert isinstance(msgs[0].message.data, wallet_protocol.RejectAdditionsRequest)
# If there are no transactions, empty proof and coins
blocks_new = bt.get_consecutive_blocks(
test_constants,
10,
block_list=blocks_list,
reward_puzzlehash=wallet_a.get_new_puzzlehash(),
)
for block in blocks_new:
[_ async for _ in full_node_1.respond_block(fnp.RespondBlock(block))]
msgs = [
_
async for _ in full_node_1.request_additions(
wallet_protocol.RequestAdditions(
blocks_new[-4].height, blocks_new[-4].header_hash, None
)
)
]
assert len(msgs) == 1
assert isinstance(msgs[0].message.data, wallet_protocol.RespondAdditions)
assert len(msgs[0].message.data.coins) == 0
assert msgs[0].message.data.proofs is None
# Add a block with transactions
spend_bundles = []
puzzle_hashes = [wallet_a.get_new_puzzlehash(), wallet_a.get_new_puzzlehash()]
for i in range(5):
spend_bundles.append(
wallet_a.generate_signed_transaction(
100, puzzle_hashes[i % 2], blocks_new[i - 8].header.data.coinbase,
)
)
height_with_transactions = len(blocks_new) + 1
agg = SpendBundle.aggregate(spend_bundles)
dic_h = {
height_with_transactions: (
best_solution_program(agg),
agg.aggregated_signature,
)
}
blocks_new = bt.get_consecutive_blocks(
test_constants, 5, block_list=blocks_new, transaction_data_at_height=dic_h
)
for block in blocks_new:
[_ async for _ in full_node_1.respond_block(fnp.RespondBlock(block))]
# If no puzzle hashes requested, respond all coins and NO proof
msgs = [
_
async for _ in full_node_1.request_additions(
wallet_protocol.RequestAdditions(
blocks_new[height_with_transactions].height,
blocks_new[height_with_transactions].header_hash,
None,
)
)
]
assert len(msgs) == 1
assert isinstance(msgs[0].message.data, wallet_protocol.RespondAdditions)
# One puzzle hash with change and fee (x3) = 9, minus two repeated ph = 7
assert len(msgs[0].message.data.coins) == 7
assert msgs[0].message.data.proofs is None
additions_merkle_set = MerkleSet()
for sb in spend_bundles:
for coin in sb.additions():
if coin is not None:
additions_merkle_set.add_already_hashed(coin.name())
# Ask for one coin and check both PoI
ph_list = [puzzle_hashes[0]]
msgs = [
_
async for _ in full_node_1.request_additions(
wallet_protocol.RequestAdditions(
blocks_new[height_with_transactions].height,
blocks_new[height_with_transactions].header_hash,
ph_list,
)
)
]
assert len(msgs) == 1
assert isinstance(msgs[0].message.data, wallet_protocol.RespondAdditions)
assert len(msgs[0].message.data.coins) == 1
assert len(msgs[0].message.data.coins[0][1]) == 3
assert msgs[0].message.data.proofs is not None
assert len(msgs[0].message.data.proofs) == 1
assert confirm_included_already_hashed(
blocks_new[height_with_transactions].header.data.additions_root,
ph_list[0],
msgs[0].message.data.proofs[0][1],
)
coin_list_for_ph = [
coin
for coin in blocks_new[height_with_transactions].additions()
if coin.puzzle_hash == ph_list[0]
]
assert confirm_included_already_hashed(
blocks_new[height_with_transactions].header.data.additions_root,
hash_coin_list(coin_list_for_ph),
msgs[0].message.data.proofs[0][2],
)
# Ask for one ph and check PoE
ph_list = [token_bytes(32)]
msgs = [
_
async for _ in full_node_1.request_additions(
wallet_protocol.RequestAdditions(
blocks_new[height_with_transactions].height,
blocks_new[height_with_transactions].header_hash,
ph_list,
)
)
]
assert len(msgs) == 1
assert isinstance(msgs[0].message.data, wallet_protocol.RespondAdditions)
assert len(msgs[0].message.data.coins) == 1
assert len(msgs[0].message.data.coins[0][1]) == 0
assert msgs[0].message.data.proofs is not None
assert len(msgs[0].message.data.proofs) == 1
assert confirm_not_included_already_hashed(
blocks_new[height_with_transactions].header.data.additions_root,
ph_list[0],
msgs[0].message.data.proofs[0][1],
)
assert msgs[0].message.data.proofs[0][2] is None
# Ask for two puzzle_hashes
ph_list = [puzzle_hashes[0], token_bytes(32)]
msgs = [
_
async for _ in full_node_1.request_additions(
wallet_protocol.RequestAdditions(
blocks_new[height_with_transactions].height,
blocks_new[height_with_transactions].header_hash,
ph_list,
)
)
]
assert len(msgs) == 1
assert isinstance(msgs[0].message.data, wallet_protocol.RespondAdditions)
assert len(msgs[0].message.data.coins) == 2
assert len(msgs[0].message.data.coins[0][1]) == 3
assert msgs[0].message.data.proofs is not None
assert len(msgs[0].message.data.proofs) == 2
assert confirm_included_already_hashed(
blocks_new[height_with_transactions].header.data.additions_root,
ph_list[0],
msgs[0].message.data.proofs[0][1],
)
assert confirm_included_already_hashed(
blocks_new[height_with_transactions].header.data.additions_root,
hash_coin_list(coin_list_for_ph),
msgs[0].message.data.proofs[0][2],
)
assert confirm_not_included_already_hashed(
blocks_new[height_with_transactions].header.data.additions_root,
ph_list[1],
msgs[0].message.data.proofs[1][1],
)
assert msgs[0].message.data.proofs[1][2] is None